Skip to content

refactor: use provider/processor instead of event listeners #5657

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

Merged
merged 2 commits into from
Sep 2, 2023

Conversation

soyuka
Copy link
Member

@soyuka soyuka commented Jul 10, 2023

tldr: This patch introduces providers and processors as a replacement for Symfony listeners.

API Platform standalone

The idea behind that is to make API Platform work without using Symfony's HttpKernel. This means that to use API Platform, all you need to do is:

$uriVariables = ['id' => 1];
$operation = new Get(uriVariables: ['id'], resourceClass: Book::class);
$body = $this->provider->provide($operation, $uriVariables, $context);
return $this->processor->process($body, $operation, $uriVariables, $context);

This allows:

  • to share more code between GraphQl and HTTP (very similar to [Proposal] Add Stages #2978, we had no provider/processor at that time though)
  • to make the code more portable (goes in pair with our ongoing subtree split)
  • to be able to use API Platform without the Symfony framework, it was in fact possible but a bit harder to do
  • improve your Developer Experience a lot

Impact on DX and BC-layer

Let me explain this last part. Today API Platform relies on the HttpKernel and hooks various event listeners do to work. While doing this, it alters the way Symfony itself works by adding (or mutating) Request::$attributes. The most notable change compared to a standard Symfony is that, when you're router goes through API Platform your controller can return mixed instead of a Response:

final class PlaceholderAction
{
/**
* @param object $data
*
* @return object
*/
public function __invoke($data)
{
return $data;
}
}

Indeed, API Platform will intercept the mixed data and create the Response for you. This IMO is not so good especially when using API Platform programmatically.

Because we rely on event listeners, we need to make choices for the framework and for example we choose to call Symfony's security before validating the user input. This lead (and still leads) to issues (#5756) as you may want stuff to be made differently. Even within Symfony, http kernel listeners priorities are NOT part of the backward compatibility layer, and therefore everyone will tell you to avoid using listeners if possible, especially avoid relying on vendor listeners.

State providers and processor will help a lot with that. For example in API platform 3.1 we'd have this for our security:

<!-- This method must be executed only when the current object is available, before deserialization -->
<tag name="kernel.event_listener" event="kernel.request" method="onSecurity" priority="3" />
<!-- This method must be executed only when the current object is available, after deserialization -->
<tag name="kernel.event_listener" event="kernel.request" method="onSecurityPostDenormalize" priority="1" />
<!-- This method must be executed only when the current object is available, after validation -->
<tag name="kernel.event_listener" event="kernel.view" method="onSecurityPostValidation" priority="63" />

(note that the logic is currently duplicated to work with graphql)

But in API Platform 3.2 we declare 3 different services that just decorate the correct service where it needs to hook security (read, deserialize, validate):

<service id="api_platform.state_provider.access_checker" class="ApiPlatform\Symfony\Security\State\AccessCheckerProvider" decorates="api_platform.state_provider.read">
<argument type="service" id="api_platform.state_provider.access_checker.inner" />
<argument type="service" id="api_platform.security.resource_access_checker" />
</service>
<service id="api_platform.state_provider.access_checker.post_deserialize" class="ApiPlatform\Symfony\Security\State\AccessCheckerProvider" decorates="api_platform.state_provider.deserialize">
<argument type="service" id="api_platform.state_provider.access_checker.post_deserialize.inner" />
<argument type="service" id="api_platform.security.resource_access_checker" />
<argument>post_denormalize</argument>
</service>
<service id="api_platform.state_provider.access_checker.post_validate" class="ApiPlatform\Symfony\Security\State\AccessCheckerProvider" decorates="api_platform.state_provider.validate">
<argument type="service" id="api_platform.state_provider.access_checker.post_validate.inner" />
<argument type="service" id="api_platform.security.resource_access_checker" />
<argument>post_validate</argument>
</service>

There is a better compatibility layer, you can also do this yourself if you have custom needs without needing an event listener or to merge a new feature. This is, in my opinion, way better to extend API Platform and be less "constraint" then we previously where on every aspect!

This is the result of almost 6 months of code and years of discussions with the core team, many users at conferences or at work so thanks to everyone!

@soyuka soyuka force-pushed the refactor/state branch 4 times, most recently from 9edb6c7 to 29c764c Compare August 14, 2023 09:56
@soyuka soyuka force-pushed the refactor/state branch 3 times, most recently from 523fc1d to 5138850 Compare August 18, 2023 13:07
@soyuka soyuka marked this pull request as ready for review August 18, 2023 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant