Description
API Platform version(s) affected: 4.x
Description
There is an issue with PHP 8.1 backed enums when used in GraphQL operations with JavaScript/TypeScript clients. While API Platform correctly generates GraphQL schema introspection for PHP backed enums, it uses the basic EnumType
class from webonyx/graphql-php instead of the specialized PhpEnumType
class.
This causes a validation error when sending requests from JavaScript/TypeScript clients because JSON serialization always converts enum values to strings (with quotes), but the EnumType
class expects raw enum values (without quotes).
For example, when a TypeScript enum is sent in a GraphQL mutation:
enum OrderStatus {
PENDING = "PENDING",
PROCESSING = "PROCESSING",
COMPLETED = "COMPLETED"
}
// In client code:
createOrder({ status: OrderStatus.PENDING })
// This becomes { "status": "PENDING" } in JSON
API Platform rejects this with an error:
Enum "OrderStatusEnum" cannot represent non-enum value: "PENDING". Did you mean the enum value "PENDING"?
The error occurs because API Platform uses EnumType
instead of PhpEnumType
, and EnumType
doesn't handle string values automatically.
How to reproduce
- Create a PHP 8.1 backed enum:
<?php
namespace App\Enum;
enum OrderStatus: string
{
case PENDING = 'PENDING';
case PROCESSING = 'PROCESSING';
case COMPLETED = 'COMPLETED';
}
- Use it in an API Resource:
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Mutation;
use App\Enum\OrderStatus;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource(
graphQlOperations: [
new Mutation(name: 'create', args: [
'status' => ['type' => OrderStatus::class]
])
]
)]
class Order
{
#[ORM\Column(type: 'string', enumType: OrderStatus::class)]
private OrderStatus $status;
// Other properties, getters, setters...
}
- Create a TypeScript/JavaScript GraphQL client that uses the enum:
const CREATE_ORDER = gql`
mutation CreateOrder($status: OrderStatusEnum!) {
createOrder(input: { status: $status }) {
order { id }
}
}
`;
// This will fail:
client.mutate({
mutation: CREATE_ORDER,
variables: { status: "PENDING" }
});
The request fails with the error message described above, even though "PENDING" is a valid enum value.
Possible Solution
The root cause is in API Platform's enum type building implementation which uses EnumType
rather than PhpEnumType
for PHP backed enums.
This seems to be handled in the TypeBuilder.php
getEnumType()
method.
The key difference between these classes is in their construction:
EnumType
is constructed with a configuration array:
$enumType = new EnumType([
'name' => $enumName,
'values' => $enumCases,
// ...
]);
While PhpEnumType
is specifically designed for PHP 8.1 enums and is constructed differently:
/**
* @param class-string<\UnitEnum> $enumClass The fully qualified class name of a native PHP enum
* @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class
* @param string|null $description The description the enum will have in the schema, defaults to PHPDoc of the given class
* @param array<EnumTypeExtensionNode>|null $extensionASTNodes
*
* @throws \Exception
* @throws \ReflectionException
*/
$enumType = new PhpEnumType(
OrderStatus::class, // The enum class name (must be a UnitEnum)
$name, // Optional name (defaults to basename of class)
$description // Optional description (defaults to PHPDoc)
);
PhpEnumType
is purpose-built for PHP 8.1 enums - it automatically extracts enum cases from the class through reflection and, most importantly, handles the string-to-enum conversion properly:
public function __construct(
string $enumClass,
?string $name = null,
?string $description = null,
// ...
) {
$this->enumClass = $enumClass;
$reflection = new \ReflectionEnum($enumClass);
// Extract enum cases automatically
$enumDefinitions = [];
foreach ($reflection->getCases() as $case) {
$enumDefinitions[$case->name] = [
'value' => $case->getValue(),
// ...
];
}
parent::__construct([
'name' => $name ?? $this->baseName($enumClass),
'values' => $enumDefinitions,
// ...
]);
}
Due to these constructor differences, a solution would need to:
- Identify when a GraphQL type represents a PHP 8.1 enum
- Use
PhpEnumType
with the enum class name instead of building anEnumType
with a config array
This could be implemented by decorating the appropriate API Platform type builder service to use PhpEnumType
when appropriate.
Additional Context
This issue only affects GraphQL operations when using JavaScript/TypeScript clients. REST operations work correctly because they use Symfony's serializer which properly handles enum deserialization.
The author of webonyx/graphql-php confirmed that the PhpEnumType
class is specifically designed to handle this case, as evidenced by the tests in: