Introduction to events and listeners
How it works
- Code that wants to announce something calls
$this->eventManager()->fire('onModelAfterCreate', [$model, $savedRecord]). - The
EventManagercalls the methodonModelAfterCreateof every listener that was registered for this event. - The listener does its work. Its return value is ignored.
The core fires the model events from the callbacks of Hubleto\Framework\Model. That is why the callbacks of your models must call parent::: without it the event is never fired.
Write a listener
A listener extends Hubleto\Framework\EventListener and has one method named after each event it handles. EventManager::fire() calls $listener->onEventName(...$args) for every listener registered for the event, so the listener must have a method with that exact name for every event it is registered for. PHP raises an error otherwise.
apps/PriceLists/EventListeners/InvalidateScopeCache.php
<?php
namespace Hubleto\App\Community\PriceLists\EventListeners;
use Hubleto\App\Community\Customers\Models\CustomerTag;
use Hubleto\App\Community\Products\Models\Category;
use Hubleto\App\Community\Products\Models\Group;
use Hubleto\App\Community\PriceLists\Services\PriceResolver;
use Hubleto\Framework\Interfaces\ModelInterface;
/**
* Clears cached categories, groups and customer tags of the price resolver when they change within a request.
*/
class InvalidateScopeCache extends \Hubleto\Framework\EventListener implements \Hubleto\Framework\Interfaces\EventListenerInterface
{
public function onModelAfterCreate(ModelInterface $model, array $savedRecord): void
{
if (!$this->isScopeModel($model)) return;
$this->clear();
}
public function onModelAfterUpdate(ModelInterface $model, array $originalRecord, array $savedRecord): void
{
if (!$this->isScopeModel($model)) return;
$this->clear();
}
public function onModelAfterDelete(ModelInterface $model, int $id): void
{
if (!$this->isScopeModel($model)) return;
$this->clear();
}
protected function isScopeModel(ModelInterface $model): bool
{
return $model instanceof Category || $model instanceof Group || $model instanceof CustomerTag;
}
protected function clear(): void
{
/** @var PriceResolver $resolver */
$resolver = $this->getService(PriceResolver::class);
$resolver->clearCache();
}
}
Listeners of onModel* events receive the events of all models in the system, so the first thing a listener does is to check what it got and return if it does not care.
Register a listener
Register the listener in Loader::init() of your app, once for each event:
apps/PriceLists/Loader.php (excerpt)
/** @var EventListeners\InvalidateScopeCache $invalidateCache */
$invalidateCache = $this->getService(EventListeners\InvalidateScopeCache::class);
foreach (['onModelAfterCreate', 'onModelAfterUpdate', 'onModelAfterDelete'] as $event) {
$this->eventManager()->addEventListener($event, $invalidateCache);
}
Listeners of a project
A listener that does not belong to an app goes to src/event-listeners of the project. The folder is autoloaded (namespace HubletoProject\EventListeners, see composer.json of the project), but nothing registers the classes in it automatically. Register them yourself, e.g. in init() of a custom app of the project:
erp-project: src/event-listeners/Example.php
<?php
/*
This event listener does not do anything. It is just an example of how the
event listener should be implemented.
*/
namespace HubletoProject\EventListeners;
use Hubleto\Erp\Controller;
class Example extends \Hubleto\Framework\EventListener implements \Hubleto\Framework\Interfaces\EventListenerInterface
{
public function onControllerSetView(Controller $controller, string $view): void
{
// This method is invoked ater a $eventManager()->fire('onControllerSetView')
// Do anything you want here.
}
}
$this->eventManager()->addEventListener(
'onControllerSetView',
$this->getService(\HubletoProject\EventListeners\Example::class)
);
Rules
- Keep listeners fast. They run inside the request that saves the record.
- Never throw an exception, unless you really want to stop the saving in an
onModelBefore*event. - Do not modify the record in an
onModelAfter*listener. Use the model's callbacks for that. - One listener per concern. Do not collect unrelated reactions in one class.
