← Back to blog

Magento 2 Factories, Proxies and the generated Folder

Rodrigo Biazzi

Magento 2 writes a lot of PHP for you. Factories, proxies, interceptors and extension attribute classes are not in any module. They are generated into generated/code the first time they are needed, or ahead of time by setup:di:compile. Most errors that people blame on caching are actually problems with these generated classes, so it pays to know what each one does.

This applies to Magento Open Source and Adobe Commerce 2.4.8 on PHP 8.3 or 8.4.

Why the framework generates code

The object manager shares instances by default. That is right for services, but wrong for entities. If you inject Magento\Catalog\Model\Product directly, you get one shared product object, and every class that injects it sees the same data. You need a way to ask for a fresh instance without calling new and without using the object manager in your class.

The same goes for laziness. Some dependencies are expensive to build, and you do not want to create them in a constructor if the method that uses them is rarely called. And plugins need a place to hook in. Magento solves all three with generated classes, following naming conventions that the autoloader recognizes.

Factories

A factory is a class with a create() method that returns a new instance of a given type. You never write it. You type hint a class name ending in Factory, and Magento generates it:

<?php
declare(strict_types=1);

namespace Acme\Notice\Model;

use Acme\Notice\Model\ResourceModel\Notice\CollectionFactory;

class NoticeCreator
{
    public function __construct(
        private readonly NoticeFactory $noticeFactory,
        private readonly CollectionFactory $collectionFactory
    ) {
    }

    public function build(string $message): Notice
    {
        $notice = $this->noticeFactory->create();
        $notice->setMessage($message);
        return $notice;
    }

    public function countActive(): int
    {
        return $this->collectionFactory->create()
            ->addFieldToFilter('is_active', 1)
            ->getSize();
    }
}

Note that NoticeFactory is used without a use statement because it lives in the same namespace as Notice. The generated file ends up in generated/code/Acme/Notice/Model/NoticeFactory.php.

create() accepts an array of constructor arguments by name. For data models built on AbstractModel or DataObject, the usual form is passing initial data:

$notice = $this->noticeFactory->create(['data' => ['message' => 'Free shipping this week']]);

You can also generate factories for interfaces. Magento\Catalog\Api\Data\ProductInterfaceFactory creates whatever class the preference for ProductInterface points to. This is the right choice when your code works with the contract and should not care about the implementation.

When not to use a factory

Services do not need factories. If a class has no per instance state, inject it directly. A factory for a repository, a helper or a config reader just adds a create() call that returns a new object you did not need. In some cases it also breaks things, because two instances of a service that caches data internally will not share that cache.

Proxies

A proxy is a subclass of the original class that holds nothing until you call a method on it. The first call creates the real object through the object manager and forwards the call. All later calls go straight to that instance.

You do not type hint a proxy in PHP. You keep the original type in the constructor and switch it in di.xml:

<type name="Acme\Notice\Console\Command\ImportNotices">
    <arguments>
        <argument name="importer" xsi:type="object">Acme\Notice\Model\Importer\Proxy</argument>
    </arguments>
</type>

The generated class is generated/code/Acme/Notice/Model/Importer/Proxy.php. Your class does not know it received a proxy, which is the point. The decision about laziness is configuration, not code.

Where proxies matter

  • CLI commands. Running bin/magento builds every registered command to list them. If one command injects something that opens a database connection or reads a large config in its constructor chain, every bin/magento call pays for it, and some commands break before the database exists, for example during setup:install. Proxies on the heavy dependencies of a command avoid that. The custom CLI command article shows a full example.
  • Sessions. Core injects Magento\Customer\Model\Session\Proxy and Magento\Checkout\Model\Session\Proxy in many places. Starting a session has side effects, including headers and cookies, so it should only happen when the session is actually used.
  • Plugins and observers with rare paths. If a plugin only uses a dependency in one branch, a proxy keeps the common path cheap.

Proxies cannot be generated for final classes, because the proxy has to extend the original class to stay compatible with its type hint.

Interceptors

When any plugin is declared for a class, Magento generates Vendor\Module\Class\Interceptor, a subclass that overrides each public method and runs the before, around and after plugins. The object manager returns the interceptor instead of the original class whenever plugins exist for it.

This has practical consequences:

  • Plugins only work on public methods of classes created through the object manager. A class instantiated with new gets no interceptor.
  • Final classes and final methods cannot be intercepted.
  • Stack traces show the Interceptor class and ___callPlugins(). That is normal and tells you plugins are involved.

The design choices around plugins are covered in plugins vs observers vs preferences.

Other generated classes

For entities with extension attributes, Magento generates ProductExtensionInterface, ProductExtension and a matching factory from the merged extension_attributes.xml files. These are why adding a new extension attribute needs a regeneration step before the getter exists. The extension attributes article covers that in detail.

The generated/metadata folder holds compiled object manager configuration in production mode. It is not PHP classes you would read, but a stale copy causes the same type of confusing errors.

Developer mode vs production mode

In developer mode, a missing generated class is created on the fly by the autoloader the first time it is requested. That is convenient, but it also means old generated files stay around when you change a constructor. A factory or interceptor generated for the old signature can cause errors that look unrelated to your change.

In production mode, you generate everything ahead of time:

bin/magento setup:di:compile

This scans all modules, generates factories, proxies, interceptors and extension classes, and writes the compiled configuration. In a proper build pipeline this runs once in CI, and the result is shipped to servers as part of the release artifact.

Fixing problems with generated code

These errors almost always come from generated code:

  • Class "Acme\Notice\Model\NoticeFactory" does not exist in production mode. Compilation did not run after the class was added, or it failed partway.
  • Argument #2 must be of type ... right after a constructor change. A stale interceptor is still passing the old arguments.
  • Plugins that do not fire. The interceptor was generated before the plugin was declared and never rebuilt.

In developer mode the fix is usually to remove the affected folder and let it regenerate:

rm -rf generated/code/Acme/Notice
bin/magento cache:clean config

Removing all of generated/code and generated/metadata is safe too. It just costs a slower first request. In production mode, rebuild with setup:di:compile and flush the cache. Never edit a generated file by hand, since it will be overwritten.

Do not commit the generated folder to version control. It belongs to the build output, the same as pub/static. The zero downtime deploy article shows where compilation fits in a release pipeline.

A short checklist

  1. Use a factory when you need a new entity instance or a new collection.
  2. Inject services directly, without factories.
  3. Configure a proxy in di.xml for heavy dependencies that are not always used, and for every heavy dependency of a CLI command.
  4. Do not make classes or public methods final if other code may need to plugin into them.
  5. After constructor or plugin changes, clear the relevant generated code in development and recompile in production.

FAQ

Should I write my own factory classes?

Almost never. Magento generates them from the class name. Write one manually only when you need custom creation logic, and then give it a name that does not end in Factory to avoid confusion with generated ones.

Can I type hint a Proxy class in my constructor?

It works, but it is not the intended pattern. Type hint the original class and switch to the proxy in di.xml, so the laziness stays a configuration decision.

Is it safe to delete the generated folder?

Yes, in developer mode Magento rebuilds what it needs on the next request. In production mode you have to run setup:di:compile again before the store works.

Why do I see Interceptor in my stack traces?

Because at least one plugin is declared for that class. The interceptor is a generated subclass that runs the plugins around the original method.

I am available for freelance Magento and Adobe Commerce work. If you are fighting generated code errors in a deploy pipeline or need help refactoring a module, get in touch.

Related articles