Proxy is a structural pattern that places a surrogate in front of an object to control access to it. Three practical scenarios justify Proxy: lazy loading (deferring expensive creation), access control (checking permissions before executing), and caching. Magento 2 generates Proxy classes automatically for a specific purpose – I show both the GoF pattern and the Magento implementation.
Classic Proxy pattern
<?php
declare(strict_types=1);
interface ImageInterface
{
public function display(): void;
public function getWidth(): int;
public function getHeight(): int;
}
// Real heavyweight object - loads image from disk on creation
class RealImage implements ImageInterface
{
private \GdImage $resource;
public function __construct(private string $path)
{
// Expensive operation - loads and decodes the file
$this->resource = imagecreatefromjpeg($path);
echo "Loaded image: {$path}\n";
}
public function display(): void
{
imagejpeg($this->resource);
}
public function getWidth(): int { return imagesx($this->resource); }
public function getHeight(): int { return imagesy($this->resource); }
}
// Lazy loading Proxy - defers creation until actually needed
class LazyImageProxy implements ImageInterface
{
private ?RealImage $realImage = null;
public function __construct(private string $path) {}
private function getRealImage(): RealImage
{
if ($this->realImage === null) {
$this->realImage = new RealImage($this->path); // created only on first use
}
return $this->realImage;
}
public function display(): void { $this->getRealImage()->display(); }
public function getWidth(): int { return $this->getRealImage()->getWidth(); }
public function getHeight(): int { return $this->getRealImage()->getHeight(); }
}
// Images created but NOT loaded until display() is called
$images = [
new LazyImageProxy('/uploads/image1.jpg'),
new LazyImageProxy('/uploads/image2.jpg'),
new LazyImageProxy('/uploads/image3.jpg'),
];
// Only this image gets loaded from disk - the others are never touched
$images[0]->display();
Protection Proxy – access control
<?php
declare(strict_types=1);
interface OrderRepositoryInterface
{
public function getById(int $id): OrderInterface;
public function delete(OrderInterface $order): bool;
}
// Proxy adds authorisation without modifying the real repository
class AuthorisedOrderRepository implements OrderRepositoryInterface
{
public function __construct(
private OrderRepositoryInterface $realRepository,
private \Magento\Authorization\Model\Acl\AclRetriever $acl,
private \Magento\Backend\Model\Auth\Session $adminSession
) {}
public function getById(int $id): OrderInterface
{
// Everyone can read - no check needed
return $this->realRepository->getById($id);
}
public function delete(OrderInterface $order): bool
{
// Deletion requires specific ACL resource
if (!$this->adminSession->isAllowed('Magento_Sales::delete')) {
throw new \Magento\Framework\Exception\AuthorizationException(
__('You are not authorised to delete orders.')
);
}
return $this->realRepository->delete($order);
}
}
Proxy in Magento 2 – auto-generated lazy loading
<!-- etc/di.xml - declare that a Proxy should be used for this dependency -->
<type name="Vendor\Module\Model\SomeCommand">
<arguments>
<!-- \Proxy suffix tells Magento DI to auto-generate a Proxy class -->
<argument name="heavyService" xsi:type="object">
Vendor\Module\Model\HeavyService\Proxy
</argument>
</arguments>
</type>
<?php
// Why use Magento Proxy?
// If HeavyService has an expensive constructor (DB queries, API calls)
// and SomeCommand only sometimes uses it, Proxy defers instantiation.
class SomeCommand
{
public function __construct(
private \Vendor\Module\Model\HeavyService $heavyService // via Proxy in di.xml
) {
// HeavyService is NOT instantiated here - Proxy takes its place
// Real object only created on first method call
}
public function execute(string $mode): string
{
if ($mode === 'quick') {
return 'quick result'; // HeavyService never instantiated
}
// HeavyService is created NOW - only when actually needed
return $this->heavyService->doExpensiveWork();
}
}
# Magento generates the Proxy class during DI compilation bin/magento setup:di:compile # Generated class location # generated/code/Vendor/Module/Model/HeavyService/Proxy.php
When to use Proxy in Magento 2
- CLI commands – commands that load many services but use only a few depending on arguments. Proxy prevents instantiating everything on every
bin/magento list. - Event observers – observers loaded on every request but rarely triggered. Proxy defers construction of heavy dependencies.
- Circular dependencies – when two services depend on each other, a Proxy on one of them breaks the cycle for DI.
Summary
Proxy is a transparent intermediary – callers do not know they are talking to a proxy. In GoF form it is useful for access control and caching without modifying the original class. In Magento 2 the auto-generated Proxy gives lazy loading for free with a single line in di.xml. It is most valuable in CLI contexts and event observers where you want fast startup times regardless of what the command actually does.
