Magento 2: Zdarzenia i obserwatory
Magento implementuje wzorzec projektowy zdarzenia (ang. event) i obserwatora (ang. observer). Wzorzec ten daje nam możliwość modyfikacji w kluczowych miejscach przetwarzania w Magento. Dzięki temu możemy w łatwy sposób wiązać pomiędzy sobą różne moduły.
Implementacja wzorca zdarzenia i obserwatory składa się z dwóch części: generatora zdarzenia, który posiada informacje na temat obiektu i samego zdarzenia, oraz obserwatora, który nasłuchuje konkretnego zdarzenia.
Generator zdarzeń
Do tworzenia i generowania zdarzeń używamy klasy Magento\Framework\Event\ManagerInterface. Interfejs zawiera tylko jedną metodę:
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 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ declare(strict_types=1); namespace Magento\Framework\Event; /** * Interface \Magento\Framework\Event\ManagerInterface * * @api */ interface ManagerInterface { /** * Dispatch event * * Calls all observer callbacks registered for this event * and multiple observers matching event name pattern * * @param string $eventName * @param array $data * @return void */ public function dispatch($eventName, array $data = []); } |
Tak więc wygenerowanie zdarzenia jest proste. Klasę podajemy jako zależność do konstruktora. Następnie wywołujemy metodę dispatch() klasy event managera, gdzie podajemy nazwę zdarzenia oraz tablicę z danymi.
Przejdźmy do przykładu z generowaniem zdarzenia. Mamy klasę Magento\Catalog\Model\ResourceModel\Product\Collection, która jest odpowiedzialna za załadowanie kolekcji produktów. Po załadowaniu kolekcji wykonywana jest metoda _afterLoad(), która wykonuje dodatkowe działania na kolekcji. Na końcu emituje ona zdarzenie catalog_product_collection_load_after:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents. * * @return $this */ protected function _afterLoad() { if ($this->_addUrlRewrite) { $this->_addUrlRewrite(); } $this->_prepareUrlDataObject(); $this->prepareStoreId(); if (count($this)) { $this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]); } return $this; } |
Przy okazji, dodam, że to nie jest jedyna klasa, która generuje zdarzenie catalog_product_collection_load_after. Dokumentacja List of events zawiera listę zdarzeń. Znajdziemy tam informacje o tym, jakie klasy generują jakie zdarzenia wraz z krótkim opisem, zawierający również informację o rodzajach obiektów przekazanych do tablicy.
Działanie metody dispatch()
Jeśli sprawdzimy konfigurację w app/etc/di.xml, to zobaczymy, że interfejsowi Magento\Framework\Event\ManagerInterface odpowiada klasa Magento\Framework\Event\Manager\Proxy. Proxy jest klasą wygenerowaną przez Magento, więcej przeczytasz o tym w tej notce). Właściwa klasa Magento\Framework\Event\Manager posiada zaimplementowaną metodę disptach() w następujący sposób:
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 |
/** * Dispatch event * * Calls all observer callbacks registered for this event * and multiple observers matching event name pattern * * @param string $eventName * @param array $data * @return void */ public function dispatch($eventName, array $data = []) { $eventName = $eventName !== null ? mb_strtolower($eventName) : ''; \Magento\Framework\Profiler::start('EVENT:' . $eventName, ['group' => 'EVENT', 'name' => $eventName]); foreach ($this->_eventConfig->getObservers($eventName) as $observerConfig) { $event = new \Magento\Framework\Event($data); $event->setName($eventName); $wrapper = new Observer(); // phpcs:ignore Magento2.Performance.ForeachArrayMerge $wrapper->setData(array_merge(['event' => $event], $data)); \Magento\Framework\Profiler::start('OBSERVER:' . $observerConfig['name']); $this->_invoker->dispatch($observerConfig, $wrapper); \Magento\Framework\Profiler::stop('OBSERVER:' . $observerConfig['name']); } \Magento\Framework\Profiler::stop('EVENT:' . $eventName); } |
Metoda wykonuje następujące czynności:
- Wczytuje konfigurację po nazwie zdarzenia,
- Dla każdego z dostępnych obserwatorów generator zdarzenia tworzy instancję obiektu obserwatora.
- Dalsze działania wykonuje klasa kryjąca się pod interfejsem Magento\Framework\Event\InvokerInterface, której wywoływana jest metoda dispatch(). Na podstawie konfiguracji decyduje czy ostatecznie wywołać metodę. Jeśli tak to tworzy w odpowiedni sposób instancję zdefiniowanej klasy nasłuchującej, która docelowo implementuje interfejs Magento\Framework\Event\ObserverInterface. Następnie wywołuje metodę execute() narzuconą przez ten interfejs i podaje do niej utworzony wcześniej obiekt obserwatora.
Pod interfejsem Magento\Framework\Event\InvokerInterface kryje się klasa Magento\Framework\Event\Invoker\InvokerDefault (określonej w domyślnej konfiguracji w app/etc/di.xml). Omówiona w ostatnim podpunkcie metoda wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * Dispatch event * * @param array $configuration * @param Observer $observer * @return void */ public function dispatch(array $configuration, Observer $observer) { /** Check whether event observer is disabled */ if (isset($configuration['disabled']) && true === $configuration['disabled']) { return; } if (isset($configuration['shared']) && false === $configuration['shared']) { $object = $this->_observerFactory->create($configuration['instance']); } else { $object = $this->_observerFactory->get($configuration['instance']); } $this->_callObserverMethod($object, $observer); } |
Sprawdza ona również parametry konfiguracji takie jak disabled, shared i instance, która definiowana jest w pliku events.xml. Parametry te omówimy w części dotyczącej definiowania konfiguracji dla klasy nasłuchującej.
Wiązanie Obserwatorów
Generowanie zdarzeń to jedno, konieczne jest również wskazanie, który obserwator nasłuchuje jakiego zdarzenia. Konfigurację taką definiujemy w pliku events.xml. Nasz przykładowy moduł będzie się nazywał Anna_Events. Obsłużymy tutaj wcześniej wspomniane zdarzenie: catalog_product_collection_load_after.
Potrzebujemy wykonać następujące czynności:
- Stworzyć nowy katalog Observer w naszym module, gdzie dodamy klasę obsługującą zdarzenie,
- Stworzona klasa obsługująca zdarzenie musi implementować interfejs Magento\Framework\Event\ObserverInterface,
- Ostatnim krokiem jest zarejestrowanie obsługi obserwatora, czyli określenie konfiguracji w pliku events.xml.
Klasa obsługująca zdarzenie
Na początek stworzymy klasę. Klasy obsługi obserwatora umieszczamy katalogu Observer.
Nasza klasa obsługująca obserwatora zwyczajowo nie dziedziczy po żadnej klasie. Jedynym wymogiem jest tutaj implementacja interfejsu Magento\Framework\Event\ObserverInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?php /** * Observer interface * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Event; /** * Interface \Magento\Framework\Event\ObserverInterface * * @api * @since 100.0.2 */ interface ObserverInterface { /** * @param Observer $observer * @return void */ public function execute(Observer $observer); } |
Zdarzenie catalog_product_collection_load_after klasę Magento\Catalog\Model\ResourceModel\Product\Collection. Obserwator otrzyma tablicę, która zawiera jeden element z kluczem collection. Zawiera on obiekt załadowanej kolekcji produktów. Dla przykładu wypiszemy pewne dane o produktach do pliku logu. Poniżej utworzona klasa Anna\Events\Observer\CatalogProductCollectionLoadAfter:
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 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php declare(strict_types=1); namespace Anna\Events\Observer; use Magento\Catalog\Model\Product; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Psr\Log\LoggerInterface; class CatalogProductCollectionLoadAfter implements ObserverInterface { /** @var LoggerInterface */ protected $logger; public function __construct( LoggerInterface $logger ) { $this->logger = $logger; } public function execute(Observer $observer): void { $collection = $observer->getCollection(); // or by $observer->getData('collection'); foreach ($collection as $product) { $this->logProductData($product); } } protected function logProductData(Product $product): void { $data = [ 'name' => $product->getName(), 'price' => $product->getPrice(), ]; $this->logger->debug(sprintf('Info about product with id %d', $product->getId())); $this->logger->debug(print_r($data, true)); } } |
Dane będą logowane do pliku debug.log, który jest dostępny w katalogu var/log.
Konfiguracja events.xml
Plik events.xml tworzymy w katalogu etc i możemy go stworzyć bezpośrednio w tym katalogu, aby obserwator nasłuchiwał w globalnym zasięgu, lub w zależności od potrzeb, umieścić go w podkatalogu z nazwą obszaru.
Konfiguracja dla naszego przykładu w pliku etc/frontend/events.xml:
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="catalog_product_collection_load_after"> <observer name="catalog_product_collection_load_after" instance="Anna\Events\Observer\CatalogProductCollectionLoadAfter"/> </event> </config> |
Element <event> zawiera jeden atrybut, name, który jest nazwą wyemitowanego zdarzenia. Wewnątrz tego elementu tworzymy element <observer>, dla którego możemy zdefiniować następujące atrybuty:
- name (wymagany) — nazwa dla naszej definicji. Powinna być ona unikalna, w przeciwnym razie może dojść do przypadkowego nadpisania.
- instance (wymagany) — podajemy naszą klasę, która obsługuję zdarzenie. Pamiętaj o tym, by klasa implementowała interfejs Magento\Framework\Event\ObserverInterface, jako że nie wskazujemy, jaka to metoda ma zostać wywołana (jak to było w przypadku Magento 1).
- disabled — określa, czy dana nasłuchiwanie zdarzenia jest aktywne, czy nie. Domyślną wartością jest false.
- shared — determinuje cykl życia klasy. Domyślna wartość to true.
Ostatnim krokiem będzie weryfikacja czy wszystko działa. Potrzebujemy wyczyścić cache, a następnie wejść na kategorie produktów i sprawdzić log debug.log. Fragment przykładowego logu:
1 2 3 4 5 6 7 |
[2022-09-11T22:30:25.147877+00:00] main.DEBUG: Info about product with id 1383 [] [] [2022-09-11T22:30:25.151272+00:00] main.DEBUG: Array ( [name] => Olivia 1/4 Zip Light Jacket-XS-Purple [price] => 77.000000 ) [] [] |
Pluginy w Magento 2 - Web Programming
4 grudnia 2023 @ 02:57
[…] „przechwytaczami” (ang. interceptors), stanowią mechanizm nadpisywania klas zaraz obok obsługi zdarzeń czy bezpośredniego nadpisywania klasy za pomocą konfiguracji […]