0

Proxy — controlling access, lazy loading

Intermediate5 min read·eng-03-004
comparelaravel-src

Concept

Proxy pattern provides a surrogate or placeholder for another object. The Proxy controls access to the original object, allowing it to intercept operations before or after they reach the target.

Types of Proxy:

  • Virtual Proxy (Lazy Loading): Delays expensive object creation until it's needed. $user->posts in Eloquent is a virtual proxy — the posts query only runs when the property is accessed.
  • Protection Proxy: Controls access based on permissions. Check authorization before delegating to the real object.
  • Remote Proxy: Represents an object in a different address space or server. An RPC stub is a remote proxy.
  • Caching Proxy: Caches results of expensive operations. Returns cached results without calling the real object.
  • Logging Proxy: Records method calls for debugging or auditing. (Similar to Decorator, but the intent is different — Proxy controls access, Decorator adds behavior.)

Structure: Proxy and real subject implement the same interface. Proxy holds a reference to the real subject (or creates it lazily). Clients interact with the Proxy, unaware they're not talking to the real thing.

Proxy vs Decorator: Both wrap an object. Proxy CONTROLS access to the object (and often creates it lazily). Decorator ADDS behavior to an object. The intent differs: Proxy is about access control/deferral; Decorator is about extension.

Proxy in PHP: __get(), __call(), __set() magic methods can implement transparent proxies. Dynamic proxies using Reflection can intercept any method call.

Code Example

php
<?php
interface ImageInterface
{
    public function render(): string;
    public function getSize(): int;
}

// Real subject — expensive to load
class HeavyImage implements ImageInterface
{
    private string $data;

    public function __construct(private readonly string $path)
    {
        // Expensive: reads file, decodes, processes
        $this->data = file_get_contents($path);
        echo "Image loaded from disk: {$path}\n"; // for demo
    }

    public function render(): string { return "<img src='data:image/jpg;base64," . base64_encode($this->data) . "'>"; }
    public function getSize(): int   { return strlen($this->data); }
}

// Virtual Proxy — lazy loading
class LazyImageProxy implements ImageInterface
{
    private ?HeavyImage $realImage = null;

    public function __construct(private readonly string $path) {}

    private function loadRealImage(): HeavyImage
    {
        if ($this->realImage === null) {
            $this->realImage = new HeavyImage($this->path); // created on first access
        }
        return $this->realImage;
    }

    public function render(): string { return $this->loadRealImage()->render(); }
    public function getSize(): int   { return $this->loadRealImage()->getSize(); }
}

// Protection Proxy — authorization check
class AuthorizationProxy implements ImageInterface
{
    public function __construct(
        private readonly ImageInterface $real,
        private readonly string $userRole,
        private readonly string $requiredPermission = 'view_image',
    ) {}

    public function render(): string
    {
        if (!$this->hasPermission()) {
            throw new \RuntimeException("Access denied. Required permission: {$this->requiredPermission}");
        }
        return $this->real->render();
    }

    public function getSize(): int
    {
        if (!$this->hasPermission()) throw new \RuntimeException('Access denied.');
        return $this->real->getSize();
    }

    private function hasPermission(): bool
    {
        $permissions = ['admin' => ['view_image', 'delete_image'], 'user' => ['view_image']];
        return in_array($this->requiredPermission, $permissions[$this->userRole] ?? []);
    }
}

// Caching Proxy
class CachingProxy implements ImageInterface
{
    private ?string $cachedRender = null;

    public function __construct(private readonly ImageInterface $real) {}

    public function render(): string
    {
        if ($this->cachedRender === null) {
            $this->cachedRender = $this->real->render(); // render once, cache
        }
        return $this->cachedRender;
    }

    public function getSize(): int { return $this->real->getSize(); }
}

// Usage — stacking proxies
$image = new AuthorizationProxy(
    new CachingProxy(
        new LazyImageProxy('/images/hero.jpg')
    ),
    userRole: 'user'
);
// Image not loaded yet
echo $image->render(); // loads image, renders, caches
echo $image->render(); // returns cached render, no disk read