<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nikita Sirenko</title>
    <description>The latest articles on DEV Community by Nikita Sirenko (@qweik).</description>
    <link>https://dev.to/qweik</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4239692%2F9b3f0c29-e0a9-4372-9545-822afb40dae3.png</url>
      <title>DEV Community: Nikita Sirenko</title>
      <link>https://dev.to/qweik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qweik"/>
    <language>en</language>
    <item>
      <title>After save isn't after commit: the event trap Magento 2 and Laravel share</title>
      <dc:creator>Nikita Sirenko</dc:creator>
      <pubDate>Thu, 12 Nov 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/qweik/after-save-isnt-after-commit-the-event-trap-magento-2-and-laravel-share-i7p</link>
      <guid>https://dev.to/qweik/after-save-isnt-after-commit-the-event-trap-magento-2-and-laravel-share-i7p</guid>
      <description>&lt;p&gt;Magento's &lt;code&gt;catalog_product_save_after&lt;/code&gt; runs before the COMMIT. So does Laravel's &lt;code&gt;saved()&lt;/code&gt;, as soon as the save sits inside &lt;code&gt;DB::transaction()&lt;/code&gt;. Publish to a queue from either, and a fast consumer can read the old row.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one save does in Magento
&lt;/h2&gt;

&lt;p&gt;In Magento 2, &lt;code&gt;$productRepository-&amp;gt;save()&lt;/code&gt; isn't one UPDATE. It opens a transaction, 2 levels deep: a CatalogSearch plugin opens one, the EntityManager another. Then it fires &lt;code&gt;catalog_product_save_before&lt;/code&gt;, writes the product and its attribute values, fires &lt;code&gt;catalog_product_save_after&lt;/code&gt;, and only then commits. Stock data, URL rewrites and the quote refresh hang off &lt;code&gt;catalog_product_save_after&lt;/code&gt;. To add behavior, you declare an observer in &lt;code&gt;events.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Laravel has the same shape without the XML: an Eloquent observer with &lt;code&gt;saving()&lt;/code&gt; and &lt;code&gt;saved()&lt;/code&gt;, attached to the model with &lt;code&gt;#[ObservedBy]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the commit
&lt;/h2&gt;

&lt;p&gt;Publish to RabbitMQ from &lt;code&gt;catalog_product_save_after&lt;/code&gt; and 2 things can go wrong. A fast consumer reads the product before the transaction ends and gets the old row. Or something fails later in the save, the transaction rolls back, and the ERP has already heard about a product that was never saved.&lt;/p&gt;

&lt;p&gt;Magento's answer is a separate event. After the commit, the resource model runs its commit callbacks. One of them is &lt;code&gt;Magento\Framework\Model\AbstractModel::afterCommitCallback()&lt;/code&gt;, and it fires &lt;code&gt;catalog_product_save_commit_after&lt;/code&gt;. &lt;code&gt;AbstractResource::commit()&lt;/code&gt; runs those callbacks only when the transaction level is back to 0. If your save is part of a bigger transaction, the event waits for the outermost commit.&lt;/p&gt;

&lt;p&gt;The core uses that event sparingly. Across all areas, the core has 5 observers on &lt;code&gt;catalog_product_save_before&lt;/code&gt;, 5 on &lt;code&gt;catalog_product_save_after&lt;/code&gt;, and 1 on &lt;code&gt;catalog_product_save_commit_after&lt;/code&gt;: &lt;code&gt;ImageResizeAfterProductSave&lt;/code&gt;, which resizes the new product images.&lt;/p&gt;

&lt;p&gt;The heavier jobs register commit callbacks directly. Search reindex does it in Update on Save mode. Since 2.4.9 catalog price rules do the same: before that, &lt;code&gt;ApplyRules&lt;/code&gt; reindexed the saved product in an &lt;code&gt;afterSave&lt;/code&gt; plugin, inside the transaction. The code comment says why it moved: to avoid DDL transaction issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;config&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
        &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"urn:magento:framework:Event/etc/events.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- not catalog_product_save_after: that one runs before the COMMIT --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;event&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"catalog_product_save_commit_after"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;observer&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"acme_notify_erp"&lt;/span&gt; &lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Acme\Catalog\Observer\NotifyErp"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/event&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotifyErp&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ObserverInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;PublisherInterface&lt;/span&gt; &lt;span class="nv"&gt;$publisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Observer&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$observer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSku&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'acme.product.saved'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The topic itself is declared in &lt;code&gt;communication.xml&lt;/code&gt;, &lt;code&gt;queue_publisher.xml&lt;/code&gt; and &lt;code&gt;queue_topology.xml&lt;/code&gt;. All 3 are in &lt;a href="https://gist.github.com/qweik/f142c3d1ddd08534301c5915e1971ded" rel="noopener noreferrer"&gt;this gist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel: the same problem, one level up
