From 1768a434e2fd5f8816dc0c062c1bf3169ce2e86d Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 12:40:46 +0200 Subject: [PATCH 1/7] update graphql-php to v15 --- composer.json | 2 +- composer.lock | 52 ++++++++++++------- src/Gql/Directives/FormatDateTime.php | 13 +++-- src/Gql/Directives/Markdown.php | 9 ++-- src/Gql/Directives/Money.php | 9 ++-- src/Gql/Directives/StripTags.php | 5 +- src/Gql/Directives/Transform.php | 12 ----- src/Gql/Gql.php | 5 +- src/Gql/Mutations/Mutation.php | 9 +++- src/Gql/Resolvers/ElementMutationResolver.php | 8 +-- src/Gql/Types/DateTime.php | 10 +--- src/Gql/Types/Money.php | 10 +--- src/Gql/Types/Number.php | 10 +--- src/Gql/Types/QueryArgument.php | 10 +--- 14 files changed, 73 insertions(+), 91 deletions(-) diff --git a/composer.json b/composer.json index 15511b6da1b..c9e7d0e1d3e 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,7 @@ "twig/twig": "~3.21.1", "voku/portable-ascii": "^2.0", "web-auth/webauthn-lib": "~5.2.4", - "webonyx/graphql-php": "~14.11.10", + "webonyx/graphql-php": "~15.31.5", "yiisoft/arrays": "^3.2", "yiisoft/html": "^3.11", "yiisoft/translator": "^3.2", diff --git a/composer.lock b/composer.lock index e9a49eefee8..a59d1c0360c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a6c4f08c1188b01800e240ddb00392ca", + "content-hash": "2a3229efa36c81c382736e0f1c09c15e", "packages": [ { "name": "bacon/bacon-qr-code", @@ -9463,38 +9463,48 @@ }, { "name": "webonyx/graphql-php", - "version": "v14.11.10", + "version": "v15.31.5", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19" + "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19", - "reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9", + "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "php": "^7.1 || ^8" + "php": "^7.4 || ^8" }, "require-dev": { - "amphp/amp": "^2.3", - "doctrine/coding-standard": "^6.0", - "nyholm/psr7": "^1.2", + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.94.2", + "mll-lab/php-cs-fixer-config": "5.13.0", + "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.82", - "phpstan/phpstan-phpunit": "0.12.18", - "phpstan/phpstan-strict-rules": "0.12.9", - "phpunit/phpunit": "^7.2 || ^8.5", - "psr/http-message": "^1.0", - "react/promise": "2.*", - "simpod/php-coveralls-mirror": "^3.0" + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.1.46", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.10", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7 || ^8", + "thecodingmachine/safe": "^1.3 || ^2 || ^3", + "ticketswap/phpstan-error-formatter": "1.3.0" }, "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", "psr/http-message": "To use standard GraphQL server", "react/promise": "To leverage async resolving on React PHP platform" }, @@ -9516,15 +9526,19 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v14.11.10" + "source": "https://github.com/webonyx/graphql-php/tree/v15.31.5" }, "funding": [ + { + "url": "https://github.com/spawnia", + "type": "github" + }, { "url": "https://opencollective.com/webonyx-graphql-php", "type": "open_collective" } ], - "time": "2023-07-05T14:23:37+00:00" + "time": "2026-04-11T18:06:15+00:00" }, { "name": "yiisoft/aliases", diff --git a/src/Gql/Directives/FormatDateTime.php b/src/Gql/Directives/FormatDateTime.php index 18715616d4e..62c3844ccbc 100644 --- a/src/Gql/Directives/FormatDateTime.php +++ b/src/Gql/Directives/FormatDateTime.php @@ -12,7 +12,6 @@ use DateTime; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive as GqlDirective; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -32,23 +31,23 @@ public static function create(): GqlDirective DirectiveLocation::FIELD, ], 'args' => [ - new FieldArgument([ + [ 'name' => 'format', 'type' => Type::string(), 'defaultValue' => self::DEFAULT_FORMAT, 'description' => 'The format to use. Can be `short`, `medium`, `long`, `full`, an [ICU date format](http://userguide.icu-project.org/formatparse/datetime), or a [PHP date format](https://www.php.net/manual/en/function.date.php). Defaults to the [Atom date time format](https://www.php.net/manual/en/class.datetimeinterface.php#datetime.constants.atom]).', - ]), - new FieldArgument([ + ], + [ 'name' => 'timezone', 'type' => Type::string(), 'description' => 'The full name of the timezone (e.g., America/New_York). Defaults to '.self::defaultTimeZone().' if no timezone set on the field.', 'defaultValue' => self::defaultTimeZone(), - ]), - new FieldArgument([ + ], + [ 'name' => 'locale', 'type' => Type::string(), 'description' => 'The locale to use when formatting the date. (E.g., en-US)', - ]), + ], ], 'description' => 'Formats a date in the desired format. Can be applied to all fields, only changes output of DateTime fields.', ])); diff --git a/src/Gql/Directives/Markdown.php b/src/Gql/Directives/Markdown.php index 8dfa43725cf..690832b9c83 100644 --- a/src/Gql/Directives/Markdown.php +++ b/src/Gql/Directives/Markdown.php @@ -8,7 +8,6 @@ use CraftCms\Cms\Support\Facades\Markdown as MarkdownFacade; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive as GqlDirective; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -28,18 +27,18 @@ public static function create(): GqlDirective DirectiveLocation::FIELD, ], 'args' => [ - new FieldArgument([ + [ 'name' => 'flavor', 'type' => Type::string(), 'defaultValue' => self::DEFAULT_FLAVOR, 'description' => 'The “flavor” of Markdown the input should be interpreted with. Accepts the same flavor names as `CraftCms\\Cms\\Support\\Facades\\Markdown::parse()`.', - ]), - new FieldArgument([ + ], + [ 'name' => 'inlineOnly', 'type' => Type::boolean(), 'defaultValue' => self::DEFAULT_INLINE_ONLY, 'description' => 'Whether to only parse inline elements, omitting any `

` tags.', - ]), + ], ], 'description' => 'Parses the passed field value as Markdown.', ])); diff --git a/src/Gql/Directives/Money.php b/src/Gql/Directives/Money.php index b02b8c0be86..ab1069d921c 100644 --- a/src/Gql/Directives/Money.php +++ b/src/Gql/Directives/Money.php @@ -9,7 +9,6 @@ use CraftCms\Cms\Support\Money as MoneyHelper; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive as GqlDirective; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -33,17 +32,17 @@ public static function create(): GqlDirective DirectiveLocation::FIELD, ], 'args' => [ - new FieldArgument([ + [ 'name' => 'format', 'type' => Type::string(), 'defaultValue' => self::FORMAT_STRING, 'description' => 'This specifies the format to output. This can be `amount`, `decimal`, `number`, or `string`. It defaults to the `string`.', - ]), - new FieldArgument([ + ], + [ 'name' => 'locale', 'type' => Type::string(), 'description' => 'The locale to use when formatting the money value. (e.g. `en_US`). This argument is only valid with `number` and `string` formats.', - ]), + ], ], 'description' => 'Formats a money object to the desired format. It can be applied to any fields, but only changes a Money field.', ])); diff --git a/src/Gql/Directives/StripTags.php b/src/Gql/Directives/StripTags.php index 45a6ca2ecfa..41208888dba 100644 --- a/src/Gql/Directives/StripTags.php +++ b/src/Gql/Directives/StripTags.php @@ -7,7 +7,6 @@ use CraftCms\Cms\Gql\GqlEntityRegistry; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive as GqlDirective; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -24,12 +23,12 @@ public static function create(): GqlDirective ], 'description' => 'Strips HTML tags from the field value.', 'args' => [ - new FieldArgument([ + [ 'name' => 'allowed', 'type' => Type::listOf(Type::string()), 'defaultValue' => [], 'description' => 'List of allowed tag names.', - ]), + ], ], ])); } diff --git a/src/Gql/Directives/Transform.php b/src/Gql/Directives/Transform.php index 02ca9e72b68..f13ead83336 100644 --- a/src/Gql/Directives/Transform.php +++ b/src/Gql/Directives/Transform.php @@ -11,23 +11,11 @@ use CraftCms\Cms\Gql\GqlHelper as Gql; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive as GqlDirective; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Support\Collection; class Transform extends Directive { - public function __construct(array $config) - { - $args = &$config['args']; - - foreach ($args as &$argument) { - $argument = new FieldArgument($argument); - } - - parent::__construct($config); - } - public static function create(): GqlDirective { $typeName = static::name(); diff --git a/src/Gql/Gql.php b/src/Gql/Gql.php index 2e3665f089e..21fbcfe69c1 100644 --- a/src/Gql/Gql.php +++ b/src/Gql/Gql.php @@ -197,7 +197,8 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f if (isset($item['args'])) { foreach ($item['args'] as $arg) { if ($arg instanceof InputObjectType) { - TypeInfo::extractTypes($arg); + $typeMap = []; + TypeInfo::extractTypes($arg, $typeMap); } } } @@ -1126,7 +1127,7 @@ private function _loadGqlDirectives(?GqlSchema $schema): array event($event = new RegisterGqlDirectives(directives: $directiveClasses)); $directiveClasses = $event->directives; - $directives = GraphQL::getStandardDirectives(); + $directives = GqlDirective::builtInDirectives(); foreach ($directiveClasses as $class) { /** @var class-string $class */ diff --git a/src/Gql/Mutations/Mutation.php b/src/Gql/Mutations/Mutation.php index 49d3996ec07..3cf90cd64fe 100644 --- a/src/Gql/Mutations/Mutation.php +++ b/src/Gql/Mutations/Mutation.php @@ -8,6 +8,7 @@ use CraftCms\Cms\Gql\Concerns\HasGqlType; use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver; use CraftCms\Cms\Gql\Resolvers\MutationResolver; +use GraphQL\Type\Definition\WrappingType; abstract class Mutation { @@ -29,7 +30,13 @@ protected static function prepareResolver(MutationResolver $resolver, array $con $contentFieldType = $contentField->getContentGqlMutationArgumentType(); $handle = $contentField->handle; $fieldList[$handle] = $contentFieldType; - $configArray = is_array($contentFieldType) ? $contentFieldType : $contentFieldType->config; + if (is_array($contentFieldType)) { + $configArray = $contentFieldType; + } elseif ($contentFieldType instanceof WrappingType) { + $configArray = $contentFieldType->getWrappedType()->config; + } else { + $configArray = $contentFieldType->config; + } if (is_array($configArray) && ! empty($configArray['normalizeValue'])) { $resolver->setValueNormalizer($handle, $configArray['normalizeValue']); diff --git a/src/Gql/Resolvers/ElementMutationResolver.php b/src/Gql/Resolvers/ElementMutationResolver.php index b8c7ce76a66..0c8cf6acc1f 100644 --- a/src/Gql/Resolvers/ElementMutationResolver.php +++ b/src/Gql/Resolvers/ElementMutationResolver.php @@ -12,7 +12,7 @@ use CraftCms\Cms\Gql\Exceptions\GqlException; use CraftCms\Cms\Support\Facades\Elements; use GraphQL\Error\UserError; -use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\Argument; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -139,12 +139,12 @@ private function _traverseAndNormalizeArguments(array $argumentDefinitions, arra $normalized = []; // Keep track of known argument names and the corresponding input types. - /** @var FieldArgument $argumentDefinition */ + /** @var Argument $argumentDefinition */ foreach ($argumentDefinitions as $argumentDefinition) { $typeDef = $argumentDefinition->getType(); - if ($typeDef instanceof WrappingType) { - $typeDef = $typeDef->getWrappedType(true); + while ($typeDef instanceof WrappingType) { + $typeDef = $typeDef->getWrappedType(); } $this->argumentTypeDefsByName[$argumentDefinition->name] = $typeDef; diff --git a/src/Gql/Types/DateTime.php b/src/Gql/Types/DateTime.php index 58e76a05fe9..625770c76d0 100644 --- a/src/Gql/Types/DateTime.php +++ b/src/Gql/Types/DateTime.php @@ -16,17 +16,11 @@ class DateTime extends ScalarType implements SingularTypeInterface { - /** - * @var string - */ #[Override] - public $name = 'DateTime'; + public string $name = 'DateTime'; - /** - * @var string - */ #[Override] - public $description = 'The `DateTime` scalar type represents a point in time.'; + public ?string $description = 'The `DateTime` scalar type represents a point in time.'; /** * @var bool Whether parsed dates should be set to the system time zone diff --git a/src/Gql/Types/Money.php b/src/Gql/Types/Money.php index 5d2381a755b..9af5ef8bd86 100644 --- a/src/Gql/Types/Money.php +++ b/src/Gql/Types/Money.php @@ -17,17 +17,11 @@ class Money extends ScalarType implements SingularTypeInterface { - /** - * @var string - */ #[Override] - public $name = 'Money'; + public string $name = 'Money'; - /** - * @var string - */ #[Override] - public $description = 'The `Money` scalar type represents a money value as a string.'; + public ?string $description = 'The `Money` scalar type represents a money value as a string.'; public static function getType(): Money { diff --git a/src/Gql/Types/Number.php b/src/Gql/Types/Number.php index 1776c822c8e..47c27996886 100644 --- a/src/Gql/Types/Number.php +++ b/src/Gql/Types/Number.php @@ -16,17 +16,11 @@ class Number extends ScalarType implements SingularTypeInterface { - /** - * @var string - */ #[Override] - public $name = 'Number'; + public string $name = 'Number'; - /** - * @var string - */ #[Override] - public $description = 'The `Number` scalar type represents a number that can be a float, an integer or a null value.'; + public ?string $description = 'The `Number` scalar type represents a number that can be a float, an integer or a null value.'; public static function getType(): Number { diff --git a/src/Gql/Types/QueryArgument.php b/src/Gql/Types/QueryArgument.php index a434c674dd9..fac07a68c29 100644 --- a/src/Gql/Types/QueryArgument.php +++ b/src/Gql/Types/QueryArgument.php @@ -15,17 +15,11 @@ class QueryArgument extends ScalarType implements SingularTypeInterface { - /** - * @var string - */ #[Override] - public $name = 'QueryArgument'; + public string $name = 'QueryArgument'; - /** - * @var string - */ #[Override] - public $description = 'The `QueryArgument` scalar type represents a value to be using in Craft element queries. It can be an integer, a string, or a boolean value.'; + public ?string $description = 'The `QueryArgument` scalar type represents a value to be using in Craft element queries. It can be an integer, a string, or a boolean value.'; public static function getType(): QueryArgument { From aa6dfe444187469dc8615d3dca574948006f1466 Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 13:10:31 +0200 Subject: [PATCH 2/7] phpstan --- src/Gql/ElementQueryConditionBuilder.php | 8 +++----- src/Gql/Gql.php | 20 ++++++++++++-------- src/Gql/GqlHelper.php | 4 ++-- src/Gql/Mutations/Mutation.php | 10 ++++++---- src/Gql/TypeLoader.php | 3 +++ 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Gql/ElementQueryConditionBuilder.php b/src/Gql/ElementQueryConditionBuilder.php index b47d696a325..1dc9655262e 100644 --- a/src/Gql/ElementQueryConditionBuilder.php +++ b/src/Gql/ElementQueryConditionBuilder.php @@ -34,7 +34,7 @@ use GraphQL\Language\AST\ObjectValueNode; use GraphQL\Language\AST\VariableNode; use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Type\Definition\WrappingType; +use GraphQL\Type\Definition\Type; use InvalidArgumentException; class ElementQueryConditionBuilder extends Component @@ -265,11 +265,9 @@ private function _prepareTransformArguments(array $arguments): array private function _isInsideAssetQuery(): bool { - if ($this->_resolveInfo->returnType instanceof WrappingType) { - return $this->_resolveInfo->returnType->getWrappedType()->name === AssetInterface::getName(); - } + $namedType = Type::getNamedType($this->_resolveInfo->returnType); - return $this->_resolveInfo->returnType->name === AssetInterface::getName(); + return $namedType !== null && $namedType->name() === AssetInterface::getName(); } /** diff --git a/src/Gql/Gql.php b/src/Gql/Gql.php index 21fbcfe69c1..b4caaf3b482 100644 --- a/src/Gql/Gql.php +++ b/src/Gql/Gql.php @@ -55,6 +55,7 @@ use CraftCms\Cms\Gql\Types\DateTime; use CraftCms\Cms\Gql\Types\Mutation; use CraftCms\Cms\Gql\Types\Number; +use CraftCms\Cms\Gql\Types\ObjectType; use CraftCms\Cms\Gql\Types\Query; use CraftCms\Cms\Gql\Types\QueryArgument; use CraftCms\Cms\ProjectConfig\Events\ConfigEvent; @@ -181,7 +182,7 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f $this->_registerGqlMutations(); $schemaConfig = [ - 'typeLoader' => TypeLoader::class.'::loadType', + 'typeLoader' => TypeLoader::loadType(...), 'query' => TypeLoader::loadType('Query'), 'mutation' => TypeLoader::loadType('Mutation'), 'directives' => $this->_loadGqlDirectives($schema), @@ -193,12 +194,14 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f $this->_schemaDef = new Schema($schemaConfig); // but we always have to add the InputObjectType mutation args - foreach ($schemaConfig['mutation']->config['fields'] as $item) { + /** @var ObjectType $mutation */ + $mutation = $schemaConfig['mutation']; + foreach ($mutation->config['fields'] as $item) { if (isset($item['args'])) { foreach ($item['args'] as $arg) { - if ($arg instanceof InputObjectType) { - $typeMap = []; - TypeInfo::extractTypes($arg, $typeMap); + $argType = Type::getNamedType($arg->getType()); + if ($argType instanceof InputObjectType) { + TypeInfo::extractTypes($argType, $arg); } } } @@ -261,7 +264,7 @@ public function getValidationRules(bool $debug = false, bool $isIntrospectionQue } if (! $generalConfig->enableGraphqlIntrospection && Auth::guest()) { - $validationRules[DisableIntrospection::class] = new DisableIntrospection; + $validationRules[DisableIntrospection::class] = new DisableIntrospection(0); } event($event = new DefineGqlValidationRules( @@ -941,8 +944,9 @@ public function getContentArguments(array $contexts, string $elementType): array } /** - * @param Error[] $errors - * @return Error[] + * @param list $errors + * @param callable(Throwable): array{message: string, locations?: array, path?: array, extensions?: array} $formatter + * @return list, path?: array, extensions?: array}> */ public function handleQueryErrors(array $errors, callable $formatter): array { diff --git a/src/Gql/GqlHelper.php b/src/Gql/GqlHelper.php index 117ab5df518..0721b452e67 100644 --- a/src/Gql/GqlHelper.php +++ b/src/Gql/GqlHelper.php @@ -215,7 +215,6 @@ public static function createFullAccessSchema(): GqlSchema public static function applyDirectives(mixed $source, ResolveInfo $resolveInfo, mixed $value): mixed { - /** @phpstan-ignore-next-line */ if (! isset($resolveInfo->fieldNodes[0]->directives)) { return $value; } @@ -309,7 +308,8 @@ private static function _convertArgumentValue(mixed $value, array $variableValue public static function getFieldNameWithAlias(ResolveInfo $resolveInfo, mixed $source, ?array $context): string { - $fieldName = is_array($resolveInfo->path) ? array_slice($resolveInfo->path, -1)[0] : $resolveInfo->fieldName; + // $resolveInfo->path is either an array or not set, so we need to check if it's set + $fieldName = isset($resolveInfo->path) ? array_slice($resolveInfo->path, -1)[0] : $resolveInfo->fieldName; $isAlias = $fieldName !== $resolveInfo->fieldName; /** @var ElementQueryConditionBuilder|null $conditionBuilder */ diff --git a/src/Gql/Mutations/Mutation.php b/src/Gql/Mutations/Mutation.php index 3cf90cd64fe..b7e7688516b 100644 --- a/src/Gql/Mutations/Mutation.php +++ b/src/Gql/Mutations/Mutation.php @@ -8,6 +8,7 @@ use CraftCms\Cms\Gql\Concerns\HasGqlType; use CraftCms\Cms\Gql\Resolvers\ElementMutationResolver; use CraftCms\Cms\Gql\Resolvers\MutationResolver; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\WrappingType; abstract class Mutation @@ -32,13 +33,14 @@ protected static function prepareResolver(MutationResolver $resolver, array $con $fieldList[$handle] = $contentFieldType; if (is_array($contentFieldType)) { $configArray = $contentFieldType; - } elseif ($contentFieldType instanceof WrappingType) { - $configArray = $contentFieldType->getWrappedType()->config; } else { - $configArray = $contentFieldType->config; + $innerType = $contentFieldType instanceof WrappingType + ? $contentFieldType->getWrappedType() + : $contentFieldType; + $configArray = $innerType instanceof InputObjectType ? $innerType->config : []; } - if (is_array($configArray) && ! empty($configArray['normalizeValue'])) { + if (! empty($configArray) && ! empty($configArray['normalizeValue'])) { $resolver->setValueNormalizer($handle, $configArray['normalizeValue']); } } diff --git a/src/Gql/TypeLoader.php b/src/Gql/TypeLoader.php index 5ae88e2be42..d3a40538e4a 100644 --- a/src/Gql/TypeLoader.php +++ b/src/Gql/TypeLoader.php @@ -5,6 +5,7 @@ namespace CraftCms\Cms\Gql; use CraftCms\Cms\Gql\Exceptions\GqlException; +use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\Type; class TypeLoader @@ -15,6 +16,8 @@ class TypeLoader private static array $_typeLoaders = []; /** + * @phpstan-return NamedType&Type + * * @throws GqlException */ public static function loadType(string $type): Type From 5c568047bfd78955649750fdc93cfff2b782bf83 Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 13:31:01 +0200 Subject: [PATCH 3/7] fix content block and option field tests --- tests/Feature/Gql/Resolvers/Elements/ContentBlockTest.php | 4 ++-- tests/Feature/Gql/Resolvers/OptionFieldTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Gql/Resolvers/Elements/ContentBlockTest.php b/tests/Feature/Gql/Resolvers/Elements/ContentBlockTest.php index b71b947f995..5c5ff327577 100644 --- a/tests/Feature/Gql/Resolvers/Elements/ContentBlockTest.php +++ b/tests/Feature/Gql/Resolvers/Elements/ContentBlockTest.php @@ -19,7 +19,7 @@ function createResolveInfoForContentBlock(string $fieldName): ResolveInfo { $parentType = new ObjectType(['name' => 'Test', 'fields' => []]); - $fieldDefinition = FieldDefinition::create([ + $fieldDefinition = new FieldDefinition([ 'name' => $fieldName, 'type' => Type::string(), ]); @@ -31,7 +31,7 @@ function createResolveInfoForContentBlock(string $fieldName): ResolveInfo return new ResolveInfo( $fieldDefinition, - [$fieldNode], + new ArrayObject($fieldNode), $parentType, ['test', $fieldName], new Schema([]), diff --git a/tests/Feature/Gql/Resolvers/OptionFieldTest.php b/tests/Feature/Gql/Resolvers/OptionFieldTest.php index 984c638a60e..5258303dd6e 100644 --- a/tests/Feature/Gql/Resolvers/OptionFieldTest.php +++ b/tests/Feature/Gql/Resolvers/OptionFieldTest.php @@ -21,7 +21,7 @@ function createResolveInfoForOptionField(string $fieldName): ResolveInfo { $parentType = new ObjectType(['name' => 'Test', 'fields' => []]); - $fieldDefinition = FieldDefinition::create([ + $fieldDefinition = new FieldDefinition([ 'name' => $fieldName, 'type' => Type::string(), ]); @@ -33,7 +33,7 @@ function createResolveInfoForOptionField(string $fieldName): ResolveInfo return new ResolveInfo( $fieldDefinition, - [$fieldNode], + new ArrayObject($fieldNode), $parentType, ['test', $fieldName], new Schema([]), From 19222de0784ab9906e3e17c39f217fd7b49d3643 Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 14:03:31 +0200 Subject: [PATCH 4/7] description is now supported and kind of needed --- src/Gql/Gql.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Gql/Gql.php b/src/Gql/Gql.php index b4caaf3b482..6b67fd3a273 100644 --- a/src/Gql/Gql.php +++ b/src/Gql/Gql.php @@ -192,6 +192,11 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f // as the query is being resolved thanks to the magic of lazy-loading, so we needn't worry. if (! $prebuildSchema) { $this->_schemaDef = new Schema($schemaConfig); + // add default description (use schema name), or DumpSchemaCommandTest and SchemaPrinter::printSchemaDefinition() will error + // because of SchemaPrinter::hasDefaultRootOperationTypes($schema) and "Subscription" + if ($this->_schemaDef->description === null) { + $this->_schemaDef->description = $schema?->name; + } // but we always have to add the InputObjectType mutation args /** @var ObjectType $mutation */ @@ -224,6 +229,11 @@ public function getSchemaDef(?GqlSchema $schema = null, bool $prebuildSchema = f try { $this->_schemaDef = new Schema($schemaConfig); $this->_schemaDef->getTypeMap(); + // add default description (use schema name), or DumpSchemaCommandTest and SchemaPrinter::printSchemaDefinition() will error + // because of SchemaPrinter::hasDefaultRootOperationTypes($schema) and "Subscription" + if ($this->_schemaDef->description === null) { + $this->_schemaDef->description = $schema?->name; + } } catch (Throwable $exception) { throw new GqlException('Failed to validate the GQL Schema - '.$exception->getMessage(), previous: $exception); From 5ebbfed50123c0434ed3ec417a1fdf316697664a Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 14:04:02 +0200 Subject: [PATCH 5/7] typed property for the legacy mock type too --- yii2-adapter/legacy/test/mockclasses/gql/MockType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yii2-adapter/legacy/test/mockclasses/gql/MockType.php b/yii2-adapter/legacy/test/mockclasses/gql/MockType.php index 606c84d01de..b2f43be46eb 100644 --- a/yii2-adapter/legacy/test/mockclasses/gql/MockType.php +++ b/yii2-adapter/legacy/test/mockclasses/gql/MockType.php @@ -13,7 +13,7 @@ class MockType extends ScalarType /** * @var string */ - public $name = 'mockType'; + public string $name = 'mockType'; /** * Returns a singleton instance to ensure one type per schema. From b85d5621654195d4ec56ded895ae70a11a687c2b Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 14:35:50 +0200 Subject: [PATCH 6/7] no need to use while; use getInnermostType() instead --- src/Gql/Resolvers/ElementMutationResolver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gql/Resolvers/ElementMutationResolver.php b/src/Gql/Resolvers/ElementMutationResolver.php index 0c8cf6acc1f..450a7d1d8d9 100644 --- a/src/Gql/Resolvers/ElementMutationResolver.php +++ b/src/Gql/Resolvers/ElementMutationResolver.php @@ -143,8 +143,8 @@ private function _traverseAndNormalizeArguments(array $argumentDefinitions, arra foreach ($argumentDefinitions as $argumentDefinition) { $typeDef = $argumentDefinition->getType(); - while ($typeDef instanceof WrappingType) { - $typeDef = $typeDef->getWrappedType(); + if ($typeDef instanceof WrappingType) { + $typeDef = $typeDef->getInnermostType(); } $this->argumentTypeDefsByName[$argumentDefinition->name] = $typeDef; From 008801db41666101ea7f4be58e16100f79634512 Mon Sep 17 00:00:00 2001 From: i-just Date: Mon, 27 Apr 2026 15:06:30 +0200 Subject: [PATCH 7/7] update package in the adapter too --- yii2-adapter/composer.lock | 54 ++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/yii2-adapter/composer.lock b/yii2-adapter/composer.lock index 916b84b7353..d839737477f 100644 --- a/yii2-adapter/composer.lock +++ b/yii2-adapter/composer.lock @@ -479,7 +479,7 @@ "dist": { "type": "path", "url": "..", - "reference": "52fbc060042b9199686ac7376f94d1245a0abc4a" + "reference": "b85d5621654195d4ec56ded895ae70a11a687c2b" }, "require": { "bacon/bacon-qr-code": "^2.0", @@ -530,7 +530,7 @@ "twig/twig": "~3.21.1", "voku/portable-ascii": "^2.0", "web-auth/webauthn-lib": "~5.2.4", - "webonyx/graphql-php": "~14.11.10", + "webonyx/graphql-php": "~15.31.5", "yiisoft/arrays": "^3.2", "yiisoft/html": "^3.11", "yiisoft/translator": "^3.2", @@ -9584,38 +9584,48 @@ }, { "name": "webonyx/graphql-php", - "version": "v14.11.10", + "version": "v15.31.5", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19" + "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19", - "reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9", + "reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "php": "^7.1 || ^8" + "php": "^7.4 || ^8" }, "require-dev": { - "amphp/amp": "^2.3", - "doctrine/coding-standard": "^6.0", - "nyholm/psr7": "^1.2", + "amphp/amp": "^2.6", + "amphp/http-server": "^2.1", + "dms/phpunit-arraysubset-asserts": "dev-master", + "ergebnis/composer-normalize": "^2.28", + "friendsofphp/php-cs-fixer": "3.94.2", + "mll-lab/php-cs-fixer-config": "5.13.0", + "nyholm/psr7": "^1.5", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.82", - "phpstan/phpstan-phpunit": "0.12.18", - "phpstan/phpstan-strict-rules": "0.12.9", - "phpunit/phpunit": "^7.2 || ^8.5", - "psr/http-message": "^1.0", - "react/promise": "2.*", - "simpod/php-coveralls-mirror": "^3.0" + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "2.1.46", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.10", + "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", + "psr/http-message": "^1 || ^2", + "react/http": "^1.6", + "react/promise": "^2.0 || ^3.0", + "rector/rector": "^2.0", + "symfony/polyfill-php81": "^1.23", + "symfony/var-exporter": "^5 || ^6 || ^7 || ^8", + "thecodingmachine/safe": "^1.3 || ^2 || ^3", + "ticketswap/phpstan-error-formatter": "1.3.0" }, "suggest": { + "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform", "psr/http-message": "To use standard GraphQL server", "react/promise": "To leverage async resolving on React PHP platform" }, @@ -9637,15 +9647,19 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v14.11.10" + "source": "https://github.com/webonyx/graphql-php/tree/v15.31.5" }, "funding": [ + { + "url": "https://github.com/spawnia", + "type": "github" + }, { "url": "https://opencollective.com/webonyx-graphql-php", "type": "open_collective" } ], - "time": "2023-07-05T14:23:37+00:00" + "time": "2026-04-11T18:06:15+00:00" }, { "name": "yiisoft/aliases",