diff --git a/docs/master/api-reference/resolvers.md b/docs/master/api-reference/resolvers.md index e18ab249c7..963695fb3a 100644 --- a/docs/master/api-reference/resolvers.md +++ b/docs/master/api-reference/resolvers.md @@ -10,12 +10,7 @@ Resolvers are always called with the same 4 arguments: use GraphQL\Type\Definition\ResolveInfo; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; -public function resolve( - $rootValue, - array $args, - GraphQLContext $context, - ResolveInfo $resolveInfo -) +function ($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) ``` 1. `$rootValue`: The result that was returned from the parent field. @@ -27,15 +22,15 @@ Lighthouse passes in an instance of `Nuwave\Lighthouse\Schema\Context` by defaul 4. `ResolveInfo $resolveInfo`: Information about the query itself, such as the execution state, the field name, path to the field from the root, and more. +The return value of this must fit the return type defined for the corresponding field from the schema. + ## Complexity function signature The complexity function is used to calculate a query complexity score for a field. You can define your own complexity function with the [@complexity](../api-reference/directives.md#complexity) directive. ```php -<?php - -public function complexity(int $childrenComplexity, array $args): int +function (int $childrenComplexity, array $args): int ``` 1. `$childrenComplexity`: The complexity of the children of the field. In case you expect to return diff --git a/docs/master/getting-started/installation.md b/docs/master/getting-started/installation.md index 4e70e78ae7..14b64da4b7 100644 --- a/docs/master/getting-started/installation.md +++ b/docs/master/getting-started/installation.md @@ -1,5 +1,8 @@ # Installation +The following section teaches you how to install Lighthouse in your project. +Make sure you familiarize yourself with [the basics](../the-basics/schema.md) before diving in. + ## Install via composer ```bash diff --git a/docs/master/guides/plugin-development.md b/docs/master/guides/plugin-development.md index 85d184f68e..f8def94ff8 100644 --- a/docs/master/guides/plugin-development.md +++ b/docs/master/guides/plugin-development.md @@ -31,3 +31,22 @@ You can add your custom directives to Lighthouse by listening for the [`Register Check out [the test suite](https://github.com/nuwave/lighthouse/tree/master/tests/Integration/Events/RegisterDirectiveNamespacesTest.php) for an example of how this works. + +## Change the default resolver + +The first priority when looking for a resolver is always given to `FieldResolver` directives. + +After that, Lighthouse attempts to find a default resolver. + +The interface [`\Nuwave\Lighthouse\Support\Contracts\ProvidesResolver`](../../../src/Support/Contracts/ProvidesResolver.php) +is expected to provide a resolver in case no resolver directive is defined for a field. + +If the field is defined on the root `Query` or `Mutation` types, +Lighthouse's default implementation looks for a class with the capitalized name +of the field in the configured default location. + +Non-root fields fall back to [webonyx's default resolver](http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver). +You may overwrite this by passing a `callable` to `\GraphQL\Executor\Executor::setDefaultFieldResolver`. + +When the field is defined on the root `Subscription` type, the [`\Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver`](../../../src/Support/Contracts/ProvidesSubscriptionResolver.php) +interface is used instead. diff --git a/docs/master/the-basics/fields.md b/docs/master/the-basics/fields.md index 92f4e22615..3538331614 100644 --- a/docs/master/the-basics/fields.md +++ b/docs/master/the-basics/fields.md @@ -1,12 +1,19 @@ # Fields -To fetch data from your GraphQL endpoint, you need to define resolvers for your fields. -Lighthouse makes this easy by providing easy to use, pre-built resolvers that work -great together with your Eloquent models. +The entrypoints to any GraphQL API are the fields of the root types `Query`, `Mutation` and `Subscription`. -## Hello World! +*Every* field has a function associated with it that is called when the field +is requested as part of a query. This function is called a **resolver**. + +The following section will teach you how to define a resolver for your fields +and how you can utilize Lighthouse's built-in resolvers. + +## Resolving fields As is the tradition of our people, this section will teach you how to say "hello world!" through Lighthouse. + +### Schema definition + The following schema defines a simple field called `hello` that returns a `String`. ```graphql @@ -15,8 +22,14 @@ type Query { } ``` -You need to implement the actual resolver next. Lighthouse looks for a class with the capitalized name of the -field in `App\GraphQL\Queries` and calls its `resolve` function. +You need to implement the actual resolver next. + +### Defining resolvers + +By default, Lighthouse looks for a class with the capitalized name of the field in `App\GraphQL\Queries` +or `App\GraphQL\Mutations` and calls its `resolve` function with [the usual resolver arguments](../api-reference/resolvers.md#resolver-function-signature). + +In this case, our field is called `hello` so we need to define our class as follows: ```php <?php @@ -32,6 +45,14 @@ class Hello } ``` +The easiest way to create such a class is to use the built in `artisan` commands +`lighthouse:query` and `lighthouse:mutation`. They both take a single argument: +the name of the field you want to generate. + +For example, this is how you generate a class for the field `hello`: + + php artisan lighthouse:query Hello + Now your schema can be queried. ```graphql @@ -50,6 +71,165 @@ And will return the following response: } ``` +### Fields with arguments + +As we learned, *every* field has a resolver function associated with it. +Just like functions, fields can take arguments to control their behaviour. + +Let's construct a query that greets the user. We add a required argument `name` +that is used to construct the greeting. + +```graphql +type Query { + greet(name: String!): String +} +``` + +A minimal implementation of the field could look something like this. +The skeleton for this class can be created using `php artisan lighthouse:query Greet`. + +The second argument of the resolver function is an associative array of the +arguments that are passed to the query. + +```php +<?php + +namespace App\GraphQL\Queries; + +class Greet +{ + public function resolve($rootValue, array $args): string + { + return "Hello, {$args['name']}!"; + } +} +``` + +We can call this query, passing a `name` of our choosing. + +```graphql +{ + greet(name: "Foo") +} +``` + +And receive a friendly greeting. + +```json +{ + "data": { + "greet": "Hello, Foo!" + } +} +``` + +If we don't want to require the user to pass an argument, we can modify our schema +and make the `name` optional and provide a default value. + +```graphql +type Query { + greet(name: String = "you"): String +} +``` + +Now we can use our query like this: + +```graphql +{ + greet +} +``` + +```json +{ + "data": { + "greet": "Hello, you!" + } +} +``` + +### Resolving non-root fields + +As mentioned, every field in the schema has a resolver - but what +about fields that are not on one of the root types? + +```graphql +type Query { + user: User! +} + +type User { + id: ID! + name: String! + email: String +} +``` + +Let's play through what happens when the client send's the following query: + +```graphql +{ + user { + id + name + } +} +``` + +First, the resolver for `user` will be called. Let's suppose it returns an instance +of `App\Model\User`. + +Next, the field sub-selection will be resolved - the two requested fields are `id` and `name`. +Since we resolved the User already in the parent field, we do not want to fetch it again +to get it's attributes. + +Conveniently, the first argument of each resolver is the return value of the parent +field, in this case a User model. + +A naive implementation of a resolver for `id` might look like this: + +```php +<?php + +use App\Models\User; + +function resolveUserId(User $user): string +{ + return $user->id; +} +``` + +Writing out each such resolver would be pretty repetitive. +We can utilize the fourth and final resolver argument `ResolveInfo`, +which will give us access to the requested field name, +to dynamically access the matching property. + +```php +<?php + +use App\Models\User; +use GraphQL\Type\Definition\ResolveInfo; +use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; + +function resolveUserAttribute(User $user, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) +{ + return $user->{$resolveInfo->fieldName}; +} +``` + +Fortunately, the underlying GraphQL implementation already provides [a sensible default resolver](http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver), +that plays quite nicely with the data you would typically return from +a root resolver, e.g. `Eloquent` models or associative arrays. + +This means that in most cases, you will only have to provide resolvers for the +root fields and make sure they return data in the proper shape. + +If you need to implement custom resolvers for fields that are not on one of the +root types `Query` or `Mutation`, you can use either the +[@field](../api-reference/directives.md#field) or [@method](../api-reference/directives.md#method) directive. + +You may also [change the default resolver](../guides/plugin-development.md#change-the-default-resolver) if you need. + ## Query data Lighthouse provides many resolvers that are already built-in, so you do not have to define them yourself. @@ -102,10 +282,10 @@ Will return the following result: } ``` -### Query with arguments +### Adding query constraints -You may have noticed how every field has to have a resolver function. In many ways, fields are similar to functions. -Just like functions, fields can take arguments to make them more flexible. +Lighthouse provides built-in directives to enhance your queries by giving +additional query capabilities to the client. The following field allows you to fetch a single User by ID. @@ -262,51 +442,3 @@ Lighthouse allows you to serve GraphQL subscriptions. Compared to queries and mutations, a more elaborate setup is required. [Read more about how to set up subscriptions](../extensions/subscriptions.md) - -## Custom resolvers - -Sometimes, the built-in directives just don't cut it - you need more control! -Lighthouse allows you to implement your own resolver function for fields. - -By default, Lighthouse looks for a class with the capitalized name of the field in `App\GraphQL\Queries` -or `App\GraphQL\Mutations` and calls its `resolve` function with [the usual resolver arguments](../api-reference/resolvers.md#resolver-function-signature). -If you stick to that convention, you will not need to specify a directive at all. - -For example, the following field: - -```graphql -type Query { - latestPost: Post! -} -``` - -expects a class like this: - -```php -<?php - -namespace App\GraphQL\Queries; - -use App\Post; -use GraphQL\Type\Definition\ResolveInfo; -use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; - -class LatestPost -{ - public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Post - { - return Post::orderBy('published_at', 'DESC')->first(); - } -} -``` - -The easiest way to create such a class is to use the built in artisan commands -`lighthouse:query` and `lighthouse:mutation`. They both take a single argument: -the name of the field you want to generate. - -For example, this is how you generate a class for the field `latestPost`: - - php artisan lighthouse:query LatestPost - -If you need to implement custom resolvers for fields that are not on one of the -root types `Query` or `Mutation`, you can use the [@field](../api-reference/directives.md#field) directive. diff --git a/docs/master/the-basics/types.md b/docs/master/the-basics/types.md index c19320af9f..904c6a2f5b 100644 --- a/docs/master/the-basics/types.md +++ b/docs/master/the-basics/types.md @@ -7,7 +7,7 @@ look into the [GraphQL documentation](https://graphql.org/learn/schema/) ## Object Type Object types define the resources of your API and are closely related to Eloquent models. -They must have a unique name and have a set of fields. +They must have a unique name and contain a set of fields. ```graphql type User { diff --git a/src/Defer/DeferrableDirective.php b/src/Defer/DeferrableDirective.php index 624848e5f2..f51e67dfc2 100644 --- a/src/Defer/DeferrableDirective.php +++ b/src/Defer/DeferrableDirective.php @@ -51,13 +51,13 @@ public function name(): string */ public function handleField(FieldValue $value, Closure $next): FieldValue { - $resolver = $value->getResolver(); + $previousResolver = $value->getResolver(); $fieldType = $value->getField()->type; $value->setResolver( - function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver, $fieldType) { - $wrappedResolver = function () use ($resolver, $root, $args, $context, $resolveInfo) { - return $resolver($root, $args, $context, $resolveInfo); + function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver, $fieldType) { + $wrappedResolver = function () use ($previousResolver, $root, $args, $context, $resolveInfo) { + return $previousResolver($root, $args, $context, $resolveInfo); }; $path = implode('.', $resolveInfo->path); @@ -67,7 +67,7 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) return $this->defer->isStreaming() ? $this->defer->findOrResolve($wrappedResolver, $path) - : $resolver($root, $args, $context, $resolveInfo); + : $previousResolver($root, $args, $context, $resolveInfo); } ); diff --git a/src/LighthouseServiceProvider.php b/src/LighthouseServiceProvider.php index 3c1334db65..6dbd6f9217 100644 --- a/src/LighthouseServiceProvider.php +++ b/src/LighthouseServiceProvider.php @@ -2,6 +2,7 @@ namespace Nuwave\Lighthouse; +use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Validation\Validator; @@ -14,10 +15,12 @@ use Nuwave\Lighthouse\Console\ScalarCommand; use Illuminate\Contracts\Container\Container; use Nuwave\Lighthouse\Console\MutationCommand; +use Nuwave\Lighthouse\Schema\ResolverProvider; use Nuwave\Lighthouse\Console\InterfaceCommand; use Nuwave\Lighthouse\Execution\ContextFactory; use Nuwave\Lighthouse\Execution\GraphQLRequest; use Nuwave\Lighthouse\Execution\SingleResponse; +use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Console\ClearCacheCommand; use Nuwave\Lighthouse\Console\PrintSchemaCommand; use Nuwave\Lighthouse\Execution\GraphQLValidator; @@ -32,8 +35,10 @@ use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory; use Nuwave\Lighthouse\Support\Contracts\CreatesResponse; use Nuwave\Lighthouse\Schema\Source\SchemaSourceProvider; +use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver; use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse; use Nuwave\Lighthouse\Support\Http\Responses\ResponseStream; +use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; class LighthouseServiceProvider extends ServiceProvider { @@ -123,6 +128,18 @@ public function register(): void ); }); + $this->app->bind(ProvidesResolver::class, ResolverProvider::class); + $this->app->bind(ProvidesSubscriptionResolver::class, function (): ProvidesSubscriptionResolver { + return new class() implements ProvidesSubscriptionResolver { + public function provideSubscriptionResolver(FieldValue $fieldValue): Closure + { + throw new \Exception( + 'Add the SubscriptionServiceProvider to your config/app.php to enable subscriptions.' + ); + } + }; + }); + if ($this->app->runningInConsole()) { $this->commands([ ClearCacheCommand::class, diff --git a/src/Schema/Directives/Fields/AllDirective.php b/src/Schema/Directives/Fields/AllDirective.php index 6e8dac4169..a5b261cdb4 100644 --- a/src/Schema/Directives/Fields/AllDirective.php +++ b/src/Schema/Directives/Fields/AllDirective.php @@ -25,7 +25,6 @@ public function name(): string * Resolve the field directive. * * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue - * * @return \Nuwave\Lighthouse\Schema\Values\FieldValue */ public function resolveField(FieldValue $fieldValue): FieldValue diff --git a/src/Schema/Directives/Fields/AuthDirective.php b/src/Schema/Directives/Fields/AuthDirective.php index 33ef2fc718..5c09badf49 100644 --- a/src/Schema/Directives/Fields/AuthDirective.php +++ b/src/Schema/Directives/Fields/AuthDirective.php @@ -40,7 +40,6 @@ public function name(): string * Resolve the field directive. * * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue - * * @return \Nuwave\Lighthouse\Schema\Values\FieldValue */ public function resolveField(FieldValue $fieldValue): FieldValue diff --git a/src/Schema/Directives/Fields/CanDirective.php b/src/Schema/Directives/Fields/CanDirective.php index 0c15aafa32..ba570c4cc8 100644 --- a/src/Schema/Directives/Fields/CanDirective.php +++ b/src/Schema/Directives/Fields/CanDirective.php @@ -34,11 +34,11 @@ public function name(): string */ public function handleField(FieldValue $value, Closure $next): FieldValue { - $resolver = $value->getResolver(); + $previousResolver = $value->getResolver(); return $next( $value->setResolver( - function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) { + function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver) { $gate = app(Gate::class); $gateArguments = $this->getGateArguments(); @@ -55,7 +55,7 @@ function (string $ability) use ($context, $gate, $gateArguments): void { } ); - return call_user_func_array($resolver, func_get_args()); + return call_user_func_array($previousResolver, func_get_args()); } ) ); diff --git a/src/Schema/Directives/Fields/EventDirective.php b/src/Schema/Directives/Fields/EventDirective.php index 61233e20b7..717e5e916c 100644 --- a/src/Schema/Directives/Fields/EventDirective.php +++ b/src/Schema/Directives/Fields/EventDirective.php @@ -6,15 +6,32 @@ use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware; +use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher; class EventDirective extends BaseDirective implements FieldMiddleware { + /** + * @var \Illuminate\Contracts\Events\Dispatcher + */ + protected $eventsDispatcher; + + /** + * Construct EventDirective. + * + * @param \Illuminate\Contracts\Events\Dispatcher $eventsDispatcher + * @return void + */ + public function __construct(EventsDispatcher $eventsDispatcher) + { + $this->eventsDispatcher = $eventsDispatcher; + } + /** * Name of the directive. * * @return string */ - public function name():string + public function name(): string { return 'event'; } @@ -31,14 +48,20 @@ public function handleField(FieldValue $value, Closure $next): FieldValue { $eventBaseName = $this->directiveArgValue('fire') ?? $this->directiveArgValue('class'); $eventClassName = $this->namespaceClassName($eventBaseName); - $resolver = $value->getResolver(); + $previousResolver = $value->getResolver(); + + return $next( + $value->setResolver( + function () use ($previousResolver, $eventClassName) { + $result = call_user_func_array($previousResolver, func_get_args()); - return $next($value->setResolver(function () use ($resolver, $eventClassName) { - $args = func_get_args(); - $value = call_user_func_array($resolver, $args); - event(new $eventClassName($value)); + $this->eventsDispatcher->dispatch( + new $eventClassName($result) + ); - return $value; - })); + return $result; + } + ) + ); } } diff --git a/src/Schema/Directives/Fields/FirstDirective.php b/src/Schema/Directives/Fields/FirstDirective.php index e352f0abc3..4ae44cb01f 100644 --- a/src/Schema/Directives/Fields/FirstDirective.php +++ b/src/Schema/Directives/Fields/FirstDirective.php @@ -25,7 +25,6 @@ public function name(): string * Resolve the field directive. * * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue - * * @return \Nuwave\Lighthouse\Schema\Values\FieldValue */ public function resolveField(FieldValue $fieldValue): FieldValue diff --git a/src/Schema/Directives/Fields/InjectDirective.php b/src/Schema/Directives/Fields/InjectDirective.php index c1082dc50c..c69590b07c 100644 --- a/src/Schema/Directives/Fields/InjectDirective.php +++ b/src/Schema/Directives/Fields/InjectDirective.php @@ -48,12 +48,12 @@ public function handleField(FieldValue $value, Closure $next): FieldValue ); } - $previousResolvers = $value->getResolver(); + $previousResolver = $value->getResolver(); return $next( $value->setResolver( - function ($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($contextAttributeName, $argumentName, $previousResolvers) { - return $previousResolvers( + function ($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($contextAttributeName, $argumentName, $previousResolver) { + return $previousResolver( $rootValue, Arr::add($args, $argumentName, data_get($context, $contextAttributeName)), $context, diff --git a/src/Schema/Directives/Fields/MethodDirective.php b/src/Schema/Directives/Fields/MethodDirective.php index 2fd60d1f08..961c98fed6 100644 --- a/src/Schema/Directives/Fields/MethodDirective.php +++ b/src/Schema/Directives/Fields/MethodDirective.php @@ -24,7 +24,6 @@ public function name(): string * Resolve the field directive. * * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue - * * @return \Nuwave\Lighthouse\Schema\Values\FieldValue */ public function resolveField(FieldValue $fieldValue): FieldValue diff --git a/src/Schema/Factories/FieldFactory.php b/src/Schema/Factories/FieldFactory.php index ce5de988a8..ea252cf0fc 100644 --- a/src/Schema/Factories/FieldFactory.php +++ b/src/Schema/Factories/FieldFactory.php @@ -23,11 +23,13 @@ use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; use Nuwave\Lighthouse\Support\Contracts\HasErrorBuffer; use Nuwave\Lighthouse\Support\Contracts\HasArgumentPath; +use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver; use Nuwave\Lighthouse\Support\Traits\HasResolverArguments; use Nuwave\Lighthouse\Support\Contracts\ArgFilterDirective; use Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray; use Nuwave\Lighthouse\Support\Contracts\ArgValidationDirective; use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective; +use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; class FieldFactory { @@ -83,17 +85,36 @@ class FieldFactory */ protected $currentHandlerArgsOfArgDirectivesAfterValidationDirective = []; + /** + * @var \Nuwave\Lighthouse\Support\Contracts\ProvidesResolver + */ + protected $providesResolver; + + /** + * @var \Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver + */ + protected $providesSubscriptionResolver; + /** * @param \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory $directiveFactory * @param \Nuwave\Lighthouse\Schema\Factories\ArgumentFactory $argumentFactory * @param \Nuwave\Lighthouse\Support\Pipeline $pipeline + * @param \Nuwave\Lighthouse\Support\Contracts\ProvidesResolver $providesResolver + * @param \Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver $providesSubscriptionResolver * @return void */ - public function __construct(DirectiveFactory $directiveFactory, ArgumentFactory $argumentFactory, Pipeline $pipeline) - { + public function __construct( + DirectiveFactory $directiveFactory, + ArgumentFactory $argumentFactory, + Pipeline $pipeline, + ProvidesResolver $providesResolver, + ProvidesSubscriptionResolver $providesSubscriptionResolver + ) { $this->directiveFactory = $directiveFactory; $this->argumentFactory = $argumentFactory; $this->pipeline = $pipeline; + $this->providesResolver = $providesResolver; + $this->providesSubscriptionResolver = $providesSubscriptionResolver; } /** @@ -104,14 +125,19 @@ public function __construct(DirectiveFactory $directiveFactory, ArgumentFactory */ public function handle(FieldValue $fieldValue): array { - $this->fieldValue = $fieldValue; $fieldDefinitionNode = $fieldValue->getField(); - // Get the initial resolver from the FieldValue - // This is either the webonyx default resolver or provided by a directive - if ($fieldResolver = $this->directiveFactory->createFieldResolver($fieldDefinitionNode)) { - $this->fieldValue = $fieldResolver->resolveField($fieldValue); + // Directives have the first priority for defining a resolver for a field + if ($resolverDirective = $this->directiveFactory->createFieldResolver($fieldDefinitionNode)) { + $this->fieldValue = $resolverDirective->resolveField($fieldValue); + } else { + $this->fieldValue = $fieldValue->setResolver( + $fieldValue->getParentName() === 'Subscription' + ? $this->providesSubscriptionResolver->provideSubscriptionResolver($fieldValue) + : $this->providesResolver->provideResolver($fieldValue) + ); } + $resolver = $this->fieldValue->getResolver(); $argumentValues = $this->getArgumentValues(); diff --git a/src/Schema/ResolverProvider.php b/src/Schema/ResolverProvider.php new file mode 100644 index 0000000000..db360938a9 --- /dev/null +++ b/src/Schema/ResolverProvider.php @@ -0,0 +1,49 @@ +<?php + +namespace Nuwave\Lighthouse\Schema; + +use Closure; +use Illuminate\Support\Str; +use GraphQL\Executor\Executor; +use Nuwave\Lighthouse\Support\Utils; +use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Exceptions\DefinitionException; +use Nuwave\Lighthouse\Support\Contracts\ProvidesResolver; + +class ResolverProvider implements ProvidesResolver +{ + /** + * Provide a field resolver in case no resolver directive is defined for a field. + * + * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue + * @return \Closure + * + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + */ + public function provideResolver(FieldValue $fieldValue): Closure + { + if ($fieldValue->parentIsRootType()) { + $resolverClass = Utils::namespaceClassname( + Str::studly($fieldValue->getFieldName()), + $fieldValue->defaultNamespacesForParent(), + function (string $class): bool { + return method_exists($class, 'resolve'); + } + ); + + if (! $resolverClass) { + throw new DefinitionException( + "Could not locate a default resolver for the field {$fieldValue->getFieldName()}" + ); + } + + return Closure::fromCallable( + [app($resolverClass), 'resolve'] + ); + } + + return Closure::fromCallable( + Executor::getDefaultFieldResolver() + ); + } +} diff --git a/src/Schema/Values/FieldValue.php b/src/Schema/Values/FieldValue.php index d90bf601c9..6eeb4280cd 100644 --- a/src/Schema/Values/FieldValue.php +++ b/src/Schema/Values/FieldValue.php @@ -3,21 +3,10 @@ namespace Nuwave\Lighthouse\Schema\Values; use Closure; -use Illuminate\Support\Str; -use GraphQL\Executor\Executor; use GraphQL\Type\Definition\Type; -use Nuwave\Lighthouse\Support\Utils; -use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Language\AST\StringValueNode; -use Nuwave\Lighthouse\Schema\AST\ASTHelper; use GraphQL\Language\AST\FieldDefinitionNode; -use Nuwave\Lighthouse\Subscriptions\Subscriber; -use Nuwave\Lighthouse\Exceptions\DefinitionException; -use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription; -use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; -use Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry; use Nuwave\Lighthouse\Schema\Conversion\DefinitionNodeConverter; -use Nuwave\Lighthouse\Subscriptions\Exceptions\UnauthorizedSubscriber; class FieldValue { @@ -169,118 +158,11 @@ public function getField(): FieldDefinitionNode * * @return \Closure */ - public function getResolver(): Closure + public function getResolver(): ?Closure { - if (! isset($this->resolver)) { - $this->resolver = $this->defaultResolver(); - } - return $this->resolver; } - /** - * Get default field resolver. - * - * @return \Closure - * - * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException - */ - protected function defaultResolver(): Closure - { - if ($this->getParentName() === 'Subscription') { - return $this->defaultSubscriptionResolver(); - } - - if ($this->parentIsRootType()) { - $resolverClass = Utils::namespaceClassname( - Str::studly($this->getFieldName()), - $this->defaultNamespacesForParent(), - function (string $class): bool { - return method_exists($class, 'resolve'); - } - ); - - if (! $resolverClass) { - throw new DefinitionException( - "Could not locate a default resolver for the field {$this->field->name->value}" - ); - } - - return Closure::fromCallable( - [app($resolverClass), 'resolve'] - ); - } - - return Closure::fromCallable( - [Executor::class, 'defaultFieldResolver'] - ); - } - - /** - * Get the default resolver for a subscription field. - * - * @return \Closure - * - * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException - */ - protected function defaultSubscriptionResolver(): Closure - { - if ($directive = ASTHelper::directiveDefinition($this->field, 'subscription')) { - $className = ASTHelper::directiveArgValue($directive, 'class'); - } else { - $className = Str::studly($this->getFieldName()); - } - - $className = Utils::namespaceClassname( - $className, - $this->defaultNamespacesForParent(), - function (string $class): bool { - return is_subclass_of($class, GraphQLSubscription::class); - } - ); - - if (! $className) { - throw new DefinitionException( - "No class found for the subscription field {$this->getFieldName()}" - ); - } - - /** @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription */ - $subscription = app($className); - /** @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $subscriptionRegistry */ - $subscriptionRegistry = app(SubscriptionRegistry::class); - - // Subscriptions can only be placed on a single field on the root - // query, so there is no need to consider the field path - $subscriptionRegistry->register( - $subscription, - $this->getFieldName() - ); - - return function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($subscription, $subscriptionRegistry) { - if ($root instanceof Subscriber) { - return $subscription->resolve($root->root, $args, $context, $resolveInfo); - } - - $subscriber = new Subscriber( - $args, - $context, - $resolveInfo - ); - - if (! $subscription->can($subscriber)) { - throw new UnauthorizedSubscriber( - 'Unauthorized subscription request' - ); - } - - $subscriptionRegistry->subscriber( - $subscriber, - $subscription->encodeTopic($subscriber, $this->getFieldName()) - ); - }; - } - /** * Return the namespaces configured for the parent type. * @@ -339,7 +221,7 @@ public function getDeprecationReason(): ?string * * @return bool */ - protected function parentIsRootType(): bool + public function parentIsRootType(): bool { return in_array( $this->getParentName(), diff --git a/src/Subscriptions/SubscriptionResolverProvider.php b/src/Subscriptions/SubscriptionResolverProvider.php new file mode 100644 index 0000000000..d837390bd4 --- /dev/null +++ b/src/Subscriptions/SubscriptionResolverProvider.php @@ -0,0 +1,100 @@ +<?php + +namespace Nuwave\Lighthouse\Subscriptions; + +use Closure; +use Illuminate\Support\Str; +use Nuwave\Lighthouse\Support\Utils; +use GraphQL\Type\Definition\ResolveInfo; +use Nuwave\Lighthouse\Schema\AST\ASTHelper; +use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Exceptions\DefinitionException; +use Nuwave\Lighthouse\Schema\Types\GraphQLSubscription; +use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; +use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; +use Nuwave\Lighthouse\Subscriptions\Exceptions\UnauthorizedSubscriber; + +class SubscriptionResolverProvider implements ProvidesSubscriptionResolver +{ + /** + * @var \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry + */ + protected $subscriptionRegistry; + + /** + * ResolverProvider constructor. + * + * @param \Nuwave\Lighthouse\Subscriptions\SubscriptionRegistry $subscriptionRegistry + * @return void + */ + public function __construct(SubscriptionRegistry $subscriptionRegistry) + { + $this->subscriptionRegistry = $subscriptionRegistry; + } + + /** + * Provide a field resolver in case no resolver directive is defined for a field. + * + * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue + * @return \Closure + * + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException + */ + public function provideSubscriptionResolver(FieldValue $fieldValue): Closure + { + $fieldName = $fieldValue->getFieldName(); + + if ($directive = ASTHelper::directiveDefinition($fieldValue->getField(), 'subscription')) { + $className = ASTHelper::directiveArgValue($directive, 'class'); + } else { + $className = Str::studly($fieldName); + } + + $className = Utils::namespaceClassname( + $className, + $fieldValue->defaultNamespacesForParent(), + function (string $class): bool { + return is_subclass_of($class, GraphQLSubscription::class); + } + ); + + if (! $className) { + throw new DefinitionException( + "No class found for the subscription field {$fieldName}" + ); + } + + /** @var \Nuwave\Lighthouse\Schema\Types\GraphQLSubscription $subscription */ + $subscription = app($className); + + // Subscriptions can only be placed on a single field on the root + // query, so there is no need to consider the field path + $this->subscriptionRegistry->register( + $subscription, + $fieldName + ); + + return function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($subscription, $fieldName) { + if ($root instanceof Subscriber) { + return $subscription->resolve($root->root, $args, $context, $resolveInfo); + } + + $subscriber = new Subscriber( + $args, + $context, + $resolveInfo + ); + + if (! $subscription->can($subscriber)) { + throw new UnauthorizedSubscriber( + 'Unauthorized subscription request' + ); + } + + $this->subscriptionRegistry->subscriber( + $subscriber, + $subscription->encodeTopic($subscriber, $fieldName) + ); + }; + } +} diff --git a/src/Subscriptions/SubscriptionServiceProvider.php b/src/Subscriptions/SubscriptionServiceProvider.php index 0a6c4e45aa..5414ee9cb7 100644 --- a/src/Subscriptions/SubscriptionServiceProvider.php +++ b/src/Subscriptions/SubscriptionServiceProvider.php @@ -12,6 +12,7 @@ use Nuwave\Lighthouse\Subscriptions\Contracts\ContextSerializer; use Nuwave\Lighthouse\Subscriptions\Contracts\StoresSubscriptions; use Nuwave\Lighthouse\Subscriptions\Contracts\SubscriptionIterator; +use Nuwave\Lighthouse\Support\Contracts\ProvidesSubscriptionResolver; use Nuwave\Lighthouse\Support\Contracts\SubscriptionExceptionHandler; use Nuwave\Lighthouse\Subscriptions\Contracts\AuthorizesSubscriptions; use Nuwave\Lighthouse\Subscriptions\Contracts\BroadcastsSubscriptions; @@ -72,5 +73,6 @@ public function register(): void $this->app->bind(SubscriptionIterator::class, SyncIterator::class); $this->app->bind(SubscriptionExceptionHandler::class, ExceptionHandler::class); $this->app->bind(BroadcastsSubscriptions::class, SubscriptionBroadcaster::class); + $this->app->bind(ProvidesSubscriptionResolver::class, SubscriptionResolverProvider::class); } } diff --git a/src/Support/Contracts/ProvidesResolver.php b/src/Support/Contracts/ProvidesResolver.php new file mode 100644 index 0000000000..fa7ed3206e --- /dev/null +++ b/src/Support/Contracts/ProvidesResolver.php @@ -0,0 +1,17 @@ +<?php + +namespace Nuwave\Lighthouse\Support\Contracts; + +use Closure; +use Nuwave\Lighthouse\Schema\Values\FieldValue; + +interface ProvidesResolver +{ + /** + * Provide a field resolver in case no resolver directive is defined for a field. + * + * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue + * @return \Closure + */ + public function provideResolver(FieldValue $fieldValue): Closure; +} diff --git a/src/Support/Contracts/ProvidesSubscriptionResolver.php b/src/Support/Contracts/ProvidesSubscriptionResolver.php new file mode 100644 index 0000000000..196cd146ca --- /dev/null +++ b/src/Support/Contracts/ProvidesSubscriptionResolver.php @@ -0,0 +1,17 @@ +<?php + +namespace Nuwave\Lighthouse\Support\Contracts; + +use Closure; +use Nuwave\Lighthouse\Schema\Values\FieldValue; + +interface ProvidesSubscriptionResolver +{ + /** + * Provide a field resolver for subscriptions. + * + * @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue + * @return \Closure + */ + public function provideSubscriptionResolver(FieldValue $fieldValue): Closure; +} diff --git a/tests/Integration/CustomDefaultResolverTest.php b/tests/Integration/CustomDefaultResolverTest.php new file mode 100644 index 0000000000..41bb8b544d --- /dev/null +++ b/tests/Integration/CustomDefaultResolverTest.php @@ -0,0 +1,56 @@ +<?php + +namespace Tests\Integration; + +use Tests\TestCase; +use GraphQL\Executor\Executor; + +class CustomDefaultResolverTest extends TestCase +{ + const CUSTOM_RESOLVER_RESULT = 123; + + protected $schema = ' + type Query { + foo: Foo @field(resolver: "Tests\\\\Integration\\\\CustomDefaultResolverTest@resolve") + } + + type Foo { + bar: Int + } + '; + + public function resolve(): array + { + return [ + 'bar' => 'This should not be returned.', + ]; + } + + /** + * @test + */ + public function itCanSpecifyACustomDefaultResolver(): void + { + $previous = Executor::getDefaultFieldResolver(); + + Executor::setDefaultFieldResolver(function (): int { + return self::CUSTOM_RESOLVER_RESULT; + }); + + $this->query(' + { + foo { + bar + } + } + ')->assertJson([ + 'data' => [ + 'foo' => [ + 'bar' => self::CUSTOM_RESOLVER_RESULT, + ], + ], + ]); + + Executor::setDefaultFieldResolver($previous); + } +} diff --git a/tests/Unit/Schema/Values/FieldValueTest.php b/tests/Unit/Schema/ResolverProviderTest.php similarity index 70% rename from tests/Unit/Schema/Values/FieldValueTest.php rename to tests/Unit/Schema/ResolverProviderTest.php index 97cce1a46b..dd4908f68b 100644 --- a/tests/Unit/Schema/Values/FieldValueTest.php +++ b/tests/Unit/Schema/ResolverProviderTest.php @@ -1,16 +1,29 @@ <?php -namespace Tests\Unit\Schema\Values; +namespace Tests\Unit\Schema; use Closure; use Tests\TestCase; +use Nuwave\Lighthouse\Schema\ResolverProvider; use Nuwave\Lighthouse\Schema\Values\NodeValue; use Nuwave\Lighthouse\Schema\AST\PartialParser; use Nuwave\Lighthouse\Schema\Values\FieldValue; use Nuwave\Lighthouse\Exceptions\DefinitionException; -class FieldValueTest extends TestCase +class ResolverProviderTest extends TestCase { + /** + * @var \Nuwave\Lighthouse\Schema\ResolverProvider + */ + private $resolverProvider; + + protected function setUp(): void + { + parent::setUp(); + + $this->resolverProvider = new ResolverProvider(); + } + /** * @test */ @@ -20,7 +33,7 @@ public function itGetsTheWebonyxDefaultResolverForNonRootFields(): void $this->assertInstanceOf( Closure::class, - $fieldValue->getResolver() + $this->resolverProvider->provideResolver($fieldValue) ); } @@ -33,7 +46,7 @@ public function itGetsTheConventionBasedDefaultResolverForRootFields(): void $this->assertInstanceOf( Closure::class, - $fieldValue->getResolver() + $this->resolverProvider->provideResolver($fieldValue) ); } @@ -46,7 +59,7 @@ public function itLooksAtMultipleNamespacesWhenLookingForDefaultFieldResolvers() $this->assertInstanceOf( Closure::class, - $fieldValue->getResolver() + $this->resolverProvider->provideResolver($fieldValue) ); } @@ -57,7 +70,9 @@ public function itThrowsIfRootFieldHasNoResolver(): void { $this->expectException(DefinitionException::class); - $this->constructFieldValue('noFieldClass: Int')->getResolver(); + $this->resolverProvider->provideResolver( + $this->constructFieldValue('noFieldClass: Int') + ); } protected function constructFieldValue(string $fieldDefinition, string $parentTypeName = 'Query'): FieldValue