Mastering the ‘Decorator’ Design Pattern in Symfony

Filip Horvat
6 min readFeb 11, 2024

--

The Decorator Design Pattern is one of the design patterns introduced in the book ‘Design Patterns,’ written by the Gang of Four. The authors of this book are widely regarded as authorities in the field of software engineering.

The Decorator pattern is a structural design pattern, and its purpose is to attach additional responsibilities to an object dynamically. Decorators offer a flexible alternative to subclassing for extending functionality.

Decorator consist of 4 participants:

  • Component
  • Concrete Component
  • Decorator
  • Concrete Decorator

All three participants — Concrete Component, Decorator, and Concrete Decorator — implement the Component interface in the PHP implementation. The Decorator holds a reference to an instance of the Component.

In this story, I will try to explain, using my own example, how to implement and incorporate decorator design pattern into Symfony.

Here is documentation on how to use the decorator pattern in Symfony with the service container:

https://symfony.com/doc/current/service_container/service_decoration.html#control-the-behavior-when-the-decorated-service-does-not-exist

My story will provide more context on that.

Example

We are going to create the following example in Symfony. We will have a Building entity, and we want to store data from that entity into Elasticsearch. Before storing anything in Elasticsearch, we need to fetch the data that will be stored.

For this occasion, we have two interfaces which are added to Building Entity:

<?php

namespace App\Service\Interface;

interface IdentifierInterface
{
public function getId(): ?int;
}
<?php

namespace App\Service\Interface;

use DateTimeInterface;

interface CreatedAtInterface
{
public function getCreatedAt(): ?DateTimeInterface;
}

Those interfaces above are not part of the decorator design pattern; they are auxiliary interfaces explaining the decorator more effectively.

And here is our Building entity, which implements the previously mentioned interfaces:

<?php

namespace App\Entity;

use App\Service\Interface\CreatedAtInterface;
use App\Service\Interface\IdentifierInterface;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

#[ORM\Entity]
class Building implements IdentifierInterface, CreatedAtInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['get'])]
private ?int $id = null;

#[ORM\Column(nullable: true)]
private ?string $name = null;

#[ORM\Column(type: 'datetime')]
protected ?DateTimeInterface $createdAt;

#[ORM\Column(nullable: true)]
#[Groups(['get', 'put'])]
private ?string $address = null;

public function getId(): ?int
{
return $this->id;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(?string $name): void
{
$this->name = $name;
}

public function getCreatedAt(): ?DateTimeInterface
{
return $this->createdAt;
}

public function setCreatedAt(?DateTimeInterface $createdAt): void
{
$this->createdAt = $createdAt;
}

public function getAddress(): ?string
{
return $this->address;
}

public function setAddress(?string $address): void
{
$this->address = $address;
}
}

In Elasticsearch, we aim to store the id, name, address, and createdAt attributes from the Building entity.

We will have a service that returns the data to be stored in Elasticsearch for each building. This service will also serve as a concrete component participant in the decorator pattern implementation, providing only the name and address.

The id and created_at will be added using two other services, which will serve as concrete decorators participants in the decorator pattern implementation.

Decorator Design Pattern — Code example

Now, we are going to create the four previously mentioned participants of the decorator pattern.

Component

<?php

namespace App\Service\Decorator\Component;

interface EsDataProviderComponent
{
public function getData($entity): array;
}

The component serves as an interface, defining a getData() function specifically designed for retrieving data to be used with Elasticsearch.

Concrete Component

<?php

namespace App\Service\Decorator\Component;

use App\Entity\Building;

class EsDataProviderBuildingComponent implements EsDataProviderComponent
{
public function getData($entity): array
{
/** @var Building $entity */

return [
'name' => $entity->getName(),
'address' => $entity->getAddress(),
];
}
}

The concrete component for retrieving Elasticsearch data for the Building entity is named EsDataProviderBuildingComponent. The idea is to create a concrete component for each entity. For example, if you have an entity 'Order,' a corresponding concrete component would be 'EsDataProviderOrderComponent.

Decorator

<?php

namespace App\Service\Decorator\Decorator;

use App\Service\Decorator\Component\EsDataProviderComponent;

class EsDataProviderDecorator implements EsDataProviderComponent
{
protected EsDataProviderComponent $component;

public function __construct(EsDataProviderComponent $component)
{
$this->component = $component;
}

public function getData($entity): array
{
return $this->component->getData($entity);
}
}

The decorator holds a reference to the component, allowing it to enhance or modify the behavior of the component while maintaining a connection to the original functionality.

Concrete decorator

For this occasion, we are going to create two concrete decorators.

<?php

namespace App\Service\Decorator\Decorator;

use App\Service\Decorator\Component\EsDataProviderBuildingComponent;
use App\Service\Interface\CreatedAtInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;


class CreatedAtEsDataProviderDecorator extends EsDataProviderDecorator
{
public function getData($entity): array
{
$data = $this->component->getData($entity);

if ($entity instanceof CreatedAtInterface) {
$data += [
'created_at' => $entity->getCreatedAt()->format('Y-m-d H:i:s'),
];
}

return $data;
}
}
<?php

// Concrete Decorator

namespace App\Service\Decorator\Decorator;

use App\Service\Decorator\Component\EsDataProviderBuildingComponent;
use App\Service\Interface\IdentifierInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;


class IdentifierEsDataProviderDecorator extends EsDataProviderDecorator
{
public function getData($entity): array
{
$data = $this->component->getData($entity);

if ($entity instanceof IdentifierInterface) {
$data += [
'id' => $entity->getId(),
];
}

return $data;
}
}

The first one adds ‘createdAt’ to the data for entities implementing the ‘CreatedAtInterface,’ while the second one adds ‘id’ to the data for entities implementing the ‘IdentifierInterface.’

Decorator Design Pattern Code called WITHOUT Symfony Utils

  1. Here’s how to retrieve data for a building without using decorators:

$esDataProviderBuildingComponent = new EsDataProviderBuildingComponent();

echo 'RESULT: ';
$building = $this->createBuilding();
dd($esDataProviderBuildingComponent->getData($building));

Result is:

array:2 [▼
"name" => "test name"
"address" => "test address"
]

2. Apply the CreatedAtEsDataProviderDecorator decorator to add the 'createdAt' attribute to the data:


$esDataProviderBuildingComponent = new EsDataProviderBuildingComponent();
$esDataProviderBuildingComponent = new CreatedAtEsDataProviderDecorator($esDataProviderBuildingComponent);

echo 'RESULT: ';
$building = $this->createBuilding();
dd($esDataProviderBuildingComponent->getData($building));

Result is:

array:3 [▼
"name" => "test name"
"address" => "test address"
"created_at" => "2024-02-11 11:50:24"
]

3. Apply the IdentifierEsDataProviderDecorator decorator to add the 'id' attribute to the data:

$esDataProviderBuildingComponent = new EsDataProviderBuildingComponent();
$esDataProviderBuildingComponent = new CreatedAtEsDataProviderDecorator($esDataProviderBuildingComponent);
$esDataProviderBuildingComponent = new IdentifierEsDataProviderDecorator($esDataProviderBuildingComponent);

echo 'RESULT: ';
$building = $this->createBuilding();
dd($esDataProviderBuildingComponent->getData($building));

Result is:

array:4 [▼
"name" => "test name"
"address" => "test address"
"created_at" => "2024-02-11 11:51:40"
"id" => 1
]

Great to hear that you’ve successfully implemented the decorator design pattern!

EsDataProviderBuildingComponent concrete component was decorated by CreatedAtEsDataProviderDecorator and IdentifierEsDataProviderDecorator.