&lt;/h2&gt;

&lt;p&gt;In Laravel, Eloquent's &lt;code&gt;save()&lt;/code&gt; doesn't open a transaction. Only &lt;code&gt;saveOrFail()&lt;/code&gt; does. &lt;code&gt;saved()&lt;/code&gt; fires right after the query, and without a transaction that row is already committed. The problem shows up when you wrap the work in &lt;code&gt;DB::transaction()&lt;/code&gt;, the normal way to save a product together with its prices.&lt;/p&gt;

&lt;p&gt;The fix is an interface. An observer that implements &lt;code&gt;ShouldHandleEventsAfterCommit&lt;/code&gt; gets its &lt;code&gt;saved()&lt;/code&gt; after the outermost commit. &lt;code&gt;Illuminate\Database\DatabaseTransactionsManager&lt;/code&gt; holds the call until then. No open transaction? It runs right away. Rollback? It never runs.&lt;/p&gt;

&lt;p&gt;Before Laravel 12.54 the interface defers every method of the observer, &lt;code&gt;saving()&lt;/code&gt; included, so a change you make to the model in &lt;code&gt;saving()&lt;/code&gt; inside a transaction isn't written. Since 12.54 only the "after" methods wait. Keep an after-commit observer to &lt;code&gt;saved()&lt;/code&gt; and the difference doesn't touch you.&lt;/p&gt;

&lt;p&gt;For a job on its own there's &lt;code&gt;-&amp;gt;afterCommit()&lt;/code&gt;, or &lt;code&gt;'after_commit' =&amp;gt; true&lt;/code&gt; on the connection in &lt;code&gt;config/queue.php&lt;/code&gt;. Laravel ships it as &lt;code&gt;false&lt;/code&gt; on all 4 connections that have the option: Database, Beanstalkd, Redis and SQS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductObserver&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldHandleEventsAfterCommit&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;NotifyErp&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;#[ObservedBy(ProductObserver::class)]&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HasMany&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Price&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;createMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// saved() is called here, after the COMMIT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What waiting costs
&lt;/h2&gt;

&lt;p&gt;After the commit there's nothing left to roll back. If the publish fails there, the product is saved and the message is lost.&lt;/p&gt;

&lt;p&gt;Magento and Laravel handle that failure differently. Magento catches an exception thrown in a commit callback and logs it as CRITICAL in &lt;code&gt;exception.log&lt;/code&gt;, and &lt;code&gt;save()&lt;/code&gt; returns as if nothing happened. Laravel throws it to the caller of &lt;code&gt;DB::transaction()&lt;/code&gt;, after the row is committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the queue is in the same database
&lt;/h2&gt;

&lt;p&gt;Then there's nothing to wait for: the publish is part of the transaction. Magento's MySQL queue inserts into &lt;code&gt;queue_message&lt;/code&gt; through the default connection. Laravel's database queue driver does the same when it uses the app's default connection. A rollback removes the message, and no consumer sees it before the commit. If a message must not be lost, that's the setup that gives you both or neither.&lt;/p&gt;

&lt;p&gt;Switch to RabbitMQ, Redis or SQS and that guarantee is gone. Your observer doesn't change by a line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;If a listener talks to the outside world, a queue, an API or an email, wait for the commit, and decide what happens when the call fails after it. Magento gives you a separate event. Laravel gives you an interface.&lt;/p&gt;

&lt;p&gt;Next: Magento plugins and Laravel decorators, and why around plugins are the ones to watch.&lt;/p&gt;

&lt;p&gt;Have you ever had a consumer read a row that wasn't committed yet?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2 of 15 of the Magento 2 vs Laravel series. Originally published on &lt;a href="https://www.linkedin.com/pulse/after-save-isnt-commit-event-trap-magento-2-laravel-share-sirenko-kmzcf/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>bin/magento and php artisan are one Symfony Console in two costumes</title>
      <dc:creator>Nikita Sirenko</dc:creator>
      <pubDate>Wed, 11 Nov 2026 18:34:39 +0000</pubDate>
      <link>https://dev.to/qweik/binmagento-and-php-artisan-are-one-symfony-console-in-two-costumes-1ad4</link>
      <guid>https://dev.to/qweik/binmagento-and-php-artisan-are-one-symfony-console-in-two-costumes-1ad4</guid>
      <description>&lt;p&gt;I've worked in Magento 2 full-time for 5.5 years, and in Laravel on and off since 2015. Put the two side by side and the names change, but the engineering doesn't. The console is the clearest example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same component underneath
