-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reconsider ArgumentsTransformer #538
Comments
Hi @murtukov ! I agree that the
The thing I like about the /**
* @GQL\Input
*/
class HeroInput
{
/**
* @GQL\Field(type="String!")
* @Assert\Regex(
* pattern="/Dark/",
* match=false,
* message="A hero name can't contain 'Dark'"
* )
*/
public $name;
public function isSkywalker()
{
return strstr($this->name, 'Skywalker') !== false;
}
} It's all I need to have a generated /**
* @GQL\Provider
*/
class HeroRepository
{
/**
* @GQL\Mutation
*/
public function createHero(HeroInput $input) : int
{
...
}
} It's all I need to have a generated For the transformation part, your solution could work, and we could have a way of defining a transformer for every arg. But maybe the default should be an For the validation part, it's the same. Your solution is to add constraints to arguments and I think it would be a great addition. But we should also keep the automated part as well. As long as a transformer output an object, we should validate it (if there is no constraints attached to it, it will not trigger any validation). So in the end, I think it's great to add new features as long as we keep the simplicity of the auto-guessed stuff and at the moment, I don't know how we could do it without the Arguments Transformer. We should discuss about a service to handle this arguments transformation. @mcg-web is working on a service to inject contextual arg and it should be part of the process. I think it should be something as automatic as possible, a bit like the Symfony service auto-configuration. |
Hi @Vincz, first of all I want to say, that I am NOT against annotations or any good features related to it.
I like it all and I'm not saying that I want to remove something of it. We are all programmers here and we want things to be done as easy as possible. Thats why I love so much new features of Symfony 4 (service autowiring, service autoconfiguration, dependency injection by type-hints). I only want to enhance this features, but to do so, much needs to be changed. For now it's an experimental feature for me and is not yet ready to be used in big projects. The problem is that the current implementation of the annotations feature is too limited. The examples that you showed are too simple and will work only in small projects. In real life many people will have more complex projects (as I do). Imagine if you have an already working big project, where you use REST API, Doctrine entities and DTOs for validation purposes. And then you want to add a GraphQL API, without changig the current architecture of your project. In this case it would be very hard to use annotations for graphql schemas, because:
When we create a new feature, we should ask ourselves: How would it work with annotations? How would it work with yaml configs? How should I design this feature, so that its API is same in both? New features should be generic, but the I suggest to rework the ArgumentsTransformer without losing any of its functionality, step-by-step.
Another reason to rename it is the need to free the name transformer for the data transformer feature. Everything in short:
What do you think? |
With php annotations you do the same: you define a class, then you tell which method should be called and which arguments should be used. It's pretty much the same.
While it's a nice idea to create GraphQL types with only php and annotations it has it's disadvantages:
Anyways it's all a matter of taste and we should let the users decide, what they want to use. For the "argument list synchronization" problem I have an idea: The idea is, that you dont declare the argument list in the yaml and don't use an expression language. Instead you only point to the method: resolve: App\GraphQL\Resolver\UserResolver::findOne The arguments list will be auto guessed by type hints in the resolver method: public function findOne(ArgumentInterface $args, ArgumentsValidator $validator): User
{
// do your stuff
} This also eliminates the problem with double slashes in the expressions. The only drawback is, that you can't use expression functions inside it. Anyway you can always fallback to expressions if you want to. |
@murtukov I am totally with you. Just recently had a discussion about exactly the same things you pointed out. Kind of funny. Loosing overview of files with annotations. To much expression, etc., FQCN in config/expressions (also backslash "issue", sync). I actually would only want to use annotations for DTOs. In other cases, annotations would lead to dead classes/shells with props and no other purpose than annotate to produce graphql types. I always think like this: when I remove annotation, does the class make any sense afterwards? And having this: <?php
/** @GQL\Type(name="Query") */
class QueryType
{
} Just to create the query type, that is than later populated via So yes, being general annotation-centric is not optimal. |
As to the proposal part: do you think it is useful to be so open-minded about Why I ask: |
I am not sure I understand what you mean with "being open-minded" and "exchangeable services" 😄 The Symfony Form component is a powerfull tool, but it's designed to be used with HTML, so when we decide to use GraphQL for our APIs, we automatically reject all these nice features. My proposal here is an attempt to recreate (reimplement) these features, but with GraphQL. What is a Symfony form and what can we do with it?With forms you can define a set of data, that you expect from a client. When a request comes from the client, the form handles this request and holds the input data. It's similar to GraphQL types, but form can also map the incoming data to an entity (or any other object), if you want. This is what I mean under hydration and I want to recreate this feature in our bundle. If you have some additional fields in your GraphQL type that you don't want to hydrate - no problem, you can mark these fields as unmapped, exatly the same way the forms do. Forms also have data transformers, which make possible to convert data, before it is hydrated to an entity. A simpliest example is String <-> DateTime conversation. Now about validation. When form handles a request, it automatically performs a validation. First it hydrates an object (entity) with data and then validates this object. This approach is considered wrong, because if validation fails you have an instantiated entity with invalid data inside it, which can still be persisted in DB. My implementation of validator creates temporary DTOs and validates them. If input data is valid we could then hydrate our entities. I have no doubt, that the 3 features should be separate (validation, hydration, transformation), because they can be used separately:
With Symfony Forms you can do all of it, but it can be done easely with only one call: handleRequest(), which performs all 3 operations behind the scenes. My question is: How can we implement the handleRequest approach with GraphQL types? So how can we implement all 3 features so, that they could be used separately and, if you want, could be performed with one call? |
I didn't understand this part:
Maybe you provide some examples? |
@murtukov function resolverMethod(Arguments $args, ResolveInfo $info, User $user) {} Currently you inject those via expression: You pointed out there are however some downsides (no expressions). I addressed how this might be not the case if done in a way in which you will never really need expressions. Let's say we have an ['args' => 'Arguments', 'info' => 'ResolveInfo', 'user' => 'User'] At the end it probably should not be an array and needs some additional data... When the resolver is about to be used, the Now this Basically: you have all the information around a request, and you can map it with strategies to the resolver. Interessting application could become possible, for example: If you expect non-nullable user in resolver and user is not authenticated, you could implicitly handle it as unauthorized. But this is kind of an spin-off the idea as an aside. |
I think that the
ArgumentsTransformer
class and the associtated expression functionarguments()
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):
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 calledargs
. It should be namedtransformer
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:
We have here new entries:
entity
andtransformer
. 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: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?
The text was updated successfully, but these errors were encountered: