diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7d39e390..d6074bb76c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Changed + +- Override the default field resolver `GraphQL\Executor\Executor::defaultFieldResolver()` with `Nuwave\Lighthouse\LighthouseServiceProvider::defaultFieldResolver()` https://github.com/nuwave/lighthouse/pull/2693 + ## v6.58.0 ### Changed diff --git a/UPGRADE.md b/UPGRADE.md index a770b430cf..626e98719d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -9,6 +9,16 @@ Compare your `lighthouse.php` against the latest [default configuration](src/lig ## v6 to v7 +### Default field resolver changed + +The default field resolver was changed from `GraphQL\Executor\Executor::defaultFieldResolver()` to `Nuwave\Lighthouse\LighthouseServiceProvider::defaultFieldResolver()`. +The new default fields resolver is expected to be mostly compatible with the previous one, but introduces some behavior changes when resolving models: + +| Scenario | Previous behavior | New behavior | +|-----------------------------|---------------------------------------------------------------------------|-----------------------------------------------------------------| +| Accessing a magic attribute | Call `isset()` first, then get the value, resulting in duplicate accesses | Attempt to get the value directly, resulting in a single access | +| Accessing a PHP property | Call `isset()` first which goes into `__isset` and returns `null` | Attempt to get the value directly, returning the property value | + ### Leverage automatic test trait setup Methods you need to explicitly call to set up test traits were removed in favor of automatically set up test traits. diff --git a/docs/master/digging-deeper/extending-lighthouse.md b/docs/master/digging-deeper/extending-lighthouse.md index 5bbc66bef2..14546a548d 100644 --- a/docs/master/digging-deeper/extending-lighthouse.md +++ b/docs/master/digging-deeper/extending-lighthouse.md @@ -35,8 +35,9 @@ final class SomePackageServiceProvider extends ServiceProvider ## Changing the default resolver -Lighthouse will fall back to using [webonyx's default resolver](https://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver) +Lighthouse overrides [webonyx's default resolver](https://webonyx.github.io/graphql-php/data-fetching#default-field-resolver) for non-root fields, [see resolver precedence](../the-basics/fields.md#resolver-precedence). +See `Nuwave\Lighthouse\LighthouseServiceProvider::defaultFieldResolver()` for the implementation. You may override this by calling `GraphQL\Executor\Executor::setDefaultFieldResolver()` in your service provider's `boot()` method. diff --git a/src/LighthouseServiceProvider.php b/src/LighthouseServiceProvider.php index 82d8bdfbc4..514ca32550 100644 --- a/src/LighthouseServiceProvider.php +++ b/src/LighthouseServiceProvider.php @@ -6,9 +6,12 @@ use GraphQL\Error\Error; use GraphQL\Error\ProvidesExtensions; use GraphQL\Executor\ExecutionResult; +use GraphQL\Executor\Executor; +use GraphQL\Utils\Utils; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract; use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Http\JsonResponse; use Illuminate\Support\ServiceProvider; @@ -31,6 +34,7 @@ use Nuwave\Lighthouse\Execution\ContextFactory; use Nuwave\Lighthouse\Execution\ContextSerializer; use Nuwave\Lighthouse\Execution\ErrorPool; +use Nuwave\Lighthouse\Execution\ResolveInfo; use Nuwave\Lighthouse\Execution\SingleResponse; use Nuwave\Lighthouse\Http\Responses\ResponseStream; use Nuwave\Lighthouse\Schema\AST\ASTBuilder; @@ -45,6 +49,7 @@ use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse; use Nuwave\Lighthouse\Support\Contracts\CreatesContext; use Nuwave\Lighthouse\Support\Contracts\CreatesResponse; +use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver; use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; use Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules; @@ -142,6 +147,37 @@ public function boot(ConfigRepository $configRepository, EventsDispatcher $dispa return new JsonResponse($serializableResult); }); } + + Executor::setDefaultFieldResolver([static::class, 'defaultFieldResolver']); // @phpstan-ignore argument.type (callable not recognized) + } + + /** + * The default field resolver for GraphQL queries. + * + * This method is used to resolve fields on the object-like value returned by a resolver. + * It checks if the value is an Eloquent model and retrieves the attribute or property accordingly. + * Otherwise, it falls back to the default behavior from webonyx/graphql-php's default field resolver. + * + * @see \GraphQL\Executor\Executor::defaultFieldResolver() + * + * @param array $args + */ + public static function defaultFieldResolver(mixed $root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): mixed + { + $fieldName = $resolveInfo->fieldName; + + if ($root instanceof Model) { + $property = $root->getAttribute($fieldName); + if ($property === null && property_exists($root, $fieldName)) { + $property = $root->{$fieldName}; + } + } else { + $property = Utils::extractKey($root, $fieldName); + } + + return $property instanceof \Closure + ? $property($root, $args, $context, $resolveInfo) + : $property; } protected function loadRoutesFrom($path): void diff --git a/tests/Integration/Models/PropertyAccessTest.php b/tests/Integration/Models/PropertyAccessTest.php index 105c51fd7b..b89904bc60 100644 --- a/tests/Integration/Models/PropertyAccessTest.php +++ b/tests/Integration/Models/PropertyAccessTest.php @@ -105,8 +105,7 @@ public function testPhpProperty(): void ])->assertJson([ 'data' => [ 'user' => [ - // TODO consider changing the default resolver so this returns User::PHP_PROPERTY_VALUE - 'php_property' => null, + 'php_property' => User::PHP_PROPERTY_VALUE, ], ], ]); @@ -174,7 +173,7 @@ public function testPrefersAttributeAccessorNullThatShadowsPhpProperty(): void ])->assertJson([ 'data' => [ 'user' => [ - 'exists' => null, + 'exists' => true, ], ], ]); @@ -208,8 +207,7 @@ public function testExpensivePropertyIsOnlyCalledOnce(): void ])->assertJson([ 'data' => [ 'user' => [ - // TODO consider changing the default resolver so this returns 1 - 'expensive_property' => 2, + 'expensive_property' => 1, ], ], ]);