-
Notifications
You must be signed in to change notification settings - Fork 222
Description
Q | A |
---|---|
Bug report? | no |
Feature request? | yes |
BC Break report? | no |
RFC? | yes |
Version/Branch | 1.0 |
These are the next enhancements I would like to work on next.
Expression language
Currently this bundle heavily (if not completely) relies on Expression Language which is a powerful, yet pretty exotic feature. I don't believe it should be the main configuration tool, because it:
- requires to learn a new pseudo-language to start
- has no support of its syntax
- introduces too much logic into config files
Now it became so ubiquitous in the bundle, that we even use it inside annotations, which itself is a pseudo-language. Consider this piece of code:
/**
* @GQL\Field(resolve="resolver('hero_friends', [value, args['page']])")
*/
public $friends;
the expression inside the annotation looks just ridiculous.
The thing is, that we can achieve same things with more traditional methods and turn the Expression Language from the "must have" to the "nice to have" feature, whaе it actually was invented for. This part is an attempt to rethink the way we define resolvers in our GraphQL schema (one of the ways).
Changing the resolve
to resolver
Example:
Query:
type: object
config:
fields:
user:
type: "User!"
description: "Something, that talks"
resolver: App\GraphQL\UserResolver::getUser
Just a FQC and method name, no @=
prefix, no quotes, no double or quadro slashes, no code-eye-parsing. Pretty clean, right? And this syntax is well knows to IDEs, you can just ctrl+click on the link and teleport right to the resolver method... even if it's on the Moon 🚀🌑
The name resolve
is a verb, which makes sense with Expression Language, but in the new configuration it just points to a method and calling it resolver
is more suitable here (@akomm's idea btw.). In addition it will allow us to mark the resolve
field deprecated.
Jumping to the resolver method
Now it comes to the resolver configuration. This could be done with the new @Resolver
annotation. First question that comes into my mind is "How do we define the order of the resolver params?".
Well, first it will try to guess params. We can guess ArgumentInterface
, ResolveInfo
, InputValidator
, Models
by the type-hints. The parameters without type-hints will be guessed by names: $value
, $object
, $context
, $user
.
If non-standart names are used, they could be mapped with the @Resolver
annotation:
/**
* Configuring all variables. Only predefined values are available.
*
* @Resolver("args", "context", "info", "value")
*/
public function getUser(ArgumentInterface $args, $context, ResolveInfo $info, $value)
{
# ...
}
/**
* Configure only specific params
*
* @Resolver({"var" = "context", "parent" = "value"})
*/
public function getUser(ArgumentInterface $args, $var, ResolveInfo $info, $parent)
{
# ...
}
Services can be simply injected via constructor and that should be enough. But if the user is desperate enough, and wants
to inject services or values from expressions, he can use @Param
annotation, designed for extended configuration of a specific param:
/**
* @Param("post", expr="repository.find(args.postId)")
* @Param("options", expr="json_decode(args.options)")
*/
public function getUser(Post $post, ArgumentInterface $args, array $options)
{
# ...
}
Noticed that I access args
items with the dot symbol? We can make it possible by adding the magical method __get
to the Argument
class.
There is also no @Resolver
annotation, because it's not necessary, the class already implements ResolverInterface
. The @Resolver
annotation can be used for simple configs like order of predefined variables, or setting the resolver alias, which will be described below.
Aliases
We all love the old good aliases, expecially those, who are too lazy, to type long FQCNs (wink to @Vincz 😉).
If the FQCN is too long and it bothers you, then you can use the short alias:
Query:
type: object
config:
fields:
user:
type: "User!"
description: "Something, that talks"
resolver: UserResolver::getUser
Usually you don't have resolvers with same names and methods, so that shouldn't create any collisions in 99.99999% cases, unless you are insane. If it's the case, you can use the @Resolver
annotation to define an alias either on the class or on the method (or both): @Resolver(alias="my_alias")
.
With this however you can't ctrl+click on the alias.
Access
What if we make possible to restrict access on resolver level? It can be applied on the whole class to restrict all the resolvers inside, or only on a specific method (like access control in Symfony controllers):
/**
* @IsGranted("ROLE_EMPEROR")
*/
class NewsResolver implements ResolverInterface
{
/**
* @Resolver(alias="my_resolver")
* @IsGranted("ROLE_PRESIDENT")
*/
public function test(ArgumentInterface $a, ResolveInfo $info, UserInterface $user)
{
}
}
The @IsGranted
annotation is pretty powerful, users can define their voters and all access logic will happen there.
The getAliases
method
This method won't be necessary anymore, all required information is already provided with annotations.
Using __invoke method
If method is not defined in the resolver path, it will simply use the __invoke function.
That's it for now. Please tell me, what you think @mcg-web @Vincz @akomm @mavimo