← Blog

Magento

Magento 2 stack traces: de vijf regels die echt tellen

Het meeste van een Magento 2 stack trace is gegenereerde DI-bedrading. Vijf regels doen het echte werk: wat faalde, waar de oorzaak zit, en welk area het gooide.

Jacob Molkenboer· Oprichter · A Brand New Company· 2 mrt 2024· 6 min
Open leren logboek met vijf groene zijden linten, een messing sleutel op de rug en rode lakzegel op ivoorpapier.

Het is 21:30 op een donderdag. Je hebt om 17:00 een release uitgerold, de smoke tests waren groen, en het catalogus-team is naar huis. Dan komt er een support ticket binnen: "Categorieën opslaan lukt niet meer." Je SSH't in, doet tail var/log/exception.log, en een stack trace van zestig regels vult je terminal. Gegenereerde proxies, interceptors, framework-plumbing. Ergens in die muur zit één call die brak. Die vinden zou dertig seconden moeten kosten, geen dertig minuten.

Magento 2 traces zien er intimiderend uit omdat het framework bijna elke publieke methode wikkelt in een keten van gegenereerde klassen: plugins, interceptors, proxies, factories, lazy loaders. Het goede nieuws: het meeste kun je negeren. Vijf regels doen het werk.

Regel 1: de exception-klasse en het bestand waarin hij werd gegooid

De bovenste regel vertelt je twee dingen: hoe het framework de fout noemt, en de file:line waar iemand expliciet throw aanriep. Lees hem letterlijk. Als er staat "Could not save category", dan faalde er een save. Het pad naar het bestand vertelt je wiens code besloot op te geven.

exception 'Magento\Framework\Exception\LocalizedException' with message 'Could not save category'
in /var/www/html/vendor/magento/module-catalog/Model/CategoryRepository.php:235

Nu weet je het symptoom: core catalog-code, regel 235, een generieke "save failed" aan de oppervlakte. De oorzaak zit verderop. Drie exception-klassen die je steeds weer ziet: LocalizedException (richting gebruiker, vaak opnieuw gegooid over iets anders heen), NoSuchEntityException (een lookup mislukte), CouldNotSaveException (de persistence-laag weigerde de write). Elk is een hint over welke laag je vervolgens moet inspecteren.

Regel 2: het eerste vendor-frame dat geen gegenereerde code is

Direct onder de exception opent de stack. De bovenste paar frames zijn vrijwel altijd gegenereerde wrappers. Alles in /generated/code/, alles dat eindigt op \Interceptor, alles dat eindigt op \Proxy. Sla ze over. Het eerste frame dat direct in vendor/magento/ staat, is de echte methode die liep toen de exception werd gegooid.

#0 /var/www/html/generated/code/.../Interceptor.php(91): Magento\Catalog\Model\CategoryRepository->save()
#1 /var/www/html/vendor/magento/module-catalog/Model/CategoryRepository.php(232): Magento\Catalog\Model\CategoryRepository->validateCategory()

Frame #1 is de throw-site. Door Magento's plugin-systeem zit er bijna altijd een Interceptor tussen jouw call en de echte methode. Dat is geen ruis, maar ook niet de bug. De bug is wat de echte methode probeerde te doen.

Regel 3: het eerste frame uit app/code of een third-party vendor

Blijf naar beneden lezen. Sla vendor/magento/-frames over, sla de gegenereerde proxies over. Stop bij de eerste regel die wijst naar app/code/ of vendor/<niet-magento>/. Dat is je verdachte.

#7 /var/www/html/vendor/acme/module-catalog-sync/Plugin/CategoryRepositoryPlugin.php(58):
    Magento\Catalog\Model\CategoryRepository\Interceptor->save()

In negen van de tien productie-incidenten is dit het frame dat je doorstuurt naar de verantwoordelijke engineer. Een plugin die argumenten heeft gemuteerd. Een observer die midden in een transactie een externe service raakte. Een preference die een core-methode overschreef en een return vergat. De Adobe Commerce-docs leggen uit hoe plugins calls wrappen (zie de plugins reference), en in die wrapping zitten de meeste verrassingsbugs.

Regel 4: het entry point onderaan

Spring nu naar de onderkant van de trace. Het laatste genummerde frame vertelt je welk area van Magento de request afhandelde. Dat is belangrijk, want dezelfde exception betekent iets anders in een andere context.

#42 /var/www/html/pub/index.php(40): Magento\Framework\App\Bootstrap::run()

