Factory & Proxy Pattern in Magento 2
AI generated
Magento 2 · Design Patterns

Factory & Proxy
Pattern in Magento 2

Automatically generated factories for new objects, proxies for lazy loading of expensive services: both patterns explained together with concrete use cases and PHP 8.4.

⏱ 12 min read PHP 8.4 Magento 2.4.8

1. Factory Pattern: Why Not new?

In Magento 2 the rule is: new objects are never created with new. The reason is fundamental: new ClassName() bypasses the DI container completely.

  • Plugins (interceptors) are not applied to the created object
  • The configured preference in di.xml is ignored
  • Virtual types do not work
  • The shared/non-shared status is ignored

// WRONG: direct new, bypasses the DI container entirely
$post = new \Mironsoft\Blog\Model\Post();

// RIGHT: use a factory
class PostService
{
    public function __construct(
        private readonly \Mironsoft\Blog\Api\Data\PostInterfaceFactory $postFactory
    ) {}

    public function createPost(): \Mironsoft\Blog\Api\Data\PostInterface
    {
        // create() = new object, fully built through the ObjectManager
        // -> plugins active, DI configured, preferences respected
        return $this->postFactory->create();
    }
}

2. Automatically Generated Factories

Magento 2 generates factories automatically when running bin/magento setup:di:compile or on the fly in developer mode. Convention: declare class name + "Factory" as the type hint:


<?php
declare(strict_types=1);

use Mironsoft\Blog\Model\PostFactory;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;

class OrderProcessor
{
    public function __construct(
        // Magento generates: generated/code/Mironsoft/Blog/Model/PostFactory.php
        private readonly PostFactory $postFactory,

        // For interface-based factories:
        private readonly PostInterfaceFactory $postInterfaceFactory
    ) {}
}

The generated factory looks (simplified) like this:


<?php
// GENERATED: generated/code/Mironsoft/Blog/Model/PostFactory.php
// DO NOT EDIT THIS FILE MANUALLY!

namespace Mironsoft\Blog\Model;

class PostFactory
{
    public function __construct(
        private readonly \Magento\Framework\ObjectManagerInterface $objectManager,
        private readonly string $instanceName = Post::class
    ) {}

    /**
     * Creates a new Post instance, fully DI-configured.
     *
     * @param array<string, mixed> $data
     */
    public function create(array $data = []): Post
    {
        return $this->objectManager->create($this->instanceName, $data);
    }
}

3. Using Factories Correctly


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Model;

use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;
use Mironsoft\Blog\Api\PostRepositoryInterface;

/**
 * Blog post creator, uses a Factory for new Post instances.
 */
class PostCreator
{
    public function __construct(
        private readonly PostInterfaceFactory    $postFactory,
        private readonly PostRepositoryInterface $postRepository
    ) {}

    public function createDraft(string $title, string $content, int $authorId): PostInterface
    {
        // create() always returns a NEW instance (non-shared)
        $post = $this->postFactory->create();
        $post->setTitle($title);
        $post->setContent($content);
        $post->setAuthorId($authorId);
        $post->setStatus('draft');
        return $this->postRepository->save($post);
    }

    /** @return PostInterface[] */
    public function bulkCreate(array $postsData): array
    {
        return array_map(function (array $data): PostInterface {
            $post = $this->postFactory->create(); // new object per entry
            $post->setTitle($data['title']);
            $post->setContent($data['content']);
            return $this->postRepository->save($post);
        }, $postsData);
    }
}

4. Factory With Initial Data

Factories accept an optional $data array as a constructor argument. The explicit setter API is preferable, though:


// Calling the factory with data:
$post = $this->postFactory->create([
    'data' => [
        'title'     => 'Example Post',
        'status'    => 'draft',
        'author_id' => 42,
    ]
]);

// Preferred: setter API (more explicit, type-safe, IDE support):
$post = $this->postFactory->create();
$post->setTitle('Example Post');
$post->setStatus('draft');
$post->setAuthorId(42);

5. Proxy Pattern: Lazy Loading for Expensive Objects

The Proxy pattern defers the creation of an expensive object until it is actually needed. A proxy is injected as a stand-in: from the outside it looks exactly like the real class, but it only creates the real instance on the first method call:


// PROBLEM: HeavyIndexer is instantiated immediately, even if only
// calculateBasePrice() is called (HeavyIndexer is not needed there!)
class PriceCalculator
{
    public function __construct(
        private readonly HeavyIndexer $indexer // WITHOUT proxy: always expensive
    ) {}

    public function calculateBasePrice(float $price): float
    {
        return $price * 1.19; // HeavyIndexer is not needed here
    }

    public function calculateSpecialPrice(float $price): float
    {
        return $this->indexer->getSpecialFactor() * $price; // needed only here
    }
}

// SOLUTION: configure a proxy in di.xml
// -> HeavyIndexer is only instantiated in calculateSpecialPrice()

6. Automatically Generated Proxies

Proxies are generated automatically by Magento. Two ways to configure them:


<!-- Option 1: via di.xml (recommended) -->
<type name="Mironsoft\Catalog\Model\PriceCalculator">
    <arguments>
        <!-- Magento generates: generated/code/.../HeavyIndexer/Proxy.php -->
        <argument name="indexer" xsi:type="object">
            Mironsoft\Catalog\Model\HeavyIndexer\Proxy
        </argument>
    </arguments>
</type>

<?php
// Option 2: directly in the constructor with the \Proxy suffix
use Magento\Catalog\Model\Indexer\Product\Price\Processor\Proxy as PriceProcessorProxy;

class ProductSaver
{
    public function __construct(
        private readonly PriceProcessorProxy $priceProcessor
    ) {}
}

