-
-
Notifications
You must be signed in to change notification settings - Fork 892
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
[Proposal] Add Stages #2978
[Proposal] Add Stages #2978
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description seems outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above.
} | ||
|
||
$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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be backported on 2.4 as a bugfix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An issue has been reported for non-cloneable objects IIRC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stage sounds like “util” or “service” to me: too generic and not very descriptive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make it a callable? __invoke
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! WDYT @dunglas?
* | ||
* @author Alan Poulain <[email protected]> | ||
*/ | ||
final class ReadStage implements ReadStageInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't this class also used by the GraphQL resolver?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 <[email protected]> | ||
*/ | ||
interface ReadStageInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?