Skip to content

WIP: Add events #2329

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

Closed
Closed

Conversation

Deuchnord
Copy link
Contributor

@Deuchnord Deuchnord commented Nov 15, 2018

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #2167, api-platform/api-platform#734
License MIT
Doc PR later

Adding events to make API Platform’s actions tweaking easier.

Todos:

  • Add Pre- and Post- event for each action (based on the \ApiPlatform\Core\EventListener\* listeners)
  • Wire the actions to the events
  • Wire GraphQL to the events
  • Write unit tests
  • Write Behat tests

@alanpoulain
Copy link
Member

Thanks for working on this 🙂

@@ -95,20 +103,37 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
$context += $resourceMetadata->getGraphqlAttribute($operationName, 'denormalization_context', [], true);
$item = $this->normalizer->denormalize($args['input'], $resourceClass, ItemNormalizer::FORMAT, $context);
$this->validate($item, $info, $resourceMetadata, $operationName);

if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(PreWriteEvent::NAME, new PreWriteEvent($operationName, $item));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guess there will be an issue here, since the $operationName from GraphQL (create, update, delete) is not the same as the HTTP methods used in REST

Copy link
Contributor

@antograssiot antograssiot left a comment

Choose a reason for hiding this comment

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

Would it be useful too to have PreValidate and PostValidate events to keep the same logic than in REST ?

try {
$this->validator->validate($item, ['groups' => $validationGroups]);
} catch (ValidationException $e) {
throw Error::createLocatedError($e->getMessage(), $info->fieldNodes, $info->path);
}


if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(PostReadEvent::NAME, new PostReadEvent($item));
}
} catch (ItemNotFoundException $e) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

wouldn't it be better to change this block to

try {
    if (null !== $this->dispatcher) {
        $this->dispatcher->dispatch(PreReadEvent::NAME, new PreReadEvent(null));
    }
    $item = $this->iriConverter->getItemFromIri($args['id'], $baseNormalizationContext);
} catch (ItemNotFoundException $e) {
    $item = null;
}

if (null !== $this->dispatcher) {
    $this->dispatcher->dispatch(PostReadEvent::NAME, new PostReadEvent($item));
}

if (null === $item) {
    return null;
}

to ensure that the PostReadEvent is always sent like in the ReadListener ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it should be dispatched when Read has failed.

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.

I think we can restrict the events to:

  • read
  • deserialize
  • write
  • validate
  • serialize

Other ones are Symfony specific and should be handled using Symfony kernel events. WDYT?

@Deuchnord
Copy link
Contributor Author

Deuchnord commented Dec 5, 2018

I think we can restrict the events to:

  • read
  • deserialize
  • write
  • validate
  • serialize

Other ones are Symfony specific and should be handled using Symfony kernel events. WDYT?

@dunglas Sound good, but I think we should keep the pre- and post- events.
NB: Since you requested changes on (Pre|Post)AddFormatEvent classes, I suppose they should be kept too, though.

@Deuchnord Deuchnord force-pushed the issue2167-kernel-events branch from b7cb661 to ac09958 Compare December 5, 2018 13:54
const PRE_RESPOND = 'api_platform.pre_respond';
const POST_RESPOND = 'api_platform.post_respond';

const PRE_ADD_FORMAT = 'api_platform.pre_add_format';
Copy link
Contributor

Choose a reason for hiding this comment

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

These events should be removed as they're not related to the resource.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove the "Add Format" events.


final class PreWriteEvent extends Event
{
const NAME = Events::PRE_WRITE;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a really weird thing to do. Did I miss the discussion on why this is done this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean, defining a NAME constant in the classes?
I did it mainly for comfort purpose, actually: it makes more sense to me to use a constant attached to the concerned class, so I kept it after @dunglas asked me to create the separate Events class.
Do you want me to remove the NAME constants?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the value of these constants. Just use e.g. Events::PRE_WRITE directly.

@@ -18,6 +18,7 @@
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
<argument type="service" id="request_stack" />
<argument>%api_platform.collection.pagination.enabled%</argument>
<argument type="service" id="event_dispatcher" />
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to add the same on-invalid="null" in this file too.

@@ -90,6 +95,10 @@ public function onKernelRequest(GetResponseEvent $event)
try {
$identifiers = $this->extractIdentifiers($request->attributes->all(), $attributes);

if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(PreReadEvent::NAME, new PreReadEvent($data));
Copy link
Contributor

Choose a reason for hiding this comment

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

It's really weird to have data in Pre Read. I don't think it makes sense...

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps what we could do is to keep a reference to the event object, then check if the data is already set on the event. If it is set, we should not override it.

@jamesisaac
Copy link
Contributor

Very glad this is being worked on.

Is the idea with not attaching the Request to these events (unlike their Symfony counterparts) that the Request can be pulled out manually from the RequestStack service? I don't think the current events on their own contain enough data that you'd be able to determine whether to run most real-world side-effects (normally I'm checking the method, collection/item operation name, or route name).

Also, with the current events system they're executed in a very predictable linear order. Am I understanding that due to the way GraphQL resolvers work (one entity at a time), these events can be executed more like 1..n times (or 0..n) per request if the graphql request contains some nesting? Just trying to think if that could create any awkward conditions where side-effects get executed multiple times.

@Deuchnord
Copy link
Contributor Author

Hi, sorry for the late, I forgot to answer when I saw your comment ^^

Is the idea with not attaching the Request to these events (unlike their Symfony counterparts) that the Request can be pulled out manually from the RequestStack service? I don't think the current events on their own contain enough data that you'd be able to determine whether to run most real-world side-effects (normally I'm checking the method, collection/item operation name, or route name).

Actually, these events are supposed to be higher levelled than Symfony's ones, so they're meant to contain only the details about the data themselves. If you want to get details about the request itself, you'll probably want to intercept Symfony's events instead.

Also, with the current events system they're executed in a very predictable linear order. Am I understanding that due to the way GraphQL resolvers work (one entity at a time), these events can be executed more like 1..n times (or 0..n) per request if the graphql request contains some nesting? Just trying to think if that could create any awkward conditions where side-effects get executed multiple times.

That GraphQL part is actually quite cryptic to me, I need to study that more… if you have any tips to make my integration of the events better, feel free to share them 🙂

@jamesisaac
Copy link
Contributor

No worries!

Actually, these events are supposed to be higher levelled than Symfony's ones, so they're meant to contain only the details about the data themselves. If you want to get details about the request itself, you'll probably want to intercept Symfony's events instead.

I thought one of the motivations here was that Symfony events aren't getting fired during the lifecycle of GraphQL requests (see #2167 )? So do you mean intercepting a combination of the 2? I.e. in a single listener, listen to Symfony's request event to store the route/method, then combine that with the data retrieved from this event, to execute the side effect?

I just think that individually, these events with "only the details about the data", and Symfony's events which don't have the controller result, don't contain enough information to add granular side effects in a lot of real world cases. Both are needed together most of the time.

@Deuchnord
Copy link
Contributor Author

I thought one of the motivations here was that Symfony events aren't getting fired during the lifecycle of GraphQL requests (see #2167 )? So do you mean intercepting a combination of the 2? I.e. in a single listener, listen to Symfony's request event to store the route/method, then combine that with the data retrieved from this event, to execute the side effect?

Yes, that's what's planned ideally: an event that's risen, whenever the action comes from REST or GraphQL :)

I just think that individually, these events with "only the details about the data", and Symfony's events which don't have the controller result, don't contain enough information to add granular side effects in a lot of real world cases. Both are needed together most of the time.

I think I see what you mean, problem is, I'm not sure I can get that Symfony event, for the same reason we can't do it currently like described in #2167… We'll need to think more about it, maybe it would be a good thing to have an object that contain the information that may be useful and are available for both REST and GraphQL?

@jamesisaac
Copy link
Contributor

When I wrote my own quick workaround for #2167 (master...jamesisaac:graphql-events), I just injected the RequestStack into the ItemMutationResolverFactory etc, and included the current request in the event object. so that it closely matched the REST events.

To me that solution added a lot of convenience, as the data alone doesn't tell you the context of how that data is being reached in the API (not even whether it's a query or a mutation). But I know you mentioned these events are meant to be higher level, so maybe there's a separation of concerns I'm missing out on.

@Deuchnord
Copy link
Contributor Author

Deuchnord commented Jan 3, 2019

What about adding to all the events an object that would be common to both REST and GraphQL, and would contain the context of the events? This way, you would get as many information as possible, without having to take care about Symfony's events being actually risen or not.

Currently, I can see at least two things this event could store:

  • the origin of the event (REST or GraphQL),
  • the request type (creation, read, update or delete).

Plus, we could also append the old version of the data in the PostWriteEvent. Actually, it looks Doctrine doesn't allow getting the version before changing it from the Core (or maybe I didn't look well?)

WDYT? Do you see any other thing that could be interesting to add in such an object?

@dunglas and @teohhanhui, I'd be interested by your opinion about that too :)

@jamesisaac
Copy link
Contributor

What about adding to all the events an object that would be common to both REST and GraphQL, and would contain the context of the events? This way, you would get as many information as possible, without having to take care about Symfony's events being actually risen or not.

That would be very helpful to me. In addition to your list, the things I make use of are:

  • The entity being mutated (ControllerResult)
  • The route name (e.g. when there are multiple POST actions defined with different semantics)

@Deuchnord
Copy link
Contributor Author

Closing this PR since a better implementation is proposed on #2506.

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.

7 participants