083 —Magento
Een Magento 2 di.xml lezen: de vier nodes die tellen
Een class in je Magento 2-shop gedraagt zich anders dan de vendor-broncode. Het antwoord zit in di.xml: vier nodes doen het echte werk, de rest is ruis.
Het is 16:20 op een woensdag. Je staart naar vendor/magento/module-catalog/Model/Product.php omdat een getPrice()-call in een custom rapport een getal teruggeeft dat niet klopt met de database. De class ziet er onschuldig uit. Het gedrag niet.
De class die je aanroept is niet de class die je leest. Ergens in de codebase heeft een di.xml-bestand hem omgeleid. De dependency injection-laag van Magento 2 doet veel en is bijna volledig declaratief, wat betekent dat het antwoord op "waarom doet dit ding dat" verstopt zit in XML, niet in PHP.
Als je ooit een Magento 2 di.xml hebt opengeklapt en je middag voelde verdampen, dan is dit je leidraad. Vier nodes verklaren negentig procent van elke override die je tegenkomt. De rest is bijzaak.
De vier nodes die het werk doen
Een Magento 2 di.xml staat op twee plekken per module: etc/di.xml (globaal) en etc/<area>/di.xml waarbij area gelijk is aan frontend, adminhtml, webapi_rest, webapi_soap, graphql of crontab. Het area-specifieke bestand ligt over het globale heen. Het framework leest ze in module-load-volgorde, mergt het geheel, cachet het resultaat onder generated/metadata/ en serveert tijdens runtime één opgeloste graaf.
Binnen dat bestand zijn dit de vier nodes die je echt moet lezen:
<preference><type><virtualType><plugin>(altijd genest in een<type>)
De rest is namespacing, sensitivity hints of testfixtures. Leer deze vier en het bestand wordt leesbaar.
preference: vervang de implementatie
Een <preference> zegt: wanneer iemand interface A opvraagt, geef hem dan concrete class B.
<preference for="Magento\Catalog\Api\ProductRepositoryInterface"
type="Acme\Catalog\Model\ProductRepository" />Nadat de object manager van Magento dit heeft opgelost, krijgt elke constructor die type-hint op ProductRepositoryInterface voortaan Acme\Catalog\Model\ProductRepository in plaats van de default. Het is een platte, totale vervanging. Er is geen ervoor en geen erna. De oude class is uit de graaf verdwenen.
Twee dingen om te onthouden. Eerst: <preference> werkt ook op classes, niet alleen op interfaces, en zo kapen sommige third-party modules stilletjes core-implementaties zonder dat je het doorhebt. Daarnaast wint de laatst geladen preference. De module-load-volgorde wordt bepaald door app/etc/config.php en het sequence-element in de module.xml van elke module. Als twee modules dezelfde interface allebei prefereren, krijg je degene die later geladen is. De codebase doorzoeken op preference for="Some\Class" is de enige betrouwbare manier om te achterhalen wie het heeft overgenomen.
type: configureer de constructor
Een <type>-node past aan hoe een specifieke class gebouwd wordt. Het meest gangbare gebruik is het injecteren van geconfigureerde argumenten in zijn constructor.
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
<arguments>
<argument name="collections" xsi:type="array">
<item name="acme_invoices_listing_data_source"
xsi:type="string">Acme\Invoice\Model\ResourceModel\Invoice\Grid\Collection</item>
</argument>
</arguments>
</type>Zo worden grid collections, validators, command lists en event observer pools uitgebreid. De class zelf wordt niet vervangen. Aan zijn constructor-argument collections wordt simpelweg één extra entry toegevoegd.
Het <arguments>-blok ondersteunt xsi:type-waarden voor string, boolean, number, null, array, object en init_parameter. Het object-type is het interessante: dat zegt tegen de object manager dat hij een andere class moet oplossen en die instance moet doorgeven. Dat is de naad waar één module zijn eigen service aan een core-class kan doorgeven zonder het core-bestand zelf aan te raken. De volledige lijst staat in de Adobe Commerce docs over dependency injection, een bladwijzer waard.
<type> is ook waar <plugin>-declaraties wonen, en daar wordt het interessant.
virtualType: een kloon met andere argumenten
Een <virtualType> definieert een nieuwe typenaam, gedragen door een bestaande concrete class maar geconfigureerd met eigen argumenten. Er wordt geen nieuwe PHP geschreven. De naam bestaat alleen binnen de DI-container.
<virtualType name="Acme\Invoice\Model\ResourceModel\Invoice\Grid\Collection"
type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
<arguments>
<argument name="mainTable" xsi:type="string">acme_invoice</argument>
<argument name="resourceModel" xsi:type="string">Acme\Invoice\Model\ResourceModel\Invoice</argument>
</arguments>
</virtualType>Je hebt nu een class-naam (Acme\Invoice\Model\ResourceModel\Invoice\Grid\Collection) die wijst naar Magento's SearchResult-class met een andere set constructor-argumenten. Refereer ernaar op elke plek waar een class-FQN verwacht wordt. Hij verschijnt niet in de class-navigator van je IDE, want er is geen bestand. Zoek met string-match als je gaat speuren.
Dit is de node die de meeste mensen in de war brengt. Als je grept op een class en nul bestanden vindt maar wel een berg verwijzingen, zoek dan een virtualType met die naam.
plugin: onderschep de methode
Een <plugin> is het interceptor-mechanisme. Hij hangt before-, after- of around-methodes aan een publieke methode van een target-type. Plugins vervangen het target niet. Ze wikkelen het in.
<type name="Magento\Catalog\Model\Product">
<plugin name="acme_product_price_audit"
type="Acme\PriceAudit\Plugin\Product\PriceLoggerPlugin"
sortOrder="10"
disabled="false" />
</type>De plugin-class declareert methodes die vernoemd zijn naar de publieke methodes van het target, met before, after of around ervoor:
public function afterGetPrice(
\Magento\Catalog\Model\Product $subject,
$result
) {
$this->logger->info('product_price.read', [
'sku' => $subject->getSku(),
'price' => $result,
]);
return $result;
}Plugins zijn het juiste antwoord wanneer je gedrag wilt observeren of bijsturen zonder de class voor jezelf op te eisen. Ze zijn ook het verkeerde antwoord wanneer meer dan drie modules dezelfde methode hebben gepluggd, omdat sortOrder dan een stil slagveld wordt. De officiële richtlijn staat in de Adobe Commerce docs over plugin-interceptie, en een zorgvuldige lezing is geen overbodige luxe voordat je je vierde schrijft.
around-plugins zijn tijdens runtime het duurst en het gevaarlijkst als ze geketend zijn. Kies standaard before of after, tenzij je een reden hebt. Die reden is zelden "ik wil de call overslaan". Die reden is meestal "ik wil de argumenten vertalen".
Leesvolgorde wanneer je voor een echte di.xml zit
Wanneer je een onbekende Magento 2 di.xml opent, scan in deze volgorde:
- Elke
<preference>. Dit zijn totale vervangingen. Noteer welke interfaces zijn overgenomen. - Elke
<virtualType>. Dit zijn onzichtbare classes. Voeg ze toe aan je mentale class-lijst. - Elke
<type>met<arguments>. Dit zijn configuratie-injecties, bijna altijd grids, validators of command pools. - Elke
<plugin>binnen die<type>-nodes. Dit zijn de runtime-interceptors. NoteersortOrderendisabled.
Draai daarna bin/magento setup:di:compile tegen een schone kopie om te zien hoe de gegenereerde factories en interceptors eruitzien onder generated/code/. De gecompileerde output is wijdlopig, maar vertelt je de waarheid over welke plugins daadwerkelijk welke methodes hebben omhuld, en in welke volgorde. De XML lezen vertelt je de intentie. De gegenereerde code lezen vertelt je de werkelijkheid.
Na vanmiddag
Toen we Pier bouwden voor het soort legacy site waar één Magento 2-shop elf modules heeft die allemaal dezelfde vijf classes overriden, liepen we precies hiertegen aan: de XML staat er, maar hem lezen in een normale editor betekent tussen vijftien bestanden bladeren om één override te volgen. Wat we uiteindelijk hebben gedaan, is een chat-interface die de hele graaf voor je oplost, met volledige version history op elke wijziging zodat je elke override kunt terugdraaien zodra het misgaat.
Het kleinste wat je vandaag kunt doen: open één di.xml die je hebt vermeden, grep je repo op elke <preference for="-regel en zet de lijst van overgenomen interfaces in een comment bovenaan het bestand. De volgende persoon op de oproep, inclusief jij over drie weken, krijgt zijn middag terug.
— Vragen —
Wat is het verschil tussen preference en plugin in Magento 2?
Een preference vervangt de class volledig voor iedereen die de interface opvraagt. Een plugin wikkelt één publieke methode op het target in, zonder de oorspronkelijke implementatie uit de graaf te halen.
Kan ik een virtualType buiten di.xml gebruiken?
Ja, via string-referentie. De virtualType-naam bestaat alleen binnen de DI-container, maar je kunt die naam doorgeven op elke plek waar een class-FQN verwacht wordt, inclusief andere XML-bestanden en factory-calls.
Waarom stopt mijn plugin met vuren na een Magento-upgrade?
Waarschijnlijk heeft de vendor de target-methode op private gezet of de class final gemarkeerd. Plugins haken alleen aan publieke, niet-final method-signaturen, dus de override valt geruisloos terug zonder error.