-
-
Notifications
You must be signed in to change notification settings - Fork 266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for field directives #860
Conversation
Hi! it is a pleasure that someone has had the desire to pick up the thing again. |
The differences with the previous PR are:
@illambo I hope this answers your question! |
Yes! thanks for the quick reply and for continuing development in this. |
Just a quick no-update update: I've not forgotten, still on my list, but it's a "bad time of the year" timing and it's not that I just have to review handful lines of code. Thanks for you patience 🙏🏼 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, really nice PR! This is a preliminary review until I better understand it, please see my feedback below and inline!
I was able to follow some of the parts but not yet all of it.
I'm surprised to understand (or: maybe I got it wrong) that the graphql-php default field resolver is responsible for resoling the field directives. I would have though this would be built-in somehow.
Because if I got this right, this would mean directive support depends on the default field resolver and if a custom one is provided and does not include the support for it, it won't work.
Can you explain this in more detail?
Also, do you have an idea what it would need to support other kind of directives?
Thank you!
- Please import all FQCN (I saw it multiple times but only gave feedback in one place)
- I noticed that the
FieldResolver
andDirectiveHandler
are not registered in the service provider. This means if running under a different architecture, like in Laravel Octane via e.r. roadrunner, we create multiple instances but by and large, they never really change?
TL;DR: my question here is: can they be made singletons?
@@ -2521,6 +2522,96 @@ class AppServiceProvider extends ServiceProvider | |||
|
|||
The `macro` function accepts a name as its first argument, and a `Closure` as its second. | |||
|
|||
### Directives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed explanation here in the README!
However I see that in the three parts of the example how to add a custom directive, you used three different functionalities and none completes the other: CapitalizeDirective::class
, TrimDirective
and finally @upper
in the query example.
Is there a reason for this or can we harmonize this, so the example is one working thing?
declare(strict_types = 1); | ||
namespace Rebing\GraphQL\Support; | ||
|
||
abstract class Directive extends \GraphQL\Type\Definition\Directive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please import:
abstract class Directive extends \GraphQL\Type\Definition\Directive | |
abstract class Directive extends BaseDirective |
/** | ||
* Specify the locations where this directive can be applied. | ||
* | ||
* @return array<string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be a good place to document which one we exactly support, in a way static analyzers can also make use if it.
Can we express (only) the supported constants from \GraphQL\Language\DirectiveLocation
here?
public function handle($value, array $args = []): ?string | ||
{ | ||
if (\is_string($value)) { | ||
return strtolower($value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mb_strtolower
?
return [ | ||
'chars' => [ | ||
'type' => Type::string(), | ||
'description' => 'Trim field by given characters.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably make sense to document the defaults of the trim
function here, too
|
||
private function getDefaultFieldResolver(Schema $schema): callable | ||
{ | ||
return $this->config->get('graphql.defaultFieldResolver') ?? app(FieldResolver::class)->setSchema($schema); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does the directive support work in case a custom field resolver is provided?
I get the impression the "location \GraphQL\Language\DirectiveLocation::FIELD
" support depends on the field resolver doing the actual work of applying the directives (looking at \Rebing\GraphQL\Support\FieldResolver\FieldResolver::__invoke
), which means a custom field resolver would disable this?
public function getDirective(string $name): ?Directive | ||
{ | ||
return $this->directives[$name] ?? null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would it be a good idea to silence a directive which isn't present? Shouldn't we throw an exception instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the designed usage you can never run into the situation that the directive isn't present, because the query validation will be executed before. But since the method is public I added it to prevent the "undefined index" exception from being thrown, and anyone can decide how to handle the null value in its application.
use GraphQL\Type\Definition\ResolveInfo; | ||
use GraphQL\Type\Schema; | ||
|
||
class FieldResolver extends Executor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the design we're extending the Executor just so we can call self::defaultFieldResolver
.
This seems rather "heavy" and also misleading, as the actual Executor being invoked is (seems to me) unrelated to this resolver implementation which feels confusing.
I think just calling \GraphQL\Executor\Executor::defaultFieldResolver
directly would make more sense. If we're concerned about extending/customizing the FieldResolver
itself, we could always make a protected function callDefaultFieldResolver
so the user can override it.
This actually brings me back to my other question from \Rebing\GraphQL\Support\ExecutionMiddleware\GraphqlExecutionMiddleware::getDefaultFieldResolver
:
Wouldn't it make sense to actually invoke any custom field resolver (from config->get('graphql.defaultFieldResolver')
) in __invoke
here, if present, and otherwise call Executor::defaultFieldResolver
?
Maybe not though, might be confusing: if the custom field resolver is responsible itself for applying the directives, what if someone wants to provide their own implementation for this. This would require adapting the FieldResolver
class and not the defaultFieldResolver
$this->directiveHandler = $directiveHandler; | ||
} | ||
|
||
public function setSchema(Schema $schema): self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming is kind of weird, as we're not "setting the schema" anywhere.
Co-authored-by: Markus Podar <markus@fischer.name>
While working on this, and digging into webonyx/graphl-php, I was surprised as well. The built-in directives skip and include are hard-coded in graphql-php, by leaving out fields before they are resolved. No other directive support is available, and a related issue is open for a long time. The field resolver is the suggested approach according their documentation. That's the reason that the possible directive locations are limited currently. If someone defines a custom field resolver, the directive support will indeed no longer be available. I will add a warning for this in de readme, if you want to continue with this PR. I can understand if you see too many objections. |
If I understand it correctly, it would boil down to having to delegate only one call to your architecture to support them as I see in
This would be acceptable to me although I'm not yet sure how a final solution regarding the default field resolver should work:
Quite some time has passed and even I had to re-read all my feedback to get the picture again. My gut feeling is that we might need some more iteration(s) to nail it, but otherwise |
@sforward are you still interested in continuing this PR? |
Sorry it's been a while. The linked issue at webonyx/graphql-php was closed yesterday, so I don't expect we can achieve full support for directives here. I will close the PR. |
Summary
This PR starts where #703 was left off. It is a non-breaking implementation of the field directive, by overriding the default field resolver.
Locations
There are two types of directive locations: type system (or schema) directive locations and executable directive locations. The type system directives are not supported by webonyx/graphql-php, see here. This isn't a real problem though, since similar functionality can be achieved via the types, queries, middleware, etc of this package. I have added a warning for this in the readme.
I decided to make a PR for the field location only, since the other locations will act on different parts of the query execution.
Built-in directives
The
include
andskip
directives already worked, so I added these to the readme as well.Type of change:
Checklist:
composer fix-style