Factories and Proxies
Factories build objects you cannot inject; proxies defer building ones that are slow to construct. Magento generates both.
$this->thingFactory->create(array $data = []) | <argument xsi:type="object">Vendor\Thing\Proxy</argument>Injection assumes one shared object per request. Two kinds of dependency break that assumption, and each has a generated class as the answer.
Factories, for non-injectable objects
An object that carries per-use state cannot be shared: an HTTP client holding one response, or an entity representing one record. Type-hint SomethingFactory instead and call create() for each use. No file exists on disk; Magento generates one from the name. The generated body is a single line, $this->_objectManager->create($this->_instanceName, $data), so $factory->create() is the shorthand for the object manager call you are not allowed to write yourself. create(array $data = []) takes constructor arguments by parameter name.
Proxies, for slow constructors
A proxy extends the real class and builds it only when a method is first called. It is named by appending \Proxy, and it is configured in di.xml, never type-hinted: <argument name="storeManager" xsi:type="object">Magento\Store\Model\StoreManagerInterface\Proxy</argument>. Core does exactly that. Use one when a dependency is expensive and rarely used on a given path.
Making it take effect
Generated classes are written to generated/code. In developer mode they appear on first use. In production mode nothing is generated at runtime, so run bin/magento setup:di:compile.
Example
<?php
declare(strict_types=1);
// app/code/Vendor/Feed/Service/FeedClient.php
namespace Vendor\Feed\Service;
use Magento\Framework\HTTP\Client\CurlFactory;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;
class FeedClient
{
public function __construct(
private readonly CurlFactory $curlFactory,
private readonly LoggerInterface $logger,
// Hinted on the interface; di.xml decides whether the real
// store manager or its proxy arrives.
private readonly StoreManagerInterface $storeManager
) {
}
public function fetch(string $url): ?string
{
// One client per call. Curl holds headers, cookies and the
// response body as state, so a shared instance would leak
// one request's data into the next.
$curl = $this->curlFactory->create();
$curl->setTimeout(10);
$curl->addHeader('Accept', 'application/json');
$curl->get($url);
if ($curl->getStatus() !== 200) {
$this->logger->warning(
'Feed request failed',
[
'url' => $url,
'status' => $curl->getStatus(),
// Only this path touches the store manager, which
// is why a proxy pays off here.
'store' => $this->storeManager->getStore()->getCode(),
]
);
return null;
}
return $curl->getBody();
}
}When to use it
- Calling an external endpoint several times in one request without reusing a dirty client.
- Keeping a rarely used but expensive dependency out of the constructor cost of a hot class.
- Explaining a class-not-found error for a Factory that exists locally but not on the production server.
More examples
Shorthand and longhand: what the generated factory is
Worth reading once, because it removes the mystery: the factory is the object manager call, wrapped in a class whose name makes the dependency visible in your constructor. Open the real file under generated/code when a create() call behaves oddly; the $instanceName default tells you exactly which class is being built.
// generated/code/Magento/Framework/HTTP/Client/CurlFactory.php
// Written by the code generator from the name alone.
class CurlFactory
{
protected $_objectManager = null;
protected $_instanceName = null;
public function __construct(
\Magento\Framework\ObjectManagerInterface $objectManager,
$instanceName = '\\Magento\\Framework\\HTTP\\Client\\Curl'
) {
$this->_objectManager = $objectManager;
$this->_instanceName = $instanceName;
}
public function create(array $data = [])
{
return $this->_objectManager->create($this->_instanceName, $data);
}
}A proxy configured in di.xml
The constructor still type-hints Magento\Store\Model\StoreManagerInterface; only the configuration changes, so no PHP is edited and nothing else in the codebase is affected. Core uses this exact proxy in app/etc/di.xml. Reach for it when a profiler shows construction cost on a path that often does not touch the dependency at all, and apply it with bin/magento cache:flush, or setup:di:compile in production mode.
<!-- app/code/Vendor/Feed/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\Feed\Service\FeedClient">
<arguments>
<argument name="storeManager" xsi:type="object">Magento\Store\Model\StoreManagerInterface\Proxy</argument>
</arguments>
</type>
</config>The version that teaches a bug
This passes compilation, passes a single-call test, and fails in production the first time one request fetches two feeds. The symptom is a response body that belongs to the previous call, which is hard to attribute. Whenever a class holds the result of its own work, inject its factory instead.
<?php
declare(strict_types=1);
// app/code/Vendor/Feed/Service/BrokenFeedClient.php
namespace Vendor\Feed\Service;
use Magento\Framework\HTTP\Client\Curl;
class BrokenFeedClient
{
public function __construct(
// Shared by default: the same Curl object for the whole request.
private readonly Curl $curl
) {
}
public function fetch(string $url): string
{
$this->curl->get($url);
// Headers and cookies set by an earlier call are still set,
// and two calls in one request can return mixed-up results.
return $this->curl->getBody();
}
}
Discussion