Skip to content

[RFC] Resolver definition enhancement #708

@murtukov

Description

@murtukov
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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions