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.
Table of Contents
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?
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()?
$factory->create().3 When should you use a proxy?
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?
5 How do I create a manual factory?
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?
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?
8 Factory vs. Proxy, what is the difference?
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?
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?
final class cannot be extended. Solution: your own wrapper class with the final class as a dependency (composition instead of inheritance).