pcz/deferred-kdyby-events
deferred events extension for Nette framework (using kdyby/events & kdyby/doctrine)
aneb ~~kdyby~~ až bude Bavorov..., (*1)
This extension of kdyby/events event system provides support of deferred events in your appliaction.
Two following basic types of deferred events are implemented:, (*2)
- simple deferred event - the event is persisted & set up to be fired at some particular time in future
- grouped deferred event - each event has a groupID property. When a new event is persisted, all waiting events with its groupID
are cancelled. You can use them for example for sending notifications - when user receives 5 messages in 10 minutes, only 1 notification
will be sent using this type of event. Also, there's a class annotation @Keeping, that changes the behaviour of grouping
strategy - the nearest event will be kept, and all the following will be cancelled, so the final waiting period will not be extended.
Requirements
Installation
$ composer require pcz/deferred-kdyby-events
- enable the extension in your config neon file
extensions:
# add this line
deferredEvents: pcz\DeferredKdybyEvents\DI\DeferredEventsExtension
-
configuration of other extesions is also required, see documentation of following dependencies: Kdyby/Doctrine, Kdyby/Events and Kdyby/Console., (*3)
-
update the database schema after enabling the extension in config (be sure to clear cache and run the orm:schema-tool:update
command), (*4)
-
almost there, let's setup a cron job:, (*5)
*/5 * * * * /path/to/php /path/to/your/app/www/index.php deferredEvents:fire
Introduction
Each event is represented by its event class. It must extend one of the following base classes: pcz\DeferredKdybyEvents\DeferredEvent
or pcz\DeferredKdybyEvents\GroupedDeferredEvent
.
In fact, each event class is an ORM Entity, so don't forget to use ORM Annotations on class attributes. Usage of MTI (multi-table-inheritance) inheritance type causes that each event is represented by 1 table in database schema.
It's also necessary to update database schema after adding new event class., (*6)
Examples
Let's start with the simple deferred event type, the following example shows implementation of planned change of product price:, (*7)
The event class, (*8)
use Doctrine\ORM\Mapping as ORM;
/** @ORM\Entity */
class ChangeProductPriceEvent extends \pcz\DeferredKdybyEvents\DeferredEvent {
const EVENT_NAME = 'ChangeProductPrice';
/**
* @var Product
* @ORM\ManyToOne(targetEntity="Product")
*/
protected $product;
/**
* @var integer
* @ORM\Column(type="integer")
*/
protected $new_price;
public function __construct(\DateTime $execution_date, Product $product, $new_price) {
parent::__construct($execution_date);
$this->product = $product;
$this->new_price = $new_price;
}
public function getEventName() {
return self::EVENT_NAME;
}
public function getProduct() { return $this->product; }
public function getNewPrice() { return $this->new_price; }
}
Event creation, (*9)
/** @var $em Kdyby\Doctrine\EntityManager */
/** @var $product Product */
$event = new ChangeProductPriceEvent(
new \DateTime('2016-04-01 00:00:00'),
$product,
10
);
$em->persist($event);
$em->flush();
Event handling, (*10)
class ChangeProductEventListener implements Kdyby\Events\Subscriber {
// this listener must be registered in config, with 'kdyby.subscriber' tag of course
/** @var Kdyby\Doctrine\EntityManager */
protected $em;
public function getSubscribedEvents() {
return [ChangeProductPriceEvent::EVENT_NAME => 'changePrice'];
}
public function changePrice(ChangeProductPriceEvent $e) {
// don't use this in production, some locking should be implemented
$product = $e->getProduct();
$product->setPrice($e->getNewPrice());
$this->em->persist($product)->flush();
}
}
.. that's it:-) Now, let's see some more interesting scenario. An internal message system is part of your awesome app
and you want to send a notification when user receives a message. But you don't want to spam user's mailbox, so the notification
will be delayed. If another message is received during the delay period, user will still receive the single notification., (*11)
/** @pcz\DeferredKdybyEvents\Annotations\Keeping */
class NewMessageNotificationEvent extends pcz\DeferredKdybyEvents\GroupedDeferredEvent {
// ...
protected $user;
public function __construct(\DateTime $execution_date, User $user) {
parent::__construct($execution_date);
$this->user = $user;
}
public function getGroupKey() {
return $this->user->getId();
}
// ...
}
Note the getGroupKey()
method, it must be implemented when overriding the GroupedDeferredEvent
class. It's necessary because it
defines the grouping key.
Also the @Keeping
annotation is important, it will be discussed later. Another example is removing inactive chat member from a chat room.
Notice the lack of the @Keeping
annotation and the getGroupKey()
method., (*12)
class RemoveUserFromRoomEvent extends pcz\DeferredKdybyEvents\GroupedDeferredEvent {
// ...
public function getGroupKey() {
return $this->user->getId() . '_' . $this->room->getId();
}
// ...
}
New event then would be created every time user sends a chat message. If the delay period would be for example 48 hours (eg. call like
$event = new RemoveUserFromRoomEvent((new \DateTime())->add(new \DateInterval('PT48H')), ...)
would be used), user would be removed
from a chat room that hasn't participated to for 48 hours. When new event is persisted, all other waiting events with the same groupKey
will be suspended., (*13)
But it might not be the proper behavior for the first case (notification example). If someone send me a message every 5 minutes, i'll never
receive a notification, because the created event will always "overwrite" the last waiting one. In such cases, use the @Keeping
annotation. It
changes the strategy of grouping - the closest event will be kept and all succeeding events will be suspended., (*14)
Thanks & enjoy
& a big round of applause for Kdyby extensions, (*15)