Skip to content

[RFC] Refactor GlobalVariables and resolver expressions #775

@murtukov

Description

@murtukov
Q A
Bug report? no
Feature request? no
BC Break report? no
RFC? yes
Version/Branch 1.0 (master)

Some of the proposed changes are BC-break.

List of proposed changes

1. Rename GlobalVariables class.

GlobalVariables is a service passed to every generated GraphQL type. It contains all necessary services and references to other types to configure a GraphQL schema. The name GlobalVariables doesn't really explain anything, I suggest to rename it to better reflect it's purpose.

Suggested name: GraphQLServices
Other possible names: SchemaServices, TypeServices, TypeHelper

2. Move ConfigProcessor into GlobalVariables

Every generated type also gets ConfigProcessor object, which is only used in types and nowhere else. If GlobalVariables contains all necessary services for types, then why not also moving the ConfigProcessor inside it?

Before:

final class QueryType extends ObjectType implements GeneratedTypeInterface
{
    public const NAME = 'Query';
    
    public function __construct(ConfigProcessor $configProcessor, GraphQLServices $services)
    {
        $config = [
            'name' => self::NAME,
            'fields' => fn() => [
                'post' => [
                    'type' => Type::string(),
                    'resolve' => fn() => "Hello, World!",
                ],
            ],
        ];

        parent::__construct($configProcessor->process($config));
    }
}

After:

final class QueryType extends ObjectType implements GeneratedTypeInterface
{
    public const NAME = 'Query';
    
    public function __construct(GraphQLServices $services)
    {
        $config = [
            'name' => self::NAME,
            'fields' => fn() => [
                'post' => [
                    'type' => Type::string(),
                    'resolve' => fn() => "Hello, World!",
                ],
            ],
        ];

        parent::__construct($services->get('configProcessor')->process($config));
    }
}

3. Add shortcuts into GraphQLServices

Add methods for frequently used services, like typeResolver, resolverResolver and mutationResolver

Before:

final class QueryType extends ObjectType implements GeneratedTypeInterface
{
    public const NAME = 'Query';
    
    public function __construct(GraphQLServices $services)
    {
        $config = [
            'name' => self::NAME,
            'fields' => fn() => [
                'users' => [
                    'type' => $services->get('typeResolver')->resolve('User'),
                    'resolve' => function ($value, $args, $context, $info) use ($services) {
                        return $services->get('resolverResolver')->resolve(["get_users", [0 => $args, 1 => $value]]);
                    },
                ],
                'createUser' => [
                    'type' => $services->getType('User'),
                    'resolve' => function ($value, $args, $context, $info) use ($services) {
                        return $services->get('mutationResolver')->resolve(["create_user", [0 => $args, 1 => $value]]);
                    },
                ],
            ],
        ];

        parent::__construct($services->get('configProcessor')->process($config));
    }
}

After:

final class QueryType extends ObjectType implements GeneratedTypeInterface
{
    public const NAME = 'Query';
    
    public function __construct(GraphQLServices $services)
    {
        $config = [
            'name' => self::NAME,
            'fields' => fn() => [
                'users' => [
                    'type' => $services->getType('User'),
                    'resolve' => function ($value, $args, $context, $info) use ($services) {
                        return $services->query("get_users", $args, $value);
                    },
                ],
                'createUser' => [
                    'type' => $services->getType('User'),
                    'resolve' => function ($value, $args, $context, $info) use ($services) {
                        return $services->mutation("create_users", $args, $value);
                    },
                ],
            ],
        ];

        parent::__construct($services->processConfig($config));
    }
}

In this case arrow functions can be used:

final class QueryType extends ObjectType implements GeneratedTypeInterface
{
    public const NAME = 'Query';
    
    public function __construct(GraphQLServices $services)
    {
        $config = [
            'name' => self::NAME,
            'fields' => fn() => [
                'users' => [
                    'type' => $services->getType('User'),
                    'resolve' => fn($value, $args) => $services->query("get_users", $args, $value),
                ],
                'createUser' => [
                    'type' => $services->getType('User'),
                    'resolve' => fn ($value, $args) => $services->mutation("create_users", $args, $value),
                ],
            ],
        ];

        parent::__construct($services->processConfig($config));
    }
}

So now instead of this:

$services->get('mutationResolver')->resolve(["get_users", [0 => $args, 1 => $value]])

we have this

$services->mutation("get_users", $args, $value) // variable-length argument list

The signature is also changed to be simpler.

4. Use QueryResolver instead of ResolverResolver

We have 2 expression resolver functions: resolver and mutation. If we are using the word mutation, which is a type
of resolver, then I suggest renaming resolver into query to be more like a counterpart of mutation. It will also
make more sense in YAML configs (no tautology resolve: resolver):

Before:

Query:
    type: object
    config:
        fields:
            users:
                type: String
                resolve: '@=resolver("get_users", [args, info])'

After:

Query:
    type: object
    config:
        fields:
            users:
                type: String
                resolve: '@=query("get_users", args, info)'

The signature is also more simple now (no arrays as suggested in section 3)

It also includes renaming of the ResolverInterface into QueryInterface

5. Make the expression prefix configurable

In my projects I would like to be able to change the expression trigger. The symbols @= at the beginning of a string
require it to be wrapped in quotes. If I change the trigger symbol to $ for example, then I don't have to wrap
expression strings into quotes:

Before:

resolve: '@=mutation("get_users", args, info)'

After:

resolve: $mutation("get_users", args, info)

Or just use @:

resolve: '@mutation("get_users", args, info)'

A better alternative to GraphQLServices would be of course a separate injection of only required services into each of generated types, instead of injecting a container, which is considered a bad practice, but I am currently not sure how to implement it.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions