Skip to content

Reconsider ArgumentsTransformer #538

Open
@murtukov

Description

@murtukov

I think that the ArgumentsTransformer class and the associtated expression function arguments() should be reworked/removed and here I explain why:

I will speak about the current transformation and validation parts separately.

Why should we remove the current validation mechanism from the ArgumentsTransformer in favor of the new validation feature:

  1. It's not flexible: we can't disable it, can't use validation groups, group sequences, can't reuse constraints from another classes (code dublication), can't control cascade validation.

  2. It happens in the transformation phase (inside the ArgumentsTransformer), which breaks the single responsibility principle.
    Transformation and validation should be 2 completely separated concepts.

  3. It works only with types created by annotations.

Why should we remove/rework the current transformation mechanist:

  1. It requires GraphQL types declared with annotations to hydrate data. That means I cannot transform data into a random object - it must certainly be a GraphQL type and this is unflexible.
    For example I would like to have a possibility to hydrate data directly into Doctrine entities (without or with validation).

  2. The mapping is declared in expressions, which is not good. Expression language is a nice feature and we should use it, when it's necessary, but we shouldn't abuse the usage of expression language, because the configuration is not a place for logic.

Take a look at this 2 examples (php and yaml):

/**
 * @GQL\Type
 */
class RootMutation {
    /**
     * @GQL\Field(
     *   type="User",
     *   args={
     *     @GQL\Arg(name="input", type="UserRegisterInput")
     *   },
     *   resolve="@=call(service('UserRepository').createUser, arguments({input: 'UserRegisterInput'}, arg))"
     * )
     */
    public $createUser;
}
Mutation:
    type: object
    config:
        fields:
            register:
                type: User
                resolve: "@=mutation('register', [args, arguments({input: 'UserRegisterInput'}, args)])"
                # ...

As you can see there is too much logic inside strings (expressions) and this is definitely NOT a good practice. Configuration must be declarative and not imperative. The logic must be written in php, not in configuration. Plus expression language is hard to read, because of no highlighting.

Additionally the name arguments is confusing, because we already have a variable called args. It should be named transformer at least.

What I suggest:

First we need to distinguish 2 concepts: Transformer and Hydrator .

Transformer converts a given value into another data type or format. These can be scalars and objects.
Hydrator only populates a given object with data. It cannot convert values.

Lets follow the approach of Symfony Forms. Forms can do 3 things:
1) Hydrate entities with form data.
2) Transform field values with DataTransformers.
3) Validate data using entity constraints.
4) Validate raw data without entities (no object mapping).

I suggest to follow this approach and integrate our bundle with Doctrine entities as tight as possible, without any drawbacks, because most of the time people work with Doctrine.

3 and 4 are already implemented in the opened PR. Now we only need to implement a Hydrator and a DataTransformer.

Suppose we have the following yaml config:

Mutation:
    type: object
    config:
        fields:
            createUser:
                type: Post
                resolve: "@=mut('create_user', [args, hydrator])"
                entity: App\Entity\User
                args:
                    username:
                        type: String!
                    type: 
                        type: String!
                        transformer: App\Transformer\StringToInt # using a service
                    dateOfBirth: 
                        type: String!
                        transformer: App\Transformer\Generic::stringToDate # using a static method

We have here new entries: entity and transformer. When the resolver is called system will call transformers first. Then the entity will be populated with the transformed data with a default hydrator, using property accessor. args won't be changed:

public function createUser(ArgumentInterface $args, HydratorInterface $hydrator): User
{
    // First validates arguments and then 
    // creates and populates an object.
    $user = $hydrator->process();
	
    // At this point the $user is 100% valid.
    $this->em->persist($user);
    $this->em->flush();

    return $user;
}

Of course we need to decide in what moment the validation will be done, maybe create different validation strategies (e.g. first transform, then validate or first validate, then transform).

The form component normally takes validation constraints from the entity, but you always can add extra constraints directly to a form.
We can do the same, but in the example above I didn't use constraits directly in the config.

Let's discuss here this feature. Any suggestions?

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions