cellulosa
cellulosa

Reputation: 489

symfony2 flash messages as event listener to handle errors

We have set up an event listener that triggers flash messages when an entity gets updated, removed or persisted, but what we can't quite manage is how to handle errors.

Here is the relevant code from services.yml

flash_messages:
    class: Acme\AcmeBundle\EventListener\FlashMessages
    tags:
        - { name: doctrine.event_listener, event: postUpdate }
        - { name: doctrine.event_listener, event: postRemove }
        - { name: doctrine.event_listener, event: postPersist }
    arguments: [ @session, @translator ]

and this is the listener in Acme/AcmeBundle/EventListener/FlashMessages.php

namespace Acme\AcmeBundle\EventListener;

use
    Doctrine\ORM\Event\LifecycleEventArgs,
    Symfony\Component\HttpFoundation\Session\Session,
    Symfony\Component\Translation\TranslatorInterface
;

class FlashMessages
{
    private $session;
    protected $translator;

    public function __construct(Session $session, TranslatorInterface $translator)
    {
        $this->session = $session;
        $this->translator = $translator;
    }

    public function postUpdate(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        $this->session->getFlashBag()->add(
            'success',
            $this->translator->trans(
                '%name% entity.write.success',
                array('%name%' => $entity->getClassName())
            )
        );
    }

    public function postRemove(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        $this->session->getFlashBag()->add(
            'success',
            $this->translator->trans(
                '%name% entity.delete.success',
                array('%name%' => $entity->getClassName())
            )
        );
    }

    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        $this->session->getFlashBag()->add(
            'success',
            $this->translator->trans(
                '%name% entity.create.success',
                array('%name%' => $entity->getClassName())
            )
        );
    }
}

What we are trying to do, is to clean the controller and move all the error messages into the event listener. For example, this is our DeliveryController.php:

/**
 * Finds and displays a Delivery entity.
 *
 * @Route("/{id}", name="delivery_show")
 * @Method("GET")
 * @Template()
 */
public function showAction($id)
{
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('AcmeBundle:Delivery')->find($id);

    if (!$entity) {

        /**
         * @todo move this code to eventListener
         */
        $entity = new Delivery();

        $this->get('session')->getFlashBag()->add(
            'danger',
            $this->get('translator')->trans(
                '%name% entity.find.fail',
                array('%name%' => $entity->getClassName())
            )
        );
        // end @todo

        return new RedirectResponse($this->generateUrl('delivery'));
    }

    return array(
        'entity'      => $entity,
        'menu_tree' => $this->menu_tree,
    );
}

Like that, ideally we want to handle errors also when an entity fails to get created, persisted, and deleted.

For reference, translations are held by Acme/AcmeBundle/Resources/translations/messages.en.yml

%name% entity.create.fail: There wes an error creating the %name%. Please try again later.
%name% entity.create.success: %name% created successfully.
%name% entity.write.fail: There wes an error saving the %name%. Please try again later.
%name% entity.write.success: %name% saved successfully.
%name% entity.delete.fail: There wes an error deleting the %name%. Please try again later.
%name% entity.delete.success: %name% deleted successfully.
%name% entity.find.fail: %name% not found.

And $entity->getClassName() sits in every entity as it follows:

private $className;

/**
 * Get class name
 * @return string
 */
public function getClassName()
{
    $entity = explode('\\', get_class($this));
    return end($entity);
}

Upvotes: 2

Views: 4066

Answers (1)

cellulosa
cellulosa

Reputation: 489

We ended up up changing the way our FlashMessages worked, because we had an issue with chain of entities getting updated and too many flash messages being displayed. Doing int the following way allowed us to stop the chain effect. We went from postUpdate(), postUpdate() and postPersist() to using a single onFlush(). See our code below:

AcmeBundle/Resources/config/services.yml

flash_messages:
    class: Acme\AcmeBundle\EventListener\FlashMessages
    tags:
        - { name: doctrine.event_listener, event: onFlush }
    arguments: [ @session, @translator, @service_container ]

AcmeBundle/EventListener/FlashMessages.php

<?php
namespace Acme\AcmeBundle\EventListener;

use
    Symfony\Component\HttpFoundation\Session\Session,
    Symfony\Component\Translation\TranslatorInterface,
    Doctrine\ORM\Event\OnFlushEventArgs,
    Acme\AcmeBundle\Entity\DeliveryItem
;

class FlashMessages
{
    private $session;
    protected $translator;

    public function __construct(Session $session, TranslatorInterface $translator)
    {
        $this->session = $session;
        $this->translator = $translator;
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        $this->em = $args->getEntityManager();
        $uow = $this->em->getUnitOfWork();
        $insert = current($uow->getScheduledEntityInsertions());
        $update = current($uow->getScheduledEntityUpdates());
        $delete = current($uow->getScheduledEntityDeletions());

        // Don't show messages when updating individual DeliveryItem and StockHistory
        if ($insert instanceof DeliveryItem ||
            $update instanceof DeliveryItem ||
            $delete instanceof DeliveryItem) {

            return false;
        }

        // Flash message on insert
        if ($uow->getScheduledEntityInsertions()) {
            $this->session->getFlashBag()->add(
                'success',
                $this->translator->trans(
                    '%name% entity.create.success',
                    array('%name%' => $insert->getClassName())
                )
            );
        }

        // Flash message on update
        if ($uow->getScheduledEntityUpdates()) {
            $this->session->getFlashBag()->add(
                'success',
                $this->translator->trans(
                    '%name% entity.write.success',
                    array('%name%' => $update->getClassName())
                )
            );
        }

        // Flash message on delete
        if ($uow->getScheduledEntityDeletions()) {
            $this->session->getFlashBag()->add(
                'success',
                $this->translator->trans(
                    '%name% entity.delete.success',
                    array('%name%' => $delete->getClassName())
                )
            );
        }
    }
}

We tried to set up a onKernelException(GetResponseForExceptionEvent $event) function to handle the messages on entities not found, but in order to make that working we still had to throw an exception from the controller, passing the message translated... Basically we still had to repeat a good chunk of code, so we just rolled back to the original solution; that is, managing flash messages straight away from the controller's actions:

...

if (!$entity) {

    $entity = new Delivery();

    $this->get('session')->getFlashBag()->add(
        'danger',
        $this->get('translator')->trans(
            '%name% entity.find.fail',
            array('%name%' => $entity->getClassName())
        )
    );

    return new RedirectResponse($this->generateUrl('delivery'));
}

...

Upvotes: 3

Related Questions