Registering event listener with dependencies

Hello,

This is in fact about 2 seperate questions.

I’m trying to implement some custom listener which is supposed to delete orphaned entites from a specific table.

  1. I am using the DI container to wire everything together, so how would I go about it here?

The doc states to register your listener in either the application or plugin class and then proceeds to provide an example. But in the example, the listener class apparently has no dependencies:

    public function events(EventManagerInterface $eventManager): EventManagerInterface
    {
        $statistics = new UserStatistic();
        $eventManager->on($statistics);

        return $eventManager;
    }
  1. My inital attempt was to use the table class as a listener, since it already implements EventListenerInterface and the logic is strongly related to the table class, but to no avail. The registered callback is never triggered:

In controller

EventManager::instance()->dispatch(new Event('InvoiceDrafts.created', $this));

In table class

    public function implementedEvents(): array
    {
        return array_merge(parent::implementedEvents(), [
            'InvoiceDrafts.created' => 'cleanUp',
        ]);
    }

    [...]

    public function cleanUp(EventInterface $event): int
    {
        return $this->deleteAll([
            'buyer_id IS' => null,
            'biller_id IS' => null,
        ]);
    }

Why is this not working? I did not plan to create a behaviour here because apparently behaviours provide a convenient way to package up behavior that is common across many models. .

Thanks

Hi @Schroeder,

combining Event Listeners and the DI container is something, that is on our list. But for now you will have to manage the event objects and their constructor dependencies yourself till this is implemented more nicely into CakePHP (maybe in v6)


But about Event Listeners in general there are a few things you may seem to misunderstand:

Event Listener scope

CakePHP allows you to add listeners globally or just to specific areas of the framework (like tables, controllers, views etc.)

You are trying to add a event listener to the table class but then dispatch the Event globally via the global event manager. This cannot work, as the dispatched event doesn’t know which table object it needs to “give” the event to.

The global event manager just knows of its own - global - event Listeners which are usually added in config/bootstrap.php or Application::bootstrap or the new Application::events hook. Those are all places where you can add global event listeneres

To make your example worke you need to dispatch the event on the table instance, not on the global event manager via doing something like this

$this->InvoiceDrafts->dispatchEvent('InvoiceDrafts.created', [], $this);

Then your cleanup method will be called.


Event Listener registration

Another important parts about event listeners is the fact, that they - obviously - they need to registerd before the event is being dispatched.

In your example this wasn’t really a problem, as you “registered” your event listener inside the table class and therefore whenever the table object is being created.

In the case of a controller method the table instance is being created the moment you try to access the property $this->InvoiceDraftsso with the adjusted code from above it will be registered before the event is actually being dispatched.


I hope this helped you understand why your code didn’t work and how you can get it working again :wink:

Also FYI: There is a proposed Attribute based system you can see in RFC - 6.0 - Attribute-Based Event Listeners · Issue #19297 · cakephp/cakephp · GitHub which may get implemented in CakePHP 6

Thanks for the explanation. What is the CakePHP way or convention - if there is one - in the described use case?

  1. attaching the cleanup logic to the table class or
  2. registering a global event listener via application.php and dispatching the event on the global EventManager

Also, what if several tables listen to the InvoiceDrafts.created event? Would I then have to dispatch the event on all of these table classes in my controller, like

$this->someTable->dispatch()
$this->otherTable->dispatch()

As always there are multiple ways to do this :smiley:

I personally extract pretty much all my shared logic into service classes and then inject them wherever I need them. You can watch this video here where I explain the whole namespace concept in PHP https://www.youtube.com/watch?v=z3E_UdH1XeE

This of course puts the burdon on you to know how you architect your software, so you’d need to have a clear image on how you want your “flow” to work in your usecase.


Coming back to your problem: As far as I can tell you want to have some sort of global trigger/event which calls the `cleanup` method on any table class which is associated to that trigger/event.

You could technically achieve that via doing 1 additional thing (which I would NOT recommend)

EventManager::instance()->on(
    TableRegistry::getTableLocator()->get('InvoiceDrafts')
);

This adds the table instance the to global event listener and therefore your initial call will also work.

But I would NOT recommend instantiating table objects if you don’t need them.

If you always need to cleanup a table instance which is already present in your controller you can always just call the table method directly via $this->InvoiceDrafts→cleanup();

But if you NEED to listen on the same event on multiple table instances and do logic on that I’d rather extract that into some sort of CleanupService class and do the logic in there.

You can use the LocatorAwareTrait to fetch/create table objects inside any class you desire.

Alright. What I get from your response it that “mis-using” table classes as event listeners for custom events (not ORM-events like beforeMarshal) is not really the CakePHP way, even if the logic is closely related to the table class itself. Thanks.

Thats at least how I see it. Maybe @dereuromark or @ADmad have something else to say about that.

I just created this PR which as a proof of concept on how DI integration for Event Listeners could look like.

1 Like