POC of Clean Architecture with Symfony

Stefano Alletti
5 min readDec 27, 2022

--

Introduction

I will soon be changing jobs and the clean architecture is used in the new company. So I decided to experiment with it by making a POC.
Maybe it can inspire someone. Or could someone help me to correct any errors in my approach.

As far as I could understand there are two main approaches in clean architecture. The first involves inserting the entire framework in the infrastructure, the second instead involves inserting only some parts of it in the infrastructure directory. It contains essential infrastructure details such as the code that interacts with your database, makes calls to the file system, or code that handles HTTP calls to other applications on which you depend for example (like Hexagonal Architecture).
In this POC I will use the first approach.

The example I have implemented is simply a user registration, which can be done via a form or via an API and the business logic is shared between the two.

Organization of directories

The directories are organized in a standard way: Domain, Infrastructure, Presentation.
In the first we find all the business logic, organized by BoundedContext, in the second, the Infrastructure, we put everything concerning the framework, in this case Symfony, and in the Presentation directory everything concerning the data presenting.

src
|-- Domain
| `-- User (BoundedContext)
| |-- Entity
| | |-- ...
| |-- Service
| | |-- ...
| |-- Repository
| | |-- UserRegisterRepositoryInterface.php
| |-- UserCase
| | |-- Register
| | | |-- RegisterUserRequest.php
| | | |-- RegisterUserResponse.php
| | | |-- RegisterUserUseCase.php
| | | |-- RegisterUserPresenterInterface.php
| `-- Exception
| |-- ....
|-- Infrastructure
| `-- Symfony
| |-- Controller
| | |-- ...
| |-- Form
| | |-- ...
| |-- Repository
| | |-- ...
| |-- Entity
| | |-- ...
| |-- View
| | |-- RegisterHtmlView.php
| | |-- RegisterJsonView.php
| |-- ParamConverter
| | |-- FormToRegisterRequestConverter.php
| | |-- JsonToRegisterRequestConverter.php
| `-- ...
|-- Presentation
| `-- User
| |-- RegusterUserHtmlPresenter.php
| |-- RegusterUserJsonPresenter.php
| |-- RegusterUserHtmlViewModel.php
| |-- RegusterUserJsonViewModel.php

Code

The entry point is always the controller

<?php

declare(strict_types=1);

namespace App\Infrastructure\Symfony\Controller;
...
...

#[Route('/register', name: 'app_register')]
#ParamConverter(name="registerUserRequest", converter="FormToRegisterUserRequest")
final class RegisterUserController extends AbstractController
{
private RegisterHtmlView $registerView;
private RegisterUserUseCaseInterface $registerUseCase;
private RegisterUserHtmlPresenter $presenter;

public function __construct(
RegisterHtmlView $registerView,
RegisterUserUseCaseInterface $registerUseCase,
RegisterUserHtmlPresenter $presenter
) {
$this->registerView = $registerView;
$this->registerUseCase = $registerUseCase;
$this->presenter = $presenter;
}

public function __invoke(RegisterUserRequest $registerUserRequest): Response
{
$this->registerUseCase->execute($registerUserRequest, $this->presenter);

return $this->registerView->generateView(
$registerUserRequest,
$this->presenter->viewModel()
);
}
}

I use a converter (FormToRegisterUserRequest) to convert the http form data into a domain object named RegisterUserRequest. This DTO is passed to the execute method of UseCase with the Presenter object.

The Use Case has the task of applying the business logic using the DTO (request) and providing another DTO (response) to the Presenter.

<?php

declare(strict_types=1);

namespace App\Domain\User\UseCase\Register;

...
...

final class RegisterUserUseCase implements RegisterUserUseCaseInterface
{
private UserRepositoryInterface $userRepository;
private UserIsAlreadyRegistered $userIsAlreadyRegistered;

public function __construct(
UserRepositoryInterface $userRepository,
UserIsAlreadyRegistered $userIsAlreadyRegistered,
) {
$this->userRepository = $userRepository;
$this->userIsAlreadyRegistered = $userIsAlreadyRegistered;
}

public function execute(
RegisterUserRequest $registerRequest,
RegisterUserPresenterInterface $presenter
): void {
$registerResponse = new RegisterUserResponse();


//... other business logic here ...

try {
$user = $this->saveUser($registerRequest);
$registerResponse->setUser($user);
} catch (UserAlreadyRegisteredException $exception) {
$registerResponse->setViolations([['email' => $exception->getMessage()]]);
}
}

$presenter->present($registerResponse);
}

private function saveUser(RegisterUserRequest $registerRequest): User
{
if ($this->userIsAlreadyRegistered->isSatisfiedBy($registerRequest->email)) {
throw UserAlreadyRegisteredException::withEmail($registerRequest->email);
}

$user = User::createUser(
$registerRequest->id,
$registerRequest->email,
$registerRequest->password,
$registerRequest->firstName,
$registerRequest->lastName
);
$this->userRepository->add($user);

return $user;
}
}

The present method called here is responsible for constructing the View Model used by the Controller.

<?php

declare(strict_types=1);

namespace App\Presentation\User;

use App\Domain\User\UseCase\Register\RegisterUserPresenterInterface;
use App\Domain\User\UseCase\Register\RegisterUserResponse;

final class RegisterUserHtmlPresenter implements RegisterUserPresenterInterface
{
private RegisterUserHtmlViewModel $viewModel;

public function present(RegisterUserResponse $response): void
{
$this->viewModel = new RegisterUserHtmlViewModel();
$this->viewModel->email = $response->getUser()?->getEmail();
$this->viewModel->violations = $response->getViolations();
}

public function viewModel(): RegisterUserHtmlViewModel
{
return $this->viewModel;
}
}

The controller use a service (RegisterHtmlView) for generate the Response view. This service uses the view model created by the presenter.

<?php

declare(strict_types=1);

namespace App\Infrastructure\Symfony\View;

...

final class RegisterHtmlView
{
private Environment $twig;
private FormFactoryInterface $formFactory;

public function __construct(
Environment $twig,
FormFactoryInterface $formFactory
) {
$this->twig = $twig;
$this->formFactory = $formFactory;
}

public function generateView(
RegisterUserRequest $registerUserRequest,
RegisterUserHtmlViewModel $viewModel
): Response {
if (!$viewModel->violations && $registerUserRequest->isPosted) {
return new Response($this->twig->render(
'user/register_complete.html.twig',
[
'viewModel' => $viewModel
]
));
}

$form = $this->formFactory->createBuilder(RegisterUserType::class, $registerUserRequest)->getForm();

return new Response($this->twig->render(
'user/register.html.twig',
[
'form' => $form->createView(),
'viewModel' => $viewModel
]
));
}
}

It should be noted that in this case the response DTO and the view model built from this DTO contain only two properties, the email of the registered user or the errors.
This is because in the confirmation template I display a simple message with the registration email, or in case of error the error message(s). Obviously in case of more complex templates these objects will have to contain more properties.

{# user/register_complete.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
User with email {{ viewModel.email }} registered
{% endblock %}
{# user/register.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
{% block errors %}
{% for violation in viewModel.violations %}
{% for path, message in violation %}
{{ path }} : {{ message }} <br />
{% endfor %}
{% endfor %}
{% endblock %}

{{ form(form) }}
{% endblock %}

Now if we want to add registration via API just create the following elements:

- ApiRegisterUserController: The entry point.
- JsonToRegisterUserRequest: The converter of Json dato in to the request object (RegisterUserRequest).
- RegisterUserJsonPresenter: The Presenter used by the UseCase to build the view model.
- RegisterUserJsonViewModel: The view model used to provide the data to the user.
- RegisterJsonView: The service used to generate the response.

The complete POC code can be found here.

Conclusions

The benefits of Clean Architecture are well summed up by its inventor Uncle Bob:

Conforming to these simple rules is not hard, and will save you a lot of headaches going forward. By separating the software into layers, and conforming to The Dependency Rule, you will create a system that is intrinsically testable, with all the benefits that implies. When any of the external parts of the system become obsolete, like the database, or the web framework, you can replace those obsolete elements with a minimum of fuss.

As a novice I would like to have some advice or some modification suggestions regarding this POC.

Thank you for reading me, I hope you enjoyed this article, you can follow me on Twitter or LinkedIn.

References:

--

--

Stefano Alletti

Freelance web developer 🇮🇹 🇫🇷🇬🇧