&lt;/h2&gt;

&lt;p&gt;In Magento 2, &lt;code&gt;Magento\Framework\Console\Cli&lt;/code&gt;, the class behind bin/magento, extends Symfony's Console Application. In Laravel, so does &lt;code&gt;Illuminate\Console\Application&lt;/code&gt;, the class behind php artisan. Every command in both frameworks extends Symfony's Command.&lt;/p&gt;

&lt;p&gt;So arguments, options, input, output and return codes work the same way. Only registration differs. Magento collects commands from di.xml. Laravel picks up the classes in app/Console/Commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap they share
&lt;/h2&gt;

&lt;p&gt;When bin/magento starts, it builds every command registered in &lt;code&gt;CommandListInterface&lt;/code&gt;, with all of its constructor dependencies, even if all you asked for was cache:flush. On a fresh Magento 2.4.9 install that's 72 command objects per run. One heavy constructor slows down every bin/magento call, the cron run that fires every minute included.&lt;/p&gt;

&lt;p&gt;The classic fix is a generated Proxy, wired in di.xml. The heavy service is created only when the command actually runs. The core does this itself in the image-resize command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Console\CommandListInterface"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;argument&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"commands"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"acme_export"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Acme\Export\Console\ExportOrders&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- built on every bin/magento run: inject the heavy service lazily --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Acme\Export\Console\ExportOrders"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;argument&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"exporter"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Acme\Export\Model\OrderExporter\Proxy&lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportOrders&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OrderExporterInterface&lt;/span&gt; &lt;span class="nv"&gt;$exporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kc"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'acme:export-orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InputInterface&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OutputInterface&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;writeln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' orders exported'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Cli&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RETURN_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the exporter injected directly, &lt;code&gt;bin/magento cache:status&lt;/code&gt; builds it on every run. With the Proxy it doesn't, and &lt;code&gt;acme:export-orders&lt;/code&gt; still prints "3 orders exported".&lt;/p&gt;

&lt;p&gt;Since 2.4.8 there's a cleaner way. &lt;code&gt;Magento\Framework\Console\CommandLoader&lt;/code&gt; implements Symfony's lazy &lt;code&gt;CommandLoaderInterface&lt;/code&gt;. Register the command there by name, and Magento builds it only when you call it. No Proxy needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 2.4.8+: registered by name, built only when called --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;type&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Magento\Framework\Console\CommandLoader"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;arguments&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;argument&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"commands"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"acme_export"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;acme:export-orders&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"class"&lt;/span&gt; &lt;span class="na"&gt;xsi:type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Acme\Export\Console\ExportOrders&lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/argument&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/arguments&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel has the same split. A command without the &lt;code&gt;#[AsCommand]&lt;/code&gt; attribute is resolved from the container when Artisan starts. With the attribute, it's registered by name and built only when you call it. Or you ask for the service in &lt;code&gt;handle()&lt;/code&gt;, and the container resolves it on run. The &lt;code&gt;make:command&lt;/code&gt; stub has no constructor and injects into &lt;code&gt;handle()&lt;/code&gt;, so Laravel's default already avoids the trap.&lt;/p&gt;

&lt;p&gt;Since Laravel 13, &lt;code&gt;make:command&lt;/code&gt; writes a &lt;code&gt;#[Signature]&lt;/code&gt; attribute instead of a &lt;code&gt;$signature&lt;/code&gt; property. That attribute alone doesn't make the command lazy. Artisan only checks for &lt;code&gt;#[AsCommand]&lt;/code&gt;, so the example has both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;#[Signature('acme:export-orders')]&lt;/span&gt;
&lt;span class="c1"&gt;#[AsCommand(name: 'acme:export-orders')]&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportOrders&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Command&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderExporter&lt;/span&gt; &lt;span class="nv"&gt;$exporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exporter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' orders exported'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Keep command constructors cheap, or make them lazy. It holds in both frameworks because it's the same component.&lt;/p&gt;

&lt;p&gt;This is the first of 15 short comparisons between Magento 2 and Laravel, one idea at a time, with code. Next: why a save event can fire before the commit in both frameworks, and what that does to a queue.&lt;/p&gt;

&lt;p&gt;Which command in your project has the heaviest constructor?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1 of 15 of the Magento 2 vs Laravel series. Originally published on &lt;a href="https://www.linkedin.com/pulse/binmagento-php-artisan-one-symfony-console-two-costumes-sirenko-isrpf/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>magento</category>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