Een paar patronen die je op het oog moet herkennen:

  • pub/index.php: HTTP-request, storefront of admin. De controller zit eerder in de trace.
  • bin/magento: CLI-commando. Vaak een deploy, import of indexer-reindex.
  • pub/cron.php of een cron-group entry: scheduled job, mogelijk uit een andere process pool.
  • pub/static.php: static-content deploy.
  • Magento\Framework\App\Cron::launch: cron-ingang naar de applicatie. Check var/log/cron.log voor de planning.

"Could not save category" gegooid vanuit cron is een ander probleem dan eentje vanuit de admin. Cron draait als een andere user, vaak met een afgeknepen environment, en verliest sneller races van andere workers.

Regel 5: de previous exception

Onder de hoofd-trace hangt Magento vaak een chained exception aan. PHP ondersteunt exception chaining sinds 5.3 (de Throwable-interface levert getPrevious()), en Magento gebruikt het royaal. Het framework vangt een low-level fout, wikkelt hem in een vriendelijkere klasse, en gooit hem opnieuw. De originele oorzaak staat in het "previous"-blok.

Caused by: PDOException: SQLSTATE[23000]: Integrity constraint violation:
  1062 Duplicate entry '142-3' for key 'CATALOG_CATEGORY_PRODUCT_CATEGORY_ID_PRODUCT_ID'

Dit is de nuttigste regel op een rustige avond. Bovenaan stond "Could not save category", een generieke Magento-zin. De previous exception zegt: je probeerde een duplicate row in te voegen in catalog_category_product. Dat is geen Magento-bug meer. Dat is een data-integriteitsvraag, waarschijnlijk een plugin die een product probeert te herkoppelen dat al gekoppeld is.

Let op

Lees altijd door tot onderaan. Heeft een Magento-exception geen previous, dan ligt de fout bij het framework. Heeft hij er wel een, dan staat het echte verhaal in de SQL- of HTTP-fout eronder.

Een voorbeeld doorgelopen

Neem de trace hierboven en lees hem als één zin. Boven: save van een catalog category faalde. Throw-site: CategoryRepository, regel 235. Verdachte: acme/module-catalog-sync, plugin op save(). Entry point: pub/index.php, dus dit is de admin die een category opslaat. Previous: duplicate row in catalog_category_product.

In dertig seconden heb je een ticket van één regel: "Acme catalog-sync plugin koppelt producten dubbel bij category-save in de admin; integrity constraint violation op catalog_category_product." Een junior engineer kan dat oppakken en lokaal reproduceren. Je hoefde geen veertig frames aan interceptors te lezen.

De dertig-seconden-methode

Train jezelf om in deze volgorde naar vijf regels te kijken: bovenste exception, eerste vendor-frame dat geen gegenereerde code is, eerste frame uit app/code of niet-Magento vendor, entry point onderaan, previous exception. Negeer in de eerste lezing al het andere.

Toen we voor een Nederlandse retailer op Magento 2.4.7 een verouderde catalog-importer herbouwden, was exception.log het dagelijkse diagnose-instrument. Met de vijf-regel-methode triageerde het team veertig exceptions per dag terug naar de drie die er echt toe deden, en het is dezelfde lens die we toepassen op elke Magento 2 legacy migratie die we oppakken.

Open nu de meest recente exception.log op je eigen winkel. Vind de vijf regels. Schrijf het ticket van één regel. Lukt het niet, dan vertelt de regel die je mist welk deel van je Magento-model je eerst moet aanvullen.

Kern

Lees een Magento 2 trace in vijf sprongen: bovenste exception, echte throw-site, eerste niet-Magento frame, entry point, previous exception. Sla de interceptors in de eerste lezing over.

FAQ

Waar schrijft Magento 2 zijn exception log naartoe?

Standaard naar var/log/exception.log onder je Magento-root. system.log en debug.log staan in dezelfde map. Op managed Adobe Commerce Cloud worden logs gestreamd naar de log-service van het platform.

Wat betekent Interceptor in een Magento stack trace?

Een Interceptor is een gegenereerde klasse die Magento aanmaakt om plugins rond je methode te kunnen draaien. Frames met \Interceptor in de naam zijn wrappers, geen bugs. Kijk er doorheen naar de echte methode.

Waarom staat er Could not save terwijl de echte fout een SQL-constraint is?

Magento wikkelt low-level exceptions in user-facing varianten, zodat de admin een nette boodschap ziet in plaats van een SQL-dump. De originele oorzaak staat in de previous exception onder de hoofd-trace.

Kan ik de Interceptor-ruis uitzetten om traces korter te maken?

Nee, en dat wil je ook niet. Interceptors zijn hoe plugins werken. Je kunt ze tijdens het lezen wegfilteren: elk frame onder /generated/code/ of dat eindigt op \Interceptor of \Proxy mag je in de eerste lezing overslaan.

magentophparchitecturetoolingworkflow

Iets bouwen?

Start een project