Skip to content

[RFC] Remove/rework config processors #784

Open
@murtukov

Description

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

Whenever you make a request to your Symfony app with GraphQL Bundle installed, it instantiates all of your GraphQL types. To reduce the performance impact, webonyx/graphql-php library made possible to use closures almost at any part of your type config to make them lazy loaded.

This bundle adds so called "config processors" to each of the generated GraphQL types, which modify configs during the instantiating of the classes.

The problem here is, that configs wrapped into closures (for lazy loading) cannot be accessed, unless you call them. And this is exactly what config processor does in order to access and modify configs. This kinda defeats the purpose of wrapping configs into closures. Secondly it makes double work for no particular reason (unwrap configs, modify it and wrap back into a closure).

Every Config Processor wraps the fields closure into another closure to modify it's behaviour.

Currently we have 3 config processors

  • AclConfigProcessor
  • PublicFieldsFilterConfigProcessor
  • WrapArgumentConfigProcessor

That means that after all processors are executed, the fields array is wrapped into 4 closures.

This RFC tries to find out a better way to do tasks these processors perform.

Let's go through all processors.

1. WrapArgumentConfigProcessor

This processors finds all resolve and resolveFiled callbacks in the config array and wraps them into another callback to only modify the $args param. Here is an example of a field:

Before processing:

'myField' => [
    'type' => /* ... */,
    'resolve' => function ($value, $args, $context, $info) use ($globalVariables) {
        return $globalVariables->get('mutationResolver')->resolve(["no_validation", []]);
    },
    'args' => /* ... */,
],

After processing your resolver callback changes:

'myField' => [
    'type' => /* ... */,
    'resolve' => function () {
        $args = func_get_args();
        if (isset($args[1]) && !$args[1] instanceof ArgumentInterface) {
            $args[1] = $argumentFactory->create($args[1]);
        }
        return (function ($value, $args, $context, $info) use ($globalVariables) {
            return $globalVariables->get('mutationResolver')->resolve(["no_validation", []]);
        })(...$args);
    },
    'args' => /* ... */,
],

Wrapping of the resolve happens in runtime.

Possible solution:

Instead of wrapping the resolver, we can use the ArgumentFactory directly in the generated resolver callback. Example:

'myField' => [
    'type' => /* ... */,
    'resolve' => function ($value, $args, $context, $info) use ($globalVariables) {
        $args = $globalVariables->wrapArguments($args);
        return $globalVariables->get('mutationResolver')->resolve(["no_validation", []]);
    },
    'args' => /* ... */,
],

Fortunately, with the new generator library it's easy to change the content of the generated callbacks. With this we can remove the WrapArgumentConfigProcessor.

2. AclConfigProcessor

The closure created by this processor finds the access callback and executes it. If access returns false, the resolve callback is replaced with another one, that throws an exception. Example:
Before processing:

'myField' => [
    'resolve' => function ($value, $args, $context, $info) use ($globalVariables) {
        return $globalVariables->get('mutationResolver')->resolve(["no_validation", []]);
    },
],

After processing (if access returned false):

'myField' => [
    'resolve' => function() {
        throw new UserWarning('Access denied to this field.');
    }},
],

Possible solution:

Similar to the first one. Instead of replacing the resolve callback, we can generate the access check call in the actual resolver. Example:

'myField' => [
    'resolve' => function ($value, $args, $context, $info) use ($globalVariables) {
        $this->checkAccess('myField'); // this line throws `UserWarning` on denied
        return $globalVariables->get('mutationResolver')->resolve(["no_validation", []]);
    },
],

3. PublicFieldsFilterConfigProcessor

Closure created by this processor finds the public callback of the field config and executes it. If result is false the given field is just removed from the config array. This processor must be on the deepest level of closure nesting in order to be able to access the fields array. Otherwise the array will be wrapped into closures created by other processors.
I don't have any solution to this right now.

Why do we need this?

First and most important it should reduce the initialization and request time:

  • No unwrapping/wrapping resolvers work, that partly destroyes the lazy loading.
  • No additional callback calls.

Secondly it would make generated types less "magical", because users could see the actual type's code in one place.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions