Pluginy w Magento 2
W dzisiejszym wpisie omówię jak działają pluginy w Magento 2. Pluginy (wprowadzone od Magento 2) stanowią mechanizm nadpisywania klas zaraz obok obsługi zdarzeń czy bezpośredniego nadpisywania klasy za pomocą konfiguracji preference. Pluginy są używane w parze wraz z generowanymi przez Magento klasami-przechwytaczami (ang. interceptors).
Jaki problem rozwiązują pluginy
W Magento 1 jedynym sposobem, jeśli obsługa zdarzenia nie była dostępna, było bezpośrednie nadpisanie klasy. Przy czym, jeśli robił to równocześnie drugi moduł, to występowały konflikty. Powodem tego było to, że takie nadpisywanie działało globalnie — odpowiednikiem takiego działania jest właśnie stosowanie preference w Magento 2. Żeby zaradzić temu problemowi, został wprowadzony mechanizm pluginów.
Pluginy pozwalają na modyfikowanie publicznej metody oryginalnej klasy/interfejsu. Mają one dostęp do parametrów oryginalnej metody, więc możemy modyfikować ich wartość, zmienić wartość zwracaną przez metodę czy też wykonywać inne, dodatkowe czynności — w tym podmienić całą logikę oryginalnej metody.
Dzięki pluginom mamy możliwość dokonania wielu modyfikacji funkcjonalności klas lub interfejsów bez konieczności nadpisywania oryginalnej klasy/interfejsu.
Kiedy nie możemy stosować pluginów
Pluginy możemy stosować tylko w określonych sytuacjach. Najważniejszym ograniczeniem jest to, że nadpisywana metoda musi być publiczna. Pełna lista ograniczeń jest następująca:
- Metody finalne,
- Klasy finalne,
- Niepubliczne metody,
- Metody statyczne,
- metody __construct() i __descruct(),
- klasy typu VirtualType (określane przez konfigurację w di.xml),
- Obiekty, które są inicjalizowane przed załadowaniem klasy Magento\Framework\Interception,
- Klasy implementujące interfejs Magento\Framework\ObjectManager\NoninterceptableInterface. Interfejs ten implementują generowane klasy proxy.
Rodzaje pluginów
Pluginy są to klasy, które pozwalają na modyfikowanie publicznej metody oryginalnej klasy w trakcie jej wykonywania. W zależności od tego, kiedy wprowadzana jest modyfikacja, możemy rozróżnić trzy rodzaje pluginów:
- before — przed wykonaniem właściwej metody,
- after — po wykonaniu nadpisywanej metody,
- around — w trakcie wykonywania nadpisywanej metody, tutaj wręcz możemy całkowicie „wymienić” działanie oryginalnej metody.
W dalszej części pokażę, jak wyglądają definicje tych metod w klasie pluginu.
Nazewnictwo
Ogólnie klasa pluginu umieszczamy w katalogu Plugin. Zwyczajowo na końcu nazwy klasy dodajemy „Plugin„:
Vendor\Module\Plugin\<ModelName>Plugin
Samo nazewnictwo metod dla pluginu składa się z dodania na początek jednego z prefiksów: before, around lub after oraz nazwy metody rozpoczętej wielką literą.
public function _____Test(...)
- before
- around
- after
public function ______test(...)
- before
- around
- after
Jak wygląda deklaracja pluginu
Tworzymy nowy moduł, Anna_Plugin.
Następnie w katalogu etc tworzymy plik di.xml.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Framework\Pricing\Render\Amount"> <plugin name="customize_pricing_render_amount" sortOrder="10" type="Anna\Plugins\Plugin\Magento\Framework\Pricing\RenderAmountPlugin" disabled="false" /> </type> </config> |
Używamy węzła <type>, dla jego atrybutu name podajemy klasę, którą chcemy nadpisać. Wewnątrz niego tworzymy węzeł <plugin>, gdzie ustawiamy plugin. Ma on dostępne atrybuty:
- name (wymagany) — po prostu nazwa pluginu. Nazwa powinna być unikalna, aby nie doszło do przypadkowego nadpisania innego pluginu,
- type (wymagany) – wskazanie klasy pluginu,
- sortOrder (opcjonalny) – kolejność, parametr brany pod uwagę, gdy parę pluginów modyfikuję metodę,
- disabled (opcjonalny) – Domyślna wartość: false. Pozwala na wyłączenie wybranego pluginu.
W dalszej części pokażę jak klasę danego typu pluginu zaimplementować.
Plugin before
Jak już wspomniałam, plugin before jest wywoływany przed wywołaniem oryginalnej metody.
Tworząc metodę typu before, potrzebujemy za pierwszy argument podać oryginalną klasę. Następnie, jeśli oryginalna metoda posiadała argumenty, to również je podajemy z zachowaniem typów dla poszczególnych argumentów. Ostatecznie taka metoda powinna zwrócić tablicę z listą argumentów. Jeśli nie doszło do modyfikacji żadnego z argumentów oryginalnej metody, powinniśmy zwrócić po prostu wartość null.
Przykład z podmienieniem templaty
Możemy spróbować stworzyć prostą template z napisem „Hello world” i podmienić wybranemu blokowi jego domyślną templatę, np. dla zakładki z opisem produktu:

