0

Decorator — adding behaviour without inheritance

Intermediate5 min read·eng-03-002
interviewcompare

Concept

Decorator pattern adds behavior to an object without modifying it or using subclassing. Decorators implement the same interface as the component they wrap, and can be stacked — wrapping a decorator in another decorator.

The problem: You have a Cache class. You want to add logging to it. Then encryption. Then compression. Creating LoggingCache extends Cache, EncryptedCache extends Cache, CompressedCache extends Cache, and all combinations thereof leads to a class explosion. Decorators avoid this.

Structure:

  • Component Interface (CacheInterface): The interface both the real component and decorators implement.
  • Concrete Component (FileCache, RedisCache): The real implementation.
  • Decorator: Implements CacheInterface, takes a CacheInterface as a constructor parameter, delegates to it, and adds behavior before or after.

Stack decorators: $cache = new LoggingDecorator(new CompressionDecorator(new RedisCache())). Each decorator wraps the previous. Calls pass inward, return outward.

PHP real-world examples:

  • PSR-7 Response decorators.
  • Logger decorators (add context, channel routing).
  • Stream decorators in Guzzle.
  • Doctrine's DBAL connection decorators.
  • Laravel: Pipeline middleware IS the Decorator pattern applied to request handling.

Decorator vs Inheritance: Decorators compose behavior at runtime; inheritance bakes it in at compile time. Decorators can be combined in any order. Multiple inheritance (which PHP doesn't have) would be needed to combine multiple inheritance-based extensions.

Code Example

php
<?php
// Component Interface
interface CacheInterface
{
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl = 3600): void;
    public function delete(string $key): void;
}

// Concrete Component
class RedisCache implements CacheInterface
{
    public function get(string $key): mixed { /* fetch from Redis */ return null; }
    public function set(string $key, mixed $value, int $ttl = 3600): void { /* store in Redis */ }
    public function delete(string $key): void { /* delete from Redis */ }
}

// Abstract Decorator — DRY base that delegates to wrapped component
abstract class CacheDecorator implements CacheInterface
{
    public function __construct(protected readonly CacheInterface $inner) {}

    public function get(string $key): mixed     { return $this->inner->get($key); }
    public function set(string $key, mixed $value, int $ttl = 3600): void { $this->inner->set($key, $value, $ttl); }
    public function delete(string $key): void   { $this->inner->delete($key); }
}

// Concrete Decorators
class LoggingCacheDecorator extends CacheDecorator
{
    public function __construct(CacheInterface $inner, private readonly \Psr\Log\LoggerInterface $logger)
    {
        parent::__construct($inner);
    }

    public function get(string $key): mixed
    {
        $value = parent::get($key);
        $this->logger->debug('Cache ' . ($value !== null ? 'hit' : 'miss'), ['key' => $key]);
        return $value;
    }

    public function set(string $key, mixed $value, int $ttl = 3600): void
    {
        $this->logger->debug('Cache set', ['key' => $key, 'ttl' => $ttl]);
        parent::set($key, $value, $ttl);
    }
}

class CompressingCacheDecorator extends CacheDecorator
{
    public function get(string $key): mixed
    {
        $compressed = parent::get($key);
        return $compressed !== null ? unserialize(gzuncompress($compressed)) : null;
    }

    public function set(string $key, mixed $value, int $ttl = 3600): void
    {
        parent::set($key, gzcompress(serialize($value)), $ttl);
    }
}

class EncryptingCacheDecorator extends CacheDecorator
{
    public function __construct(CacheInterface $inner, private readonly string $key)
    {
        parent::__construct($inner);
    }

    public function get(string $key): mixed
    {
        $encrypted = parent::get($key);
        return $encrypted !== null ? $this->decrypt($encrypted) : null;
    }

    public function set(string $cacheKey, mixed $value, int $ttl = 3600): void
    {
        parent::set($cacheKey, $this->encrypt($value), $ttl);
    }

    private function encrypt(mixed $value): string { return base64_encode(serialize($value) . $this->key); }
    private function decrypt(string $value): mixed { return unserialize(base64_decode($value)); }
}

// Composing decorators — order matters!
$cache = new LoggingCacheDecorator(
    new CompressingCacheDecorator(
        new EncryptingCacheDecorator(
            new RedisCache(),
            key: env('CACHE_ENCRYPTION_KEY')
        )
    ),
    logger: app(\Psr\Log\LoggerInterface::class)
);
// get() calls: Log → Compress.get → Encrypt.get → Redis.get → decrypt → decompress → log result