Factories and Proxies

Factories build objects you cannot inject; proxies defer building ones that are slow to construct. Magento generates both.

Syntax$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

Example · php
<?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.

Example · php
// 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.

Example · xml
<!-- 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.

Example · php
<?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

  • Be the first to comment on this lesson.