Potrzebujemy najpierw odnaleźć jaki blok za to odpowiada. Możemy w tym celu włączyć template path hints. Dla strony produktu odpowiadający plik layoutu ma nazwę catalog_product_view.xml, sama definicja bloku, który nas interesuje, znajduje się tutaj. Stwórzmy teraz plugin dla klasy Magento\Catalog\Block\Product\View\Description.
Jako że przedstawiona tu modyfikacja dotyczy obszaru frontend, tworzę plik w etc/frontend/di.xml:
|
1 2 3 4 5 6 7 8 9 10 11 |
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Catalog\Block\Product\View\Description"> <plugin name="replace_content_details" sortOrder="10" type="Anna\Plugins\Plugin\Magento\Catalog\Block\Product\View\DescriptionPlugin" disabled="false" /> </type> </config> |
Bloki, zwracają wygenerowany kod html po wywołaniu metody toHtml(). Więc przed wykonaniem tego kodu możemy podmienić domyślną template, Magento_Catalog::product/view/attribute.phtml na naszą wersję Anna_Plugin::product/view/hello_world.phtml:
|
1 2 |
<!-- file: view/frontend/templates/product/view/hello_world.phtml --> <h1>Hello World!</h1> |
Czas dodać klasę pluginu.
Klasa Anna\Plugins\Plugin\Magento\Catalog\Block\Product\View\DescriptionPlugin:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php declare(strict_types=1); namespace Anna\Plugins\Plugin\Magento\Catalog\Block\Product\View; use Magento\Catalog\Block\Product\View\Description; class DescriptionPlugin { public function beforeToHtml(Description $subject) { $nameInLayout = $subject->getNameInLayout(); if ($nameInLayout === 'product.info.description' && $subject->getProduct()->hasDescription()) { $subject->setTemplate('Anna_Plugins::product/view/hello_world.phtml'); } return null; } } |
Zgodnie ze wspomnianą wcześniej zasadą, stworzyliśmy metodę z przedrostkiem before. Argument $subject to oryginalna klasa, dla której stworzyliśmy plugin. Jako że podana klasa bloku jest używana w różnych miejscach, sprawdzamy nadaną nazwę klasy bloku, wywołując jej metodę getNameInLayout(). Ponieważ zakładka z opisem jest wyświetlana tylko wtedy, gdy produkt ma zdefiniowaną cechę opis (atrybut description) to tylko w takim przypadku dokonujemy podmiany.
Inne przykłady
Poniżej lista wpisów z innymi przykładami dla pluginu typu before:
- Przykład użycia pluginu zamiast preference — zastosowanie pluginu typu before na interfejsie Magento\Framework\Mail\Template\SenderResolverInterface
- Dynamiczne ustawienie view modelu za pomocą pluginu before — przykład dla formularza kontaktu pochodzącego z modułu Magento_Contact dla klasy Magento\Contact\Block\ContactForm.
- Dodanie przycisku akcyjnego na stronie admina za pomocą pluginu before — przykład na gridzie modułu Magento_Newsletter i interfejsie Magento\Backend\Block\Widget\Button\ToolbarInterface.
Plugin after
Pluginy typu after pozwalają na modyfikację zwracanego wyniku przez oryginalną metodę albo pozwalają na wykonanie kodu po jej wykonaniu. Jako że od Magento 2.2 plugin after może przyjmować argumenty oryginalnej metody, zaleca się jego stosowanie, aby uniknąć stosowania pluginu around.
Definiując metodę typu after potrzebujemy za pierwszy argument podać obiekt klasy, dla której tworzony jest plugin. Drugi argument to wartość, wynik, zwrócona przez oryginalną metodę. Jeśli potrzebujemy, możemy również zdefiniować argumenty odpowiadające obserwowanej metodzie.
Przykład dodanie opcji do selecta
W wielu miejscach w adminie, mamy skonfigurowane elementy select z pewnym opcjami. Jeśli pragniemy dodać własną opcję, możemy to zrobić za pomocą pluginu.
Weźmy dla przykładu selecta z opcjami Yes/No. Klasa Magento\Config\Model\Config\Source\Yesno, która dostarcza listę opcji jest używana w wielu miejscach w adminie takich jak komponenty ui (listingi grida, formularzy), konfiguracja w Stores ⮞ Configuration, ustawienia dla widget’u itd . Klasa ta implementuje interfejs Magento\Framework\Option\ArrayInterface (oznaczony jako deprecated), która rozszerza interfejs Magento\Framework\Data\OptionSourceInterface (zalecany do implementowania). Interfejs wymaga zaimplementowania jednej metody, toOptionArray():
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Data; /** * Source of option values in a form of value-label pairs * * @api * @since 100.0.2 */ interface OptionSourceInterface { /** * Return array of options as value-label pairs * * @return array Format: array(array('value' => '<value>', 'label' => '<label>'), ...) */ public function toOptionArray(); } |
Dodajmy do wspomnianej klasy dodatkową opcję.
Plik etc/adminhtml/di.xml:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Config\Model\Config\Source\Yesno"> <plugin name="yesno_additional_options" sortOrder="20" type="Anna\Plugins\Plugin\Magento\Config\Model\Source\YesnoPlugin" disabled="false" /> </type> </config> |
Sama klasa pluginu z metodą afterToOptionArray():
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php declare(strict_types=1); namespace Anna\Plugins\Plugin\Magento\Config\Model\Source; use Magento\Config\Model\Config\Source\Yesno; class YesnoPlugin { public function afterToOptionArray(Yesno $subject, array $result): array { $result[] = [ 'value' => -1, 'label' => __('No idea') ]; return $result; } } |
Przykładowy select, na którym możemy sprawdzić wynik (Stores ⮞ Configuration):

