Обновление doctine entity в preUpdate event

Автор Руслан Коваленко 7/06/2012

Есть entity:

class Product
{
    protected $price;
    protected $discount;
}

Т.о. у нас есть цена и скидка. Скидка высчитывается из цены, подсчет осуществляется отдельным классом DiscountCalculator, который зареган как service.
Необходимо чтобы каждый раз при изменении цены, обновлялась скидка. Для этого будем перехватывать doctrine event: preUpdate.
Доктрина кидает множество евентов на различные события:

Если бы вычисления скидки были простыми (например скидка = 10% от цены) — мы бы могли зарегать lifecycle callback в самом классе entity:

/*
* @ORM\HasLifecycleCallbacks
 */

class Product
{
    …
    /**
     * @ORM\PreUpdate
     */

    public function updateDiscount()
    {
        $this->discount = $this->price * 10 / 100;
    }
}


Но вычисление скидки происходит через service DiscountCalculator. К service container в классе entity доступа нет, соотв-но и к DiscountCalculator тоже. Поэтому мы создаем отдельный класс-листенер и перехватываем в нем preUpdate event.

class ProductOrmListener
{
    protected $container;

    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();
        ….
    }
}

Подписываемся на событие:

#XXX/Resources/config/services.xml
<services>
    <service id="product.orm.listener" class="XXX\ProductOrmListener">
        <argument type="service" id="service_container" />
        <tag name="doctrine.event_listener" event="preUpdate"/>
    </service>
</services>

Теперь если значения полей Product изменятся, то при сохранении в базу, будет вызвано событие preUpdate, которое мы перехватим.

Далее нам нужно подсчитать скидку и сохранить ее новое значение.

Вариант 1.
Просто обновить поля entity. В этом случае изменения не сохранятся :)

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();
   
        if ($entity instanceof Product)
        {
            $entity->setDiscount(
                 $this->container->get('discount_calculator')->getDiscount($entity->getPrice())
            );
        }
    }

Вариант 2.
После этого обращаем внимание на класс PreUpdateEventArgs. У него есть функция setNewValue( string $field, mixed $value). Попробуем воспользоваться ей:

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();

        if ($entity instanceof Product)
        {
            $args->setNewValue(
                "discount",
                 $this->container->get('discount_calculator')->getDiscount($entity->getPrice())
            );
        }
    }

Однако нас ожидает ошибка:

"Field 'discount' is not a valid field of the entity"

Эта ошибка возникает из-за того, что придать новые значения мы можем только тем полям, которые поменялись. Т.е. которые присутствуют в так называем changeset. Т.к. мы в коде меняем поле price, то оно будет тут присутствовать, а вот discount нет.

Вариант 3.
Теперь возьмем за основу первый вариант и воспользуемся классом UnitOfWork.
С помощью него можно выполнить так называемый пересчет изменений и т.о. они сохранятся и занесутся в базу. Начиная с версии 2.2 есть метод: recomputeSingleEntityChangeSet.

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        if ($entity instanceof Product)
        {
            $entity->setDiscount(
                $this->container->get('discount_calculator')->getDiscount($entity->getPrice())
            );

            $uow->recomputeSingleEntityChangeSet(
                $em->getClassMetadata("XXXBundle:Product"),
                $entity
             );
        }
    }

Для полной картины проверим так же что изменения поля цены вообще происходили, чтобы лишний раз не делать пересчет. Также добавим event prePersist, чтобы задавать скидку при создании продукта.

#XXX/Resources/config/services.xml
<services>
    <service id="product.orm.listener" class="XXX\ProductOrmListener">
        <argument type="service" id="service_container" />
        <tag name="doctrine.event_listener" event="preUpdate"/>
        <tag name="doctrine.event_listener" event="prePersist"/>
    </service>
</services>

    public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        $entityManager = $args->getEntityManager();

        if ($entity instanceof Product)
        {
            $entity->setDiscount(
                $this->container->get('discount_calculator')->getDiscount($entity->getPrice())
            );
        }
    }

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getEntity();
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        if ($entity instanceof Product)
        {
            $changeset = $uow->getEntityChangeSet($entity);
            if(isset($changeset['price']) && ($changeset['price'][0] != $changeset['price'][1]))
            {
                $entity->setDiscount(
                    $this->container->get('discount_calculator')->getDiscount($entity->getPrice())
                );

                $uow->recomputeSingleEntityChangeSet(
                    $em->getClassMetadata("XXXBundle:Product"),
                    $entity
                );
            }
       }
    }


1 Комментарий

  1. 22/07/2013Sreucherand пишет:

    I solved my problem thanks to you. Good article !

Ваш комментарий:

Подтвердите, что Вы не бот — выберите человечка с поднятой рукой: