Let's imagine a situation: you have an order in an online store (Entity). The order has a certain status. When changing the status of the order, it is necessary to conduct a bunch of related actions, for example:
The question arises how to organize all this correctly in terms of program code.
Everything described below is true for Doctrine 2 and Symfony> 3.1.
If you are not familiar with the Doctrine event model, I first recommend that you familiarize yourself with the documentation .
I will give an example of the simplest code for an Entity order:
/** * Order * * @ORM\Table(name="order") */ class Order { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="fio", type="string", length=100) */ private $fio; /** * @ORM\Column(name="last_update_date", type="datetime") */ private $lastUpdateDate; /** * @ORM\Column(name="create_date", type="datetime") */ private $createDate; /** * @ORM\Column(name="status_id", type="integer") */ private $statusId; // getter/setter }
Let's start with the simplest - we need to have the creation date recorded in the create_date
field, and when any order changes, the last modified date in the last_update_date
field.
The simplest thing is to explicitly add parameters in the place where the order is created and updated (in the controller or a special service).
$order = new Order(); $order->setCreateDate(new \DateTime()); $order->setLastUpdateDate(new \DateTime()); // .... $em->persist($order); $em->flush();
The disadvantages of this approach are obvious - if an order is created, and even more so, it is updated in several places - it will be necessary to repeat this logic in each place. Fortunately, Doctrine contains event handling (LifecycleEvents).
Add to the Entity description a construct that tells Doctrine that the Entity contains some events that need to be processed:
/** * @ORM\HasLifecycleCallbacks() */
and create methods that will "respond" to these events. In our case there will be two methods:
/** * @ORM\PrePersist */ public function setCreateDate() { $this->createDate = new \DateTime(); } /** * @ORM\PreFlush */ public function setLastUpdateDate() { $this->lastUpdateDate = new \DateTime(); }
@ORM\PrePersist
and @ORM\PreFlush
tell Doctrine to execute the corresponding methods respectively when creating an Entity and each time it is updated. Now there is no need to set these dates separately. Full list of possible events can be found here.
/** * Order * * @ORM\Table(name="order") * @ORM\HasLifecycleCallbacks() */ class Order { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="fio", type="string", length=100) */ private $fio; /** * @ORM\Column(name="last_update_date", type="datetime") */ private $lastUpdateDate; /** * @ORM\Column(name="create_date", type="datetime") */ private $createDate; /** * @ORM\Column(name="status_id", type="integer") */ private $statusId; // getter/setter /** * @ORM\PrePersist */ public function setCreateDate() { $this->createDate = new \DateTime(); } /** * @ORM\PreFlush */ public function setLastUpdateDate() { $this->lastUpdateDate = new \DateTime(); } }
Let's complicate the task - now we need to record in the order history information about who and when changed the status of this order, plus we want to send a letter about the status change to the client.
/** * OrderHistory * * @ORM\Table(name="order_status_history") * @ORM\HasLifecycleCallbacks() */ class OrderHistory { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="order_id", type="integer") */ private $orderId; /** * @ORM\Column(name="manager_id", type="integer") */ private $managerId; /** * @ORM\Column(name="status_id", type="integer") */ private $statusId; /** * @ORM\ManyToOne(targetEntity="OrderStatus") * @ORM\JoinColumn(name="status_id", referencedColumnName="id") */ private $status; /** * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Manager") * @ORM\JoinColumn(name="manager_id", referencedColumnName="id") */ private $manager; /** * @ORM\ManyToOne(targetEntity="Order", inversedBy="orderHistory") * @ORM\JoinColumn(name="order_id", referencedColumnName="id") */ private $order; // getter/setter /** * @ORM\Column(name="create_date", type="datetime") */ private $createDate; /** * @ORM\PrePersist */ public function setCreateDate() { $this->createDate = new \DateTime(); } }
You can do all this "manually" in the place of the code where the status changes, but I would like everything to happen "automatically" without reference to the place of the operation to change the status.
For this, Doctrine has EntityListeners , a class that tracks changes; a place where you can keep all the logic of event handling.
There are two options: either we add an event handler at the Entity description level:
/** * @ORM\EntityListeners({"AppBundle\EntityListeners\OrderListener"}) */
And create a class Listener
class OrderHistoryListener { public function postUpdate(Order $order, LifecycleEventArgs $event) { // some code } }
The first parameter is a reference to the object in which the events occurred. The second is the event object (we'll talk about it below).
Or,
you can register handlers through standard symfony services:
services: order.history.status.listener: class: AppBundle\EntityListeners\OrderListener tags: - { name: doctrine.event_listener, event: preUpdate, method: preUpdate } - { name: doctrine.event_listener, event: prePersist, method: prePersist }
The event
parameter defines the event for which the given service will be called, method
- determines the specific method within the service. Those. service can be one, but to process different events for different Entity.
In this case, the Listener will respond to events in general of any Entity and inside the class it will be necessary to check the type of the object.
class OrderHistoryListener { public function preUpdate(PreUpdateEventArgs $event) { if ($event->getEntity() instanceof Order) { } } }
EntityListener may contain different methods (handlers), depending on which event we want to receive a response.
The $event
object already contains references to the EntityManager and to UnitOfWork. Accordingly, there is already everything to work with Doctrine objects. You can pull out the necessary objects, update and delete them.
Difficulties begin when you want to do something that is not related to the base, for example, send a letter. To do this, you need to implement dependencies on external services in the EntityListener.
In the first case, we create a view entry that will inject the dependencies into the EntityListener
services: app.doctrine.listener.order: class: AppBundle\EntityListeners\OrderListener public: false arguments: ["@mailer", "@security.token_storage"] tags: - { name: "doctrine.orm.entity_listener" }
In the second, just add a line with dependencies.
services: order.history.status.listener: class: AppBundle\EntityListeners\OrderListener arguments: ["@mailer", "@security.token_storage"] tags: - { name: doctrine.event_listener, event: preUpdate, method: preUpdate } - { name: doctrine.event_listener, event: prePersist, method: prePersist }
Then everything is as with the usual symfony-service.
Inside the Listener, you can get a check on whether the field has changed, and also get the current and previous values.
if ($event->hasChangedField('status_id')) { $oldValue = $event->getOldValue('status_id'); $newValue = $event->getNewValue('status_id'); }
/** * Order * * @ORM\Table(name="order") * @ORM\EntityListeners({"AppBundle\EntityListeners\OrderListener"}) * @ORM\HasLifecycleCallbacks() */ class Order { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="fio", type="string", length=100) */ private $fio; /** * @ORM\Column(name="last_update_date", type="datetime") */ private $lastUpdateDate; /** * @ORM\Column(name="create_date", type="datetime") */ private $createDate; /** * @ORM\Column(name="status_id", type="integer") */ private $statusId; // getter/setter /** * @ORM\PrePersist */ public function setCreateDate() { $this->createDate = new \DateTime(); } /** * @ORM\PreFlush */ public function setLastUpdateDate() { $this->lastUpdateDate = new \DateTime(); } }
class OrderListener { private $_securityContext = null, $_mailer = null; public function __construct(\SwiftMailer $mailer, TokenStorage $securityContext) { $this->_mailer = $mailer; $this->_securityContext = $securityContext; } public function postUpdate(Order $order, LifecycleEventArgs $event) { $em = $event->getEntityManager(); if ($event->hasChangedField('status_id')) { $status = $em->getRepository('AppBundle:OrderStatus')->find($event->getNewValue('status_id')); $history = new OrderHistory(); $history->setManager($this->_securityContext->getToken()->getUser()); $history->setStatus($status); $history->setOrder($order); $em->persist($history); $em->flush(); // SwiftMailer } } }
Source: https://habr.com/ru/post/339580/
All Articles