Inne przykłady
Lista wpisów dla pluginu after:
Plugin around
Pluginy typu around działają na takiej zasadzie, że ich kod jest uruchamiany przed jak i po obserwowanej metodzie. Pozwala na kompletne nadpisywanie metody. Dzięki temu masz możliwość kompletnego nadpisania oryginalnej metody.
Nazwa metody składa się oczywiście przedrostka around i nazwy oryginalnej metody. Pierwszym argumentem jest klasa oryginalnej metody, następnie argument typu callable, za pomocą którego możemy wywołać oryginalną metodę. Kolejne argumenty muszą odpowiadać typom z oryginalnej metody.
Należy pamiętać, że jeśli w metodzie pluginu typu around nie wywołamy argumentu callable, sama oryginalna metoda nie zostanie wykonana, jak również przerwie to wykonaniu następnych w kolejności pluginów.
Stosując plugin typu around, należy brać pod uwagę alternatywę, która może być na przykład zastosowanie pluginu before/after do modyfikowania parametrów. Wykorzystanie pluginu around wiąże się ze zwiększeniem stosu wywołania, co może negatywnie wpłynąć na wydajność. Zatem dobrą praktyką jest stosowanie tego rodzaju pluginu, kiedy jesteśmy zainteresowani kompletnym podmienieniem funkcjonalności.
Przykład z generowanie url dla nowego produktu
Dany produkt zawsze jest dostępny pod adresem w stylu https://<shop_url>/catalog/product/view/id/1. Bardzo często jednak tworzymy linki bardziej przyjazne SEO. Jest to realizowane poprzez nadpisywanie linków, które przechowywane są w tabeli url_rewrite.
Dla przykładu, jeśli mamy zainstalowane sample data, to możesz dostać się do produktu Push It Messenger Bag pod tymi adresami:
- https://<shop_url>/push-it-messenger-bag.html
- https://<shop_url>/catalog/product/view/id/14
Formularz dodawania nowego produktu w Magento 2, posiada zakładkę Search Engine Optimization, gdzie sami możemy ustawić adres do strony produktu edytując pole URL Key.

