Conversation
dunglas
left a comment
There was a problem hiding this comment.
Good first step! I definitely agree with the goals and it's a move in the good direction. I made some comments to go one step further. I've the feeling that we can still improve the design a lot.
src/Stage/ReadStage.php
Outdated
| use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait; | ||
|
|
||
| /** | ||
| * Retrieves data from the applicable data provider and sets it as a request parameter called data. |
There was a problem hiding this comment.
This description seems outdated
There was a problem hiding this comment.
You think? It's basically what this stage does (excepted the set part which I can remove).
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext) |
There was a problem hiding this comment.
Instead of this long list of unstructured and untyped parameters, shouldn't we introduce a new ApiRequest or ApiQuery/ApiCommand class(es), that we'll construct from the (Symfony/Laravel/PSR-7) HTTP request, or from the GraphQL query, or from any other source of request regarless of the protocol. It will allow to have a more strict typing, and will improve the evolvability of API Platform (in case we need to add more data over the time).
Basically, the responsibility of the Symfony Event Listener, or of the Laravel middleware, or of the GraphQL resolver would be to create this ApiRequest instance.
There was a problem hiding this comment.
I've always wondered if it would be interesting to introduce this kind of class instead of having a lot of parameters and a context array with more things inside 😅
I'm 👍 for it.
src/Stage/ReadStage.php
Outdated
| if (isset($attributes['item_operation_name'])) { | ||
| $data = $this->getItemData($identifiers, $attributes, $context); | ||
| } elseif (isset($attributes['subresource_operation_name'])) { | ||
| // Legacy |
There was a problem hiding this comment.
Why legacy? It's a valid use case to not inject a SubResourceDataPprovider (especially if you use the library directly, without the Symfony bridge).
There was a problem hiding this comment.
Not my code here but I can remove the comment. The SubresourceDataProvider is mandatory in the GraphQL read stage.
src/Stage/ReadStageInterface.php
Outdated
| namespace ApiPlatform\Core\Stage; | ||
|
|
||
| /** | ||
| * Retrieves data from the applicable data provider and sets it as a request parameter called data. |
|
|
||
| $request->attributes->set('data', $data); | ||
| $request->attributes->set('previous_data', \is_object($data) ? clone $data : $data); | ||
| $request->attributes->set('previous_data', \is_object($data) && (new \ReflectionClass(\get_class($data)))->isCloneable() ? clone $data : $data); |
There was a problem hiding this comment.
This should be backported on 2.4 as a bugfix.
There was a problem hiding this comment.
This is not really a bugfix. It's because now this method is always called whereas before it was not called if the operation was a collection.
There was a problem hiding this comment.
An issue has been reported for non-cloneable objects IIRC
There was a problem hiding this comment.
In this case, without this change, the tests wouldn't pass.
| /** | ||
| * @return object|iterable|null | ||
| */ | ||
| public function apply(array $attributes, array $parameters, ?array $filters, string $method, array $normalizationContext); |
There was a problem hiding this comment.
apply isn't very descriptive (I'm not a fond of "stage" too btw :D). Can't we more explicit names?
There was a problem hiding this comment.
Which one do you have in mind? Something like read?
I know you would say that 😄 Naming is really hard and subjective. "Stage" was interesting because it was not used and its meaning was alright: it's a "step in a process".
There was a problem hiding this comment.
Stage sounds like “util” or “service” to me: too generic and not very descriptive
There was a problem hiding this comment.
But it is something kind of generic. It's a step or an operation. How would you call the read, serialize, validate, etc. things?
There was a problem hiding this comment.
Make it a callable? __invoke?
| * | ||
| * @author Alan Poulain <contact@alanpoulain.eu> | ||
| */ | ||
| final class ReadStage implements ReadStageInterface |
There was a problem hiding this comment.
Why isn't this class also used by the GraphQL resolver?
There was a problem hiding this comment.
I would like to merge both too but sadly they are very different. I don't think it will be doable easily.
There was a problem hiding this comment.
But then it doesn’t fix the issue of having good extensions points working both for REST and GraphQL. Which is one of the goals.
There was a problem hiding this comment.
Why not having different ones? The need to use both REST and GraphQL is not common. It would just mean having two different service names to use.
There was a problem hiding this comment.
To me it's very common, implementing both. That's actually what drove me to API Platform in the first place, I thought it would provide a(nother) way to DRY my Controllers and Resolvers. If you feel it's too much work for this MR, that's of course your prerogative, but please don't sacrifice forever the goal of unification. Also, Stage feels okay ; it fits, and I can't find anything I deem more suitable.
There was a problem hiding this comment.
We could introduce stage for both REST and GraphQL, but the service will be an empty shell with practically no code IMO. It will be just there to be decorated.
| * | ||
| * @author Alan Poulain <contact@alanpoulain.eu> | ||
| */ | ||
| interface ReadStageInterface |
There was a problem hiding this comment.
I wonder if this new concept is really necessary. Does this logic belong to the DataProvider? Or should we create a ApiRequestFactory class instead (see my previous comment?). I'm not sure yet, I need to think about it, but I've the feeling that we're introducing unnecessary indirection layers.
There was a problem hiding this comment.
I think their responsibility is clear:
- allow or not to apply the stage
- extract, collect and normalize the right parameters (mainly extract the identifiers and build the (de)normalization context)
- call the good "solver"
We could add the second point inside an ApiRequestFactory. But IMO it will not necessarily be a good thing. It will have a lot of conditional clauses and could quickly become a monster.
And I don't know where we could put the other parts. Maybe having a ChainDataProvider and modify the ChainDataPersister to choose the right method (persist or remove to call based on the ApiRequest)?
There was a problem hiding this comment.
Also IMHO, it's easier for the user to understand and decorate this kind of service than to use the data provider / persister and the serializer.
There was a problem hiding this comment.
I’m really not sure about the last point! It’s more concepts to learn with no clear responsibilities.
There was a problem hiding this comment.
I guess it depends of the developer and their way to think.
But a lot of developers are using the events already to add more logic so the need is there IMO.
d89d62d to
3924aa8
Compare
3924aa8 to
78a864c
Compare
|
I need to find some time to dive deeper into this. I'm still not convinced that we need this new layer of complexity. I also don't like the fact that this new class isn't used by the GraphQL subsystem. |
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
|
this was quite helpful towards building #5657 thanks ! |
As I've done with GraphQL (#2959), I would like to do the same for the REST part.
The aim is to create Stage services.
They will be responsible for all the stages (or steps) API Platform is doing to resolve a request.
They will be independent of the request and will be used by the event listeners.
A lot of benefits:
Right now I've only done the read stage. Behat tests are passing.
WDYT @api-platform/core-team?