Code Generation and Compilation

What Magento writes into generated/, when setup:di:compile is mandatory, and why stale generated code survives a recompile.

Syntaxbin/magento setup:di:compile

Some classes your code names have no file. Vendor\Module\Model\RowFactory, Vendor\Module\Model\Heavy\Proxy and the interceptor that makes plugins work are written by Magento, not by you, and they land in the generated/ directory at the project root.

What is in generated/

generated/code/ holds those classes in folders matching their namespaces: factories, proxies, interceptors, generated repositories and extension attribute classes. generated/metadata/ holds the merged dependency injection configuration as one PHP file per area, such as global.php, frontend.php and adminhtml.php. Both directories are build output. Keep them out of version control and rebuild them on deploy.

Developer mode and production mode

In developer mode a missing generated class is written during the request that first needs it. In production mode nothing is generated at runtime, so everything has to exist before traffic arrives. That is the job of bin/magento setup:di:compile. bin/magento deploy:mode:set production runs it as part of the switch unless you pass --skip-compilation.

Stale generated code

Compilation cleans the cache and generated/metadata, but it does not delete generated/code, and the generator skips any class whose file is already on disk. An interceptor or factory written against a constructor you have since changed is therefore kept, and the resulting errors do not mention compilation at all: a class that does not exist, or a type error while creating an object. Clear it by hand:

rm -rf generated/code generated/metadata
bin/magento setup:di:compile

Example

Example · php
<?php
// app/code/Vendor/Module/Model/Report/Builder.php
declare(strict_types=1);

namespace Vendor\Module\Model\Report;

use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

class Builder
{
    public function __construct(
        private readonly RowFactory $rowFactory,
        private readonly TimezoneInterface $timezone
    ) {
    }

    public function build(array $data): Row
    {
        $data['generated_at'] = $this->timezone->date()->format('Y-m-d H:i:s');

        // RowFactory has no file anywhere in app/code. It is generated
        // from Row: on demand in developer mode, and by
        // setup:di:compile for production.
        return $this->rowFactory->create(['data' => $data]);
    }
}

// bin/magento setup:di:compile
//
// Compilation was started.
// Interception cache generation... 6/9 [==================>---------]  66% 51 secs 384.0 MiB
//
// Generated code and dependency injection configuration successfully.

When to use it

  • Prepare a release build so production mode starts without generating anything at runtime.
  • Explain a class-does-not-exist error that only appears after a constructor change.
  • Decide what belongs in .gitignore and what the deploy pipeline must rebuild.
  • Inject a proxy so an expensive collaborator is built only when it is used.

More examples

Asking for a proxy, which is also generated

The \Proxy suffix is a request, not a file: Magento generates a class of that name which stands in for the real one and builds it only when a method is first called. The constructor parameter keeps the real type declaration. Core does the same thing with Magento\Backend\Model\UrlInterface\Proxy in app/code/Magento/Backend/etc/adminhtml/di.xml, which shows that an interface can be proxied too. Apply it with bin/magento cache:clean config, and with setup:di:compile before production.

Example · xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Vendor\Module\Model\Report\Builder">
        <arguments>
            <argument name="heavyIndexer" xsi:type="object">Vendor\Module\Model\Indexer\Heavy\Proxy</argument>
        </arguments>
    </type>
</config>

Switching an environment to production mode

Reach for --skip-compilation when generated code and static content were built on a build machine and copied in; without it the command regenerates both on the box you are standing on. The file list is not fixed: there is one file for global plus one for every registered area, and a module can register another by adding an item to the areas argument of Magento\Framework\App\AreaList.

Example · shell
# Compilation is part of the switch unless you opt out.
bin/magento deploy:mode:set production

# Deploying to a server where the build happened elsewhere.
bin/magento deploy:mode:set production --skip-compilation

# What generated/metadata looks like afterwards: one file per area.
ls generated/metadata
# adminhtml.php  crontab.php  frontend.php  global.php  graphql.php
# webapi_rest.php  webapi_soap.php

Recovering from stale generated code

Reach for this whenever an error names a class ending in Interceptor, Factory or Proxy and the source looks correct. The generator refuses to overwrite a file that already exists, so the old class keeps being loaded until the directory is removed. In developer mode you can stop after the delete and the cache flush.

Example · shell
# Symptom after editing a constructor, in developer mode:
#   Type Error occurred when creating object:
#   Vendor\Module\Model\Report\Builder\Interceptor, ...

rm -rf generated/code generated/metadata
bin/magento cache:flush
bin/magento setup:di:compile   # production only; developer mode regenerates on the next request

Discussion

  • Be the first to comment on this lesson.