Kiedy tworzymy nowy produkt i sami nie definiujemy url do jego strony, Magento za pomocą klasy Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator tworzy nam taki url automatycznie. Poniżej fragment logiki, która za to odpowiada:
|
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 44 45 46 47 48 49 50 51 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ declare(strict_types=1); namespace Magento\CatalogUrlRewrite\Model; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** * Model product url path generator */ class ProductUrlPathGenerator { /** (...) */ /** * Generate product url key based on url_key entered by merchant or product name * * @param Product $product * @return string|null */ public function getUrlKey($product) { $generatedProductUrlKey = $this->prepareProductUrlKey($product); return ($product->getUrlKey() === false || empty($generatedProductUrlKey)) ? null : $generatedProductUrlKey; } /** * Prepare url key for product * * @param Product $product * @return string */ protected function prepareProductUrlKey(Product $product) { $urlKey = (string)$product->getUrlKey(); $urlKey = trim(strtolower($urlKey)); return $product->formatUrlKey($urlKey ?: $product->getName()); } /** (...) */ } |
Możemy zmienić kompletnie logikę dla metody getUrlKey(). Sprawmy, aby na końcu generowanego adresu url dla nowego produktu był zawsze suffix „-test”.
Na początek tworzymy plik etc/di.xml:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator"> <plugin name="catalog_product_url_path_generator_rewrite" sortOrder="10" type="Anna\Plugins\Plugin\Magento\CatalogUrlRewrite\Model\ProductUrlPathGeneratorPlugin" disabled="false" /> </type> </config> |
Klasa Anna\Plugins\Plugin\Magento\CatalogUrlRewrite\Model\ProductUrlPathGeneratorPlugin:
|
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 |
<?php declare(strict_types=1); namespace Anna\Plugins\Plugin\Magento\CatalogUrlRewrite\Model; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; class ProductUrlPathGeneratorPlugin { public function aroundGetUrlKey(ProductUrlPathGenerator $subject, callable $proceed, Product $product): string { $urlKey = $product->formatUrlKey(trim((string)$product->getUrlKey())); return $urlKey ?: $this->prepareProductUrlKey($product); } protected function prepareProductUrlKey(Product $product): string { $addSuffix = "test"; $urlKey = $product->formatUrlKey(rtrim('-' . $product->getName()) . '-' . $addSuffix); return strtolower($urlKey); } } |
Zostaje zweryfikować czy kod działa. Kolejnym krokiem jest stworzenie nowego produktu i sprawdzenie, jaki url zostanie dla niego wygenerowany.
W przykładzie zrezygnowaliśmy z wywołania argumentu callable $proceed, ale jak najbardziej możemy rozbudować logikę, aby dla wybranego warunku to oryginalna metoda została wywołana. W tym przypadku, aby wywołać oryginalną metodę, potrzebujemy w kodzie dodać poniższą linijkę:
|
1 |
return $proceed($product); |
Pamiętaj, aby do jej wywołania podać potrzebne argumenty.
Inne przykłady
Poniżej lista wpisów z przykładami dla pluginu around:
- Przykład użycia pluginu zamiast preference. Omówiony przykad dotyczy modułu Dotdigitalgroup_Email i modyfikacji metody interfejsu Magento\Framework\Mail\Template\SenderResolverInterface. Zostało tam przedstawione alternatywne rozwiązanie z wykorzystaniem pluginu around, jak i samej alternatywy dla niego, pluginu before.
Magento 2: Proxy - Web Programming
2 kwietnia 2024 @ 14:14
[…] Dla argumentu $customerSession konstruktora klasy SomeClass ładujemy klasę MagentoCustomerModelSessionProxy w miejsce normalnego modelu sesji. Dlaczego w ten sposób? Podanie jej bezpośrednio do konstruktora zlimitowałoby możliwości do rozszerzenia klasy. Dla klas proxy nie możemy tworzyć pluginów. […]