Description
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:
-
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.
-
It happens in the transformation phase (inside the ArgumentsTransformer), which breaks the single responsibility principle.
Transformation and validation should be 2 completely separated concepts. -
It works only with types created by annotations.
Why should we remove/rework the current transformation mechanist:
-
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). -
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?