Magento 2: preference
Preference w Magento 2 jest używany przez Object Manager, aby rozszerzyć domyślną implementację. Możesz użyć preference do określenia implementacji wybranych interfejsów bądź też nadpisać istniejące klasy i ich metody. Jako, że jest to związane z określaniem zależności, konfiguracja taka definiowana jest w pliku di.xml.
Przyjrzyjmy się konkretnym przykładom zastosowania, problemom i omówimy dlaczego warto stosować alternatywne rozwiązania, gdy to tylko możliwe.
Określanie implementacji interfejsu
Dla przykładu z modułu Anna_Guestbook mamy klasę repozytorium. Jeśli chcemy, aby Object Manager dla interfejsu Anna\Guestbook\Api\GuestbookRepositoryInterfece zainicjalizował nam klasę Anna\Guestbook\Model\GuestbookRepository potrzebujemy wykonać następujące kroki:
- Utworzyć plik etc/di.xml w katalogu modułu,
- Określić za pomocą <preference> jaką klasę chcemy implementować pod wybranym interfejsem, gdzie dla:
- for — określamy interfejs,
- type — klasa, która ma implementować wybrany interfejs.
Konfiguracja dla wspomnianego przykładu wygląda następująco:
1 2 3 4 5 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Anna\Guestbook\Api\GuestbookRepositoryInterface" type="Anna\Guestbook\Model\GuestbookRepository" /> </config> |
Nadpisywanie klas za pomocą preference
Jeśli potrzebujemy nadpisać klasę w Magento 2, a z innych sposobów nie możemy skorzystać, to zawsze zostaje nam opcja z <preference>. Działa podobnie jak do mechanizmu nadpisywania klas w Magento 1. Wiązać się z tym, że mogą występować konflikty, ponieważ tylko jeden moduł w tym samym czasie może nadpisywać wybraną klasę za pomocą preference! Wygrywa moduł, który zostaje załadowany jako ostatni w kolejności i to jego nadpisanie Magento weźmie pod uwagę.
- W katalogu etc modułu tworzymy plik di.xml.
- Za pomocą <preference> ustawiamy argumenty:
- for — klasę/interfejs, dla której chcemy wskazać nową klasę,
- type — klasa, która nadpisuje kod.
- Dodać w etc/module.xml zależność.
- Dodać twardą zależność w composer.json do naszego modułu.
- Klasa, która nadpisuje działanie 🙂
Przykład preference z modułu Dotdigitalgroup_Email
Weźmy dla przykładu moduł Dotdigital for Magento 2, na stronie repozytorium możemy sprawdzić kod, gdzie widzimy zdefiniowany preference dla interfejsu Magento\Framework\Mail\Template\SenderResolverInterface.
1 |
<preference for="Magento\Framework\Mail\Template\SenderResolverInterface" type="Dotdigitalgroup\Email\Model\Email\DotdigitalSenderResolver" /> |
Jeśli sprawdzimy w vendor/magento/module-email/etc/di.xml, to domyślnie implementacja powyższego interfejsu odpowiada klasa Magento\Email\Model\Template\SenderResolver pochodząca z moduł Magento_Email. Musimy brać tu pod uwagę kolejność, z jaką moduły są ładowane, tutaj istotne jest również określenie zależności twardej. Poniżej plik module.xml:
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Dotdigitalgroup_Email" setup_version="4.12.0"> <sequence> <!-- (...) --> <module name="Magento_Email" /> <!-- (...) --> </sequence> </module> </config> |
Plik composer.json:
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 |
{ "name": "dotmailer/dotmailer-magento2-extension", "description": "dotmailer integration for magento 2", "type": "magento2-module", "license": "MIT", "version": "4.12.0", "require": { "php": "^7.1", "magento/framework": ">=101 <104", <!-- (...) --> "magento/module-email": ">=100 <102", <!-- (...) --> }, "suggest": { "dotmailer/dotmailer-magento2-extension-chat": "1.5.*" }, "autoload": { "files": [ "registration.php" ], "psr-4": { "Dotdigitalgroup\\Email\\": "" } }, "repositories": [ { "type": "composer", "url": "https://repo.magento.com/" } ] } |
Sama klasa nadpisująca oryginalną:
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 |
<?php namespace Dotdigitalgroup\Email\Model\Email; use Dotdigitalgroup\Email\Helper\Transactional; use Magento\Email\Model\Template\SenderResolver; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Registry; use Dotdigitalgroup\Email\Logger\Logger; /** * Class SenderResolver * * Set the message from name and email in transactional sends, using data set in email_template. */ class DotdigitalSenderResolver extends SenderResolver { /** (...) */ /** * * @param string|array $sender * @param int|null $scopeId * * @return array * @throws \Magento\Framework\Exception\MailException */ public function resolve($sender, $scopeId = null) { $templateId = $this->templateService->getTemplateId(); if ($templateId && $this->shouldIntercept()) { $template = $this->templateFactory->create() ->loadTemplate($templateId); if ($this->isDotmailerTemplateCode($template->getTemplateCode())) { return [ 'email' => $template->getTemplateSenderEmail(), 'name' => $template->getTemplateSenderName() ]; } } return parent::resolve($sender, $scopeId); } /** (...) */ } |
Pełny kod klasy znajdziesz tutaj.
Przykład preference z modułu Dotdigitalgroup_Email alternatywne rozwiązanie z użyciem pluginu
Klasa Dotdigitalgroup\Email\Model\Email\DotdigitalSenderResolver nadpisuje publiczną metodę resolve(). Nic nie stoi na przeszkodzie, aby zastąpić preference poprzez użycie pluginu. Zdefinujmy plugin dla interfejsu Magento\Framework\Mail\Template\SenderResolverInterface.
Jaki by plugin nie zastosować, najpierw stwórzmy w etc/di.xml z odpowiednią konfigurację.
Po Pierwsze, ponieważ nie chcemy korzystać z preference oferowanego przez moduł, sprawmy, by to nasz moduł był głównym nadpisującym. Po pierwsze potrzebujemy określić kolejność ładowania modułu w pliku etc/module.xml:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Anna_Email"> <sequence> <module name="Magento_Email"/> <module name="Dotdigitalgroup_Email"/> </sequence> </module> </config> |
Następnie w etc/di.xml dodajmy dodatkowo preference dajmy na domyślną klasę:
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Framework\Mail\Template\SenderResolverInterface" type="Magento\Email\Model\Template\SenderResolver" /> <type name="Magento\Framework\Mail\Template\SenderResolverInterface"> <plugin name="sender-resolver-new-logic" type="Anna\Email\Plugin\EmailSenderResolverPlugin"/> </type> </config> |
Plugin around
Klasa Dotdigitalgroup\Email\Model\Email\DotdigitalSenderResolver tylko w jednym przypadku modyfikujemy zachowanie metody — jeśli mamy do czynienia z szablonem emaila od dotmailera. Moglibyśmy zastanowić się nad pluginem typu around dla interfejsu Magento\Framework\Mail\Template\SenderResolverInterface. Implementacja mogłaby 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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
<?php declare(strict_types=1); namespace Anna\Email\Plugin; use Dotdigitalgroup\Email\Model\Email\TemplateFactory; use Dotdigitalgroup\Email\Model\Email\TemplateService; use Dotdigitalgroup\Email\Helper\Transactional; use Dotdigitalgroup\Email\Logger\Logger; use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Framework\Registry; class EmailSenderResolverPlugin { /** * @var Registry */ private $registry; /** * @var TemplateFactory */ private $templateFactory; /** * @var Transactional */ private $transactionalHelper; /** * @var TemplateService */ private $templateService; /** * @var Logger */ private $logger; /** * @param Registry $registry * @param TemplateFactory $templateFactory * @param Transactional $transactionalHelper * @param TemplateService $templateService * @param Logger $logger */ public function __construct( Registry $registry, TemplateFactory $templateFactory, Transactional $transactionalHelper, TemplateService $templateService, Logger $logger ) { $this->registry = $registry; $this->templateFactory = $templateFactory; $this->transactionalHelper = $transactionalHelper; $this->templateService = $templateService; $this->logger = $logger; } /** * @param SenderResolverInterface $subject * @param \Closure $proceed * @param string|array $sender * @param null $scopeId * @return array|mixed */ public function aroundResolve(SenderResolverInterface $subject, callable $proceed, $sender, $scopeId = null) { $templateId = $this->templateService->getTemplateId(); if ($templateId && $this->shouldIntercept()) { $template = $this->templateFactory->create() ->loadTemplate($templateId); if ($this->isDotmailerTemplateCode($template->getTemplateCode())) { return [ 'email' => $template->getTemplateSenderEmail(), 'name' => $template->getTemplateSenderName() ]; } } return $proceed($sender, $scopeId); } /** * * @return bool */ private function shouldIntercept() { try { $storeId = $this->registry->registry('transportBuilderPluginStoreId'); return $this->transactionalHelper->isEnabled($storeId); } catch (\Exception $exception) { $this->logger->error((string) $exception); return false; } } /** * * @param string $templateCode * * @return bool */ private function isDotmailerTemplateCode($templateCode) { return $this->transactionalHelper->isDotmailerTemplate($templateCode); } } |
Implementacja wygląda bardzo podobnie, główną różnicą jest wykorzystanie argumentu typu callable, który pozwoli na wykonanie oryginalnej metody i innych pluginów.
Zastosowanie pluginu typu around także ma swoje minusy. Wykorzystanie pluginu around wiązę się ze zwiększeniem stosu wywołania, co może negatywnie wpłynąć na wydajność. Dobrą praktyką jest stosowanie tego rodzaju plugin, kiedy interesuje nam kompletne podmienienie funkcjonalności. Czy możemy dla rozwiązania tego problemuc zastosować na przykład plugin typu before? Sprawdźmy!
Plugin before
Przyjrzyjmy się teraz metodzie resolve() klasy Magento\Email\Model\Template\SenderResolver, która oryginalnie implementowała interfejs Magento\Email\Model\Template\SenderResolver.
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 52 53 |
<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Email\Model\Template; class SenderResolver implements \Magento\Framework\Mail\Template\SenderResolverInterface { /** * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $_scopeConfig; /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig) { $this->_scopeConfig = $scopeConfig; } /** * {@inheritdoc} */ public function resolve($sender, $scopeId = null) { $result = []; if (!is_array($sender)) { $result['name'] = $this->_scopeConfig->getValue( 'trans_email/ident_' . $sender . '/name', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $scopeId ); $result['email'] = $this->_scopeConfig->getValue( 'trans_email/ident_' . $sender . '/email', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $scopeId ); } else { $result = $sender; } if (!isset($result['name']) || !isset($result['email'])) { throw new \Magento\Framework\Exception\MailException(__('Invalid sender data')); } return $result; } } |
Jak spojrzymy na kod, to widzimy, że jak argument $sender jest tablicą, wynik jest po prostu przypisany do zmiennej $result, po czym mamy sprawdzenie, czy podana tablica ma odpowiednie dane. W związku z tym wystarczy, że wcześniej ustawimy tablicę z danymi dla argumentu $sender. Pomocny będzie więc plugin typu before dla interfejsu Magento\Framework\Mail\Template\SenderResolverInterface.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
<?php declare(strict_types=1); namespace Anna\Email\Plugin; use Dotdigitalgroup\Email\Model\Email\TemplateFactory; use Dotdigitalgroup\Email\Model\Email\TemplateService; use Dotdigitalgroup\Email\Helper\Transactional; use Dotdigitalgroup\Email\Logger\Logger; use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Framework\Registry; class EmailSenderResolverPlugin { /** * @var Registry */ private $registry; /** * @var TemplateFactory */ private $templateFactory; /** * @var Transactional */ private $transactionalHelper; /** * @var TemplateService */ private $templateService; /** * @var Logger */ private $logger; /** * @param Registry $registry * @param TemplateFactory $templateFactory * @param Transactional $transactionalHelper * @param TemplateService $templateService * @param Logger $logger */ public function __construct( Registry $registry, TemplateFactory $templateFactory, Transactional $transactionalHelper, TemplateService $templateService, Logger $logger ) { $this->registry = $registry; $this->templateFactory = $templateFactory; $this->transactionalHelper = $transactionalHelper; $this->templateService = $templateService; $this->logger = $logger; } /** * @param SenderResolverInterface $subject * @param string|array $sender * @param null $scopeId * @return array */ public function beforeResolve(SenderResolverInterface $subject, $sender, $scopeId = null): array { $templateId = $this->templateService->getTemplateId(); if ($templateId && $this->shouldIntercept()) { $template = $this->templateFactory->create() ->loadTemplate($templateId); if ($this->isDotmailerTemplateCode($template->getTemplateCode())) { $sender = [ 'email' => $template->getTemplateSenderEmail(), 'name' => $template->getTemplateSenderName() ]; } } return [$sender, $scopeId]; } /** * * @return bool */ private function shouldIntercept() { try { $storeId = $this->registry->registry('transportBuilderPluginStoreId'); return $this->transactionalHelper->isEnabled($storeId); } catch (\Exception $exception) { $this->logger->error((string) $exception); return false; } } /** * * @param string $templateCode * * @return bool */ private function isDotmailerTemplateCode($templateCode) { return $this->transactionalHelper->isDotmailerTemplate($templateCode); } } |
Moduł do wykrywania konfliktów
Jak już wspomniano, stosowanie preference może wiązać się z występowanie konfliktów. Tylko jeden moduł w danym czasie może nadpisywać wybraną klasę, dlatego trzeba brać pod uwagę również kolejność ładowania poszczególnych modułów.
Moduł Magefan_ConflictDetector, który pozwoli na wykrycie potencjalnych konfliktów jest dostępny na githabie jak i stronie producenta modułu. Znacznie ułatwia weryfikację potencjalnych problemów.
Pluginy w Magento 2 - Web Programming
4 grudnia 2023 @ 02:59
[…] W dzisiejszym wpisie omówię jak działają pluginy w Magento 2. Pluginy (wprowadzone od Magento 2), zwane też inaczej „przechwytaczami” (ang. interceptors), stanowią mechanizm nadpisywania klas zaraz obok obsługi zdarzeń czy bezpośredniego nadpisywania klasy za pomocą konfiguracji preference. […]