Implementing Builder Pattern in PHP and Symfony like a boss!

Stefano Alletti
4 min readNov 10, 2022

--

Builder

I would like to show you my implementation of the very popular Pattern Builder in PHP using the Symfony framework

I will not dwell on this pattern, because there are hundreds of articles that already do this.

We can simply say that:

Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

— Source: https://refactoring.guru/design-patterns/builder.

And the UML class diagram looks something like this:

Source: Wikipedia

As always, in my posts, I will give space to practice by directly showing the code I have implemented.

Scope

The scope is to build and render an object that represents a template (Environment) with parameters defined according to a specific type of object given in input.

Let’s imagine we have different types of objects, but for simplicity we consider only two, Article and Flash.
It doesn’t matter what these two Entities represent, the only thing you need to know is that they are two objects that need different twig templates with different parameters and also that both extend an abstract Content class.

But since I don’t want to duplicate the controllers, I will only use one that will return the response with the desired template through my builder pattern.

Code

Controller

The controller uses the Director to get the template. It passes the Content which can be of type Article or Flash

// App/Controller/TestController.php

final class TestController extends AbstractController
{
...
private TemplateBuilderDirector $templateBuilderDirector;

public function __construct(
...
TemplateBuilderDirector $templateBuilderDirector
) {
...
$this->templateBuilderDirector = $templateBuilderDirector;
}


public function __invoke(Content $content): Response
{
....
....

$template = $this->templateBuilderDirector->createTemplate($content, ['someParameter' => 'parameterOne']);
if (!$template) {
throw new \Exception(\sprintf('template not found for content with id %s', $content->getId()));
}

return new Response($template);
}
}

Director

The director is in charge of calling the concrete builder methods used to create the final result. All the concrete builders are injected into it. They implement a specific interface. In this case the TemplateBuilderManagerInterface. To do this I use the Symfony’s Service Tag.

Configuration file:

#config/services.yaml

_instanceof:
....
....
App\Services\Content\TemplateBuilder\Manager\TemplateBuilderManagerInterface:
tags: ['app.template.builder.manager']
....
....

App\Services\Content\TemplateBuilder\TemplateBuilderDirector:
$builderManagers: !tagged_iterator app.template.builder.manager

Interface:

//App\Services\Content\TemplateBuilder\Manager\TemplateBuilderManagerInterface.php

interface TemplateBuilderManagerInterface
{
public function supportContent(Content $content): bool;

public function createBuilder(Content $content): TemplateBuilder;

public function buildTemplateParameters(Content $model, array $context): array;

public function buildView(array $params): string;
}

Director :

//App\Services\Content\TemplateBuilder\TemplateBuilderDirector.php

final class TemplateBuilderDirector
{
private iterable $builderManagers;

public function __construct(iterable $builderManagers)
{
$this->builderManagers = $builderManagers;
}

public function createReadOnlyTemplate(Content $content, array $someParameters): ?string
{
/** @var TemplateBuilderManagerInterface $builderManager */
foreach ($this->builderManagers as $builderManager) {
if ($builderManager->supportContent($content)) {
$templateBuilder = $builderManager->createBuilder($content);

return $templateBuilder
->buildTemplateParameters($someParameters)
->buildView();
}
}

return null;
}
}

The Director iterates over the Concrete Builders and checks if it is the one to use. If it passes the check, it uses its methods to construct the object.

Concrete Builders

The concrete builders are the objects that build the final result and they implement the interface seen in the previous point.
Let’s analyze the methods step by step to understand how they work.

supportContent

Just check if the builder is the correct one:

//App\Services\Content\TemplateBuilder\Manager\ArticleManager.php

public function supportContent(Content $content): bool
{
return $content instanceof Article;
}

createBuilder

If the builder is supported then we use this method to create the builder (TemplateBuilder). This is a class in which we encapsulate the concrete builder itself.

//App\Services\Content\TemplateBuilder\Manager\ArticleManager.php

public function createBuilder(Content $content): TemplateBuilder
{
return new TemplateBuilder($this, $content);
}

This class is a further abstraction in the style of Doctrine’s QueryBuilder and the createQueryBuilder method.

//App\Services\Content\TemplateBuilder\Builder\TemplateBuilder.php

final class TemplateBuilder
{
private Content $model;
private array $params;

private TemplateBuilderManagerInterface $templateBuilderManager;

public function __construct(TemplateBuilderManagerInterface $templateBuilderManager, Content $content)
{
$this->templateBuilderManager = $templateBuilderManager;
$this->model = $content;
}

public function buildTemplateParameters(array $context): self
{
$this->params = $this->templateBuilderManager->buildTemplateParameters($this->model, $context);return $this;
}

public function buildView(): string
{
return $this->templateBuilderManager->buildView($this->params);
}
}

buildTemplateParameters

//App\Services\Content\TemplateBuilder\Manager\ArticleManager.php

public function buildTemplateParameters(Content $model, array $context): array
{
... some logic here ...

$customParameterOne = ...

return [
'customParameterOne' => $customParameterOne,
'customParameterTwo' => 'customParameterTwo',
'content' => $model,
'someParameterFromContext' => $context['someParameter'],
];
}

buildView

//App\Services\Content\TemplateBuilder\Manager\ArticleManager.php

private const TEMPLATE = 'contents/articles/article.html.twig';

public function __construct(Environment $twig)
{
....
....
}

public function buildView(array $params): string
{
return $this->twig->render(
self::TEMPLATE,
$params
);
}

The second concrete builder will build the template according to its specific type.

//App\Services\Content\TemplateBuilder\Manager\FlashManager.php

private const TEMPLATE = 'contents/articles/flash.html.twig';

public function __construct(Environment $twig)
{
....
....
}

....
....

public function supportContent(Content $content): bool
{
return Content instanceof Flash;
}

public function buildTemplateParameters(Content $model, array $context): array
{
... some logic here ...

$customParameterOne = ...

return [
'customParameterOne' => $customParameterOne,
'customParameterTwo' => 'customParameterTwo',
'customParameterThree' => 'customParameterThree',
'customParameterFour' => 'customParameterFour',
'customParameterFive' => 'customParameterFive',
'content' => $model,
'someParameterFromContext' => $context['someParameter'],
];
}

Conclusions

This pattern is really easy to implement, even more when you use the power of Symfony’s Service Tag.

If in the future, for example, you have a new Object and you want to build, for this object, the template in a custom way, just add a concrete builder that will have to implement the interface defined in your config.

Is it easy to understand and helps to comply with the SOLID:

- SRP: Each builder is responsible for building the final result.
- OCP: You will never modify existing builders, but you will add a new builder for each new type.
- ISP: All interface methods are always used by your director.
- DIP: your director will always depend on the interface implemented by the concrete builders.

--

--