Preventing events from triggering again (and again, and again..)
Much of the stuff, that we need to do in Shopware 6, revolves around events. For example, if we need to do something, when the product detail page loads, we can create a subscriber like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Shopware\Storefront\Page\Product\ProductPageLoadedEvent; class ProductSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ ProductPageLoadedEvent::class => 'onProductPageLoaded' ]; } public function onProductPageLoaded(ProductPageLoadedEvent $event): void { //do something, when the product page loads } } |
Another useful kind of events are the ‘onWritten’ events, that get triggered, when something is written to an entity, let’s say when a product translation is updated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent; class ProductSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ ProductEvents::PRODUCT_TRANSLATION_WRITTEN_EVENT => 'onProductTranslationWritten' ]; } public function onProductTranslationWritten (EntityWrittenEvent $event) { //do something, after product translation is written } } |
This is of course great, but a problem can appear, when we want to write some additional data at this point. The thing, that will happen here is, that the event gets triggered again and our function is getting called again. And it writes the data again, so the event is triggered again and our function is caleed again. And again and again and so on, until the server ends up in error 500 in most cases.
This infinite loop is an unwanted recursion. Recursion is a situation, when a function calls itself. It is not necessarily a bad thing, it is a standard programming technique, that can be very useful. But it is also a bit dangerous, because it can lead to infinite loops. And what is the solution, when we find ourselves in an infinite loop? We jump out, of course! There are multiple ways to do it, but for me the most easy and elegant one, that I wanted to share with you today, is removing the subscriber from the dispatcher.
Dispatcher is a system class, that takes care of triggering events in Shopware 6. It knows about our subscribers and calls them, when the corresponding event happens. In the end, all we need to do, is to tell it, that after the first call of our subscriber, it should remove our subscriber from its list of subscribers for that event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent; class ProductSubscriber implements EventSubscriberInterface { private $dispatcher; public function __construct( EventDispatcherInterface $dispatcher ) { $this->dispatcher = $dispatcher; } public static function getSubscribedEvents(): array { return [ ProductEvents::PRODUCT_TRANSLATION_WRITTEN_EVENT => 'onProductTranslationWritten' ]; } public function onProductTranslationWritten (EntityWrittenEvent $event) { //remove this subscriber from the dispatcher $this->dispatcher->removeSubscriber($this); //do something, after product translation is written, just once } } |
And that’s it – no more unwanted recursion in event subscribers!
UPDATE: this approach does not work in some cases, so I have come up with an alternative solution.