Skip to content
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

Closed
wants to merge 1 commit into from
Closed

Conversation

alanpoulain
Copy link
Member

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets
License MIT
Doc PR

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:

  • instead of using events and having issues with priorities (https://api-platform.com/docs/core/events/), the services could be decorated instead;
  • since they will be independent of events and the request, they could be used in different contexts (I'm thinking of Laravel for instance).

Right now I've only done the read stage. Behat tests are passing.

WDYT @api-platform/core-team?

@alanpoulain alanpoulain added the RFC label Aug 9, 2019
Copy link
Member

@dunglas dunglas left a 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.

use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;

/**
* Retrieves data from the applicable data provider and sets it as a request parameter called data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description seems outdated

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member Author

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.

if (isset($attributes['item_operation_name'])) {
$data = $this->getItemData($identifiers, $attributes, $context);
} elseif (isset($attributes['subresource_operation_name'])) {
// Legacy
Copy link
Member

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).

Copy link
Member Author

@alanpoulain alanpoulain Aug 10, 2019

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.

namespace ApiPlatform\Core\Stage;

/**
* Retrieves data from the applicable data provider and sets it as a request parameter called data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated

Copy link
Member Author

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);
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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);
Copy link
Member

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?

Copy link
Member Author

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".

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link

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.

Copy link
Member Author

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
Copy link
Member

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.

Copy link
Member Author

@alanpoulain alanpoulain Aug 10, 2019

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)?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

@dunglas
Copy link
Member

dunglas commented Aug 18, 2019

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.

Base automatically changed from master to main January 23, 2021 21:59
@stale
Copy link

stale bot commented Nov 5, 2022

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.

@soyuka
Copy link
Member

soyuka commented Oct 17, 2023

this was quite helpful towards building #5657 thanks !

@soyuka soyuka closed this Oct 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants