From 429d6c04c45944cb955740f8ea9f667f64f69b08 Mon Sep 17 00:00:00 2001 From: Tom H Anderson Date: Fri, 2 Oct 2026 19:06:04 -0600 Subject: [PATCH] Reject a computed field argument default its type cannot represent A scalar default on an argument whose type the attribute's args give, such as '2020-01-01' for a date_immutable argument, passed the metadata checks. Printing the schema then threw, introspection failed at the argument's defaultValue, and the method was given the raw default rather than a value of the type. When the type is built, a default must now be a value of the argument's type: a scalar or enum type which serializes it. Otherwise Exception\Metadata names the argument, its default and its type. Co-Authored-By: Claude Opus 5.5 --- docs/computed-fields.rst | 6 +++- src/Type/Entity/Entity.php | 35 +++++++++++++++++++ test/Entity/ComputedArgsInvalid.php | 16 +++++++++ .../ComputedFieldArgumentsTest.php | 11 ++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/computed-fields.rst b/docs/computed-fields.rst index 182abeb..9c43d51 100644 --- a/docs/computed-fields.rst +++ b/docs/computed-fields.rst @@ -193,7 +193,11 @@ GraphQL arguments are named, so the argument is ``year``, not a position. * A variadic parameter, or one passed by reference, cannot be an argument. These are checked when the metadata is built, and throw -``ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata``. +``ApiSkeletons\Doctrine\ORM\GraphQL\Exception\Metadata``. When the type +is built, an argument's type must be one which can be input, and a default +must be a value of it: ``'2020-01-01'`` is not a value of ``date_immutable``, +whose values are dates, so a parameter given that type in ``args`` may not +have it as its default. These throw the same exception. Each set of arguments has its own value, so aliases of a field with different arguments, such as ``a: totalRecordings(year: 2002) diff --git a/src/Type/Entity/Entity.php b/src/Type/Entity/Entity.php index 08e5b1d..f419fc5 100644 --- a/src/Type/Entity/Entity.php +++ b/src/Type/Entity/Entity.php @@ -24,12 +24,14 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\MappingException; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use Laminas\Hydrator\HydratorInterface; use League\Event\EventDispatcher; use ReflectionClass; +use Throwable; use function array_keys; use function array_merge; @@ -44,6 +46,7 @@ use function str_starts_with; use function substr; use function ucwords; +use function var_export; /** * This class is used to build an ObjectType for an entity @@ -492,12 +495,44 @@ private function computedFieldArgs(string $fieldName, ComputedFieldMetadata $com continue; } + // A default is a value of the argument's type, which introspection + // serializes; the method receives it as a value of the type + if (! self::isValueOf($type, $argument->default)) { + throw new MetadataException( + 'Argument ' . $name . ' of computed field ' . $fieldName . ' of entity ' . $this->getEntityClass() + . ' has the default value ' . var_export($argument->default, true) . ', which is not a value of its ' + . 'type ' . $type->toString() . '. Remove the default, or give the parameter a type of which ' + . 'it is a value.', + ); + } + $args[$name]['defaultValue'] = $argument->default; } return ['args' => $args]; } + /** + * Whether a scalar is a value of a type: the type is a scalar or an enum, + * not a list or an input object, and serializes the value + */ + private static function isValueOf(Type $type, int|float|string|bool $value): bool + { + $type = Type::getNullableType($type); + + if (! $type instanceof LeafType) { + return false; + } + + try { + $type->serialize($value); + } catch (Throwable) { + return false; + } + + return true; + } + /** * The type of a computed field: its type, or a list of it */ diff --git a/test/Entity/ComputedArgsInvalid.php b/test/Entity/ComputedArgsInvalid.php index 5d4d1d2..bab0745 100644 --- a/test/Entity/ComputedArgsInvalid.php +++ b/test/Entity/ComputedArgsInvalid.php @@ -20,6 +20,8 @@ #[GraphQL\Entity(group: 'ComputedArgsObjectDefault')] #[GraphQL\Entity(group: 'ComputedArgsUnknownArg')] #[GraphQL\Entity(group: 'ComputedArgsNotInput')] +#[GraphQL\Entity(group: 'ComputedArgsTypedDefault')] +#[GraphQL\Entity(group: 'ComputedArgsListDefault')] #[ORM\Entity] class ComputedArgsInvalid { @@ -75,4 +77,18 @@ public function getNotInput(mixed $page): int { return 0; } + + /** A default which is not a value of the type its args give it */ + #[GraphQL\ComputedField(type: 'string', group: 'ComputedArgsTypedDefault', args: ['since' => 'date_immutable'])] + public function getTypedDefault(mixed $since = '2020-01-01'): string + { + return (string) $since; + } + + /** A scalar default for a list */ + #[GraphQL\ComputedField(type: 'int', group: 'ComputedArgsListDefault', args: ['tags' => 'simple_array'])] + public function getListDefault(mixed $tags = 'live'): int + { + return 0; + } } diff --git a/test/Feature/ComputedField/ComputedFieldArgumentsTest.php b/test/Feature/ComputedField/ComputedFieldArgumentsTest.php index 82a3f41..650f359 100644 --- a/test/Feature/ComputedField/ComputedFieldArgumentsTest.php +++ b/test/Feature/ComputedField/ComputedFieldArgumentsTest.php @@ -357,6 +357,17 @@ public static function invalidProvider(): array 'The args of the ComputedField attribute of computed field method getUnknownArg of entity ' . ComputedArgsInvalid::class . ' give the type of other, which is not a parameter of the method.', ], + 'default not a value of the given type' => [ + 'ComputedArgsTypedDefault', + 'Argument since of computed field typedDefault of entity ' . ComputedArgsInvalid::class + . " has the default value '2020-01-01', which is not a value of its type DateImmutable. Remove " + . 'the default, or give the parameter a type of which it is a value.', + ], + 'scalar default for a list' => [ + 'ComputedArgsListDefault', + 'Argument tags of computed field listDefault of entity ' . ComputedArgsInvalid::class + . " has the default value 'live', which is not a value of its type [String].", + ], 'not an input type' => [ 'ComputedArgsNotInput', 'Argument page of computed field notInput of entity ' . ComputedArgsInvalid::class