// The generated proxy class (simplified):
// GENERATED: generated/code/.../HeavyIndexer/Proxy.php
class Proxy extends HeavyIndexer
{
    private ?HeavyIndexer $subject = null;

    public function __construct(
        private readonly \Magento\Framework\ObjectManagerInterface $objectManager
    ) {
        // The constructor of the real class is NOT called!
    }

    private function getSubject(): HeavyIndexer
    {
        // First real use -> the real class is now instantiated
        return $this->subject ??= $this->objectManager->get(HeavyIndexer::class);
    }

    public function getSpecialFactor(): float
    {
        return $this->getSubject()->getSpecialFactor();
    }
}

7. Session Proxies: the Most Important Use Case

The most common use case for proxies: session objects. They start PHP sessions and affect HTTP response headers. If they are instantiated at bootstrap, problems arise with page caching. Always inject session objects as a proxy.


<!-- di.xml: configuring a session proxy -->
<type name="Mironsoft\Blog\Controller\Post\View">
    <arguments>
        <!-- Always use a proxy for session objects! -->
        <argument name="customerSession" xsi:type="object">
            Magento\Customer\Model\Session\Proxy
        </argument>
        <argument name="checkoutSession" xsi:type="object">
            Magento\Checkout\Model\Session\Proxy
        </argument>
    </arguments>
</type>

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Controller\Post;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\View\Result\Page;

/**
 * Blog post view action, uses a Session Proxy for lazy initialization.
 */
class View implements HttpGetActionInterface
{
    public function __construct(
        private readonly PageFactory    $pageFactory,
        private readonly CustomerSession $customerSession // proxy injected via di.xml
    ) {}

    public function execute(): Page
    {
        // Session is initialized HERE, not at bootstrap
        $customerId = $this->customerSession->getCustomerId();

        $page = $this->pageFactory->create();
        $page->getConfig()->getTitle()->set(__('Blog Post'));
        return $page;
    }
}

8. When Should You Use a Proxy?

  • Session objects: Always! (CustomerSession\Proxy, CheckoutSession\Proxy, CoreSession\Proxy)
  • Heavy services: classes with expensive bootstrap operations in the constructor that are only rarely needed
  • Resolving circular dependencies: if A depends on B and B depends on A, inject one of them as a proxy
  • Not for: lightweight services that are needed on every request anyway, where proxy overhead is not justified

Mironsoft

Magento 2 Performance & Architecture

Want to analyze and optimize Magento 2 performance?

We analyze session initialization, expensive DI chains, and unnecessary object creation in your Magento store, and optimize it with targeted Factory and Proxy patterns.

Factory Analysis

Finding direct new ClassName() calls and replacing them with proper factory usage

Session Proxies

Switching all session injections to a proxy, avoiding page cache problems

Lazy Loading

Identifying heavy services and making them lazy through proxy injection

9. Summary

Factory and Proxy are two patterns that use the ObjectManager efficiently. Factory for new objects: never new directly. Proxy for lazy loading: especially for sessions and heavy services. Both are generated automatically by Magento.

Factory & Proxy, the Essentials at a Glance

Factory, New Objects

Never new ClassName(). Always $factory->create(). Auto-generated as ClassNameFactory. Respects DI, plugins, preferences.

Proxy, Lazy Loading

Lazily instantiates expensive classes. Auto-generated as ClassName\Proxy. Via di.xml or directly as a type hint. Mandatory for session objects.

Session Proxy

Always CustomerSession\Proxy instead of a direct session. Applies to all session classes. Prevents premature session initialization and page cache problems.

setup:di:compile

After every di.xml change: bin/magento setup:di:compile. Generates all factories, proxies, and interceptors. Generated on the fly in developer mode.

10. FAQ: Factory & Proxy in Magento 2

1 Do I have to write factories myself?
No. Declare ClassNameFactory as a type hint, Magento generates it automatically in generated/code/. Developer mode: on the fly. Production: bin/magento setup:di:compile.
2 What happens with a direct new ClassName()?
The DI container is bypassed: no plugins, no preferences, no virtual type. Leads to errors if the class has its own DI dependencies. Always use $factory->create().
3 When should you use a proxy?
Mandatory: all session classes (CustomerSession\Proxy). Useful: heavy services that are only sometimes needed. Also for resolving circular dependencies. Not for lightweight services that are always needed.
4 Why not inject a session directly?
Session starts the PHP session in the constructor and sets HTTP headers. With direct injection this happens at bootstrap, so page caching does not work. A proxy solves this through lazy loading: the session starts only on the first real access.
5 How do I create a manual factory?
Your own class that injects ObjectManagerInterface and calls it in create(). The name must end in Factory. Register it in di.xml as a preference for the auto-generated factory.
6 Create a factory for an interface?
Yes. PostInterfaceFactory is auto-generated when PostInterface is bound to Post via a preference. The factory creates the concrete class, returns the interface type.
7 How does a proxy resolve circular dependencies?
A needs B, B needs A, an infinite loop. Solution: inject one of them as a proxy. The proxy creates the real instance only on the first method call, at which point the other class is already fully initialized.
8 Factory vs. Proxy, what is the difference?
Factory: NEW instances via create() for non-injectable objects. Proxy: a lazy wrapper for injectable objects, a single instance with deferred initialization. Factory for new objects, proxy for late initialization.
9 Is setup:di:compile needed after using a factory?
Developer mode: automatic, on the fly. Production/staging: bin/magento setup:di:compile needed. Generated files end up in generated/code/, never edit them manually.
10 Is a proxy possible for final classes?
No. Proxies are created through inheritance, a final class cannot be extended. Solution: your own wrapper class with the final class as a dependency (composition instead of inheritance).