Decorator Design Pattern Code called WITH Symfony Utils

  1. Initially, we will retrieve Elasticsearch data for the building without decorators. The EsDataProviderBuildingComponent is injected with the service container, and the data is fetched for the building entity:
public function __construct(
private readonly EsDataProviderBuildingComponent $esDataProviderBuildingComponent
) {
}

private function syncBuilding(Building $building)
{
$data = $this->esDataProviderBuildingComponent->getData($building);
//...
}

Result is:

array:2 [▼
"name" => "test name"
"address" => "test address"
]

2. Now, we want to apply the two previously mentioned decorators to the concrete component EsDataProviderBuildingComponent

Essentially, we aim to inject this into the constructor:

$esDataProviderBuildingComponent = new EsDataProviderBuildingComponent();
$esDataProviderBuildingComponent = new CreatedAtEsDataProviderDecorator($esDataProviderBuildingComponent);
$esDataProviderBuildingComponent = new IdentifierEsDataProviderDecorator($esDataProviderBuildingComponent);

One line:

new IdentifierEsDataProviderDecorator(new CreatedAtEsDataProviderDecorator(new EsDataProviderBuildingComponent()));

And we want to inject that here:

public function __construct(
private readonly EsDataProviderBuildingComponent "HERE"
) {
}

We encounter a problem where our service is type-hinted as EsDataProviderBuildingComponent, but we intend to inject IdentifierEsDataProviderDecorator.

The first step is to change the type hint to EsDataProviderComponent.

public function __construct(
private readonly EsDataProviderComponent "HERE"
) {
}

Now, the type hint is correct since we want to inject IdentifierEsDataProviderDecorator, which implements EsDataProviderComponent. However, it still doesn't work because we need to inform Symfony about the actual class to inject when an interface is type-hinted, the code is here:

public function __construct(
#[Autowire(service: EsDataProviderBuildingComponent::class)]
private readonly EsDataProviderComponent $esDataProviderComponent,
) {
}

The code is now functional, but the output remains:

array:2 [▼
"name" => "test name"
"address" => "test address"
]

because decorators are still not added.

The final step is to apply the decorators:

  • IdentifierEsDataProviderDecorator
  • CreatedAtEsDataProviderDecorator

to component:

  • EsDataProviderBuildingComponent

We will simply add the #[AsDecorator] PHP attribute to the decorators, and Symfony will automatically handle the process:

#[AsDecorator(decorates: EsDataProviderBuildingComponent::class)]
class CreatedAtEsDataProviderDecorator extends EsDataProviderDecorator
{
#[AsDecorator(decorates: EsDataProviderBuildingComponent::class)]
class IdentifierEsDataProviderDecorator extends EsDataProviderDecorator
{

The final output of this code:

public function __construct(
#[Autowire(service: EsDataProviderBuildingComponent::class)]
private readonly EsDataProviderComponent $esDataProviderComponent,
) {
}

private function syncBuilding(Building $building)
{
$data = $this->esDataProviderComponent->getData($building);
dd($data);
//..
}

Result is:

array:4 [▼
"name" => "test name"
"address" => "test address"
"created_at" => "2024-02-11 12:18:37"
"id" => 1
]

The name and address were added from the EsDataProviderBuildingComponent.

The created_at was added from the CreatedAtEsDataProviderDecorator

The id was added from the IdentifierEsDataProviderDecorator

In the constructor, Symfony’s service container injects exactly what we need:

new IdentifierEsDataProviderDecorator(new CreatedAtEsDataProviderDecorator(new EsDataProviderBuildingComponent()));

EsDataProviderBuildingComponent concrete component was decorated by CreatedAtEsDataProviderDecorator and IdentifierEsDataProviderDecorator and that is what we received in the constructor.

That’s all I hope you enjoyed!

--

--

Filip Horvat

Senior Software Engineer, Backend PHP Developer, Located at Croatia, Currently working at myzone.com