How to apply Hexagonal/Clean/Onion architecture with only two changes to your symfony project

a proposal on how to follow Hexagonal/Clean/Onion architecture in symfony with minimum changes possible

Saeid Raei
6 min readAug 12, 2022

If we look into history of software engineering there are multiple solutions proposed to us around the similar concept (Hexagonal/Clean/Onion architecture) lets see some of them:

All these ideas evolve around the similar concept that has been reinvented or improved over time, but the “Clean architecture” has considered all of them so we are gonna focus on that and refer to them as “Clean architecture”.

Clean architecture extreme TL;DR : basically a way to separate a software into pieces that are layered like the image above, then we make our software communicate only from outer layers inward, not the other way around, for example Use Cases can access to Entities but not the Controllers, or Database layer can access the Entities but definitely not the other way around (how eloquent does it!). one concrete example: it’s OK to do $post = new Post(); which the Post is our Entity, but is not OK to do $doctrine->getRepository(Post::class)inside the Entity. we are not gonna go into details of this and just focus on simplicity.

The problem : The implementations of these ideas gets complicated and have no convention to it

If you look at the implementation examples of these ideas to get inspired you would face multiple complex implementations that almost every one of them have a unique structure and almost none of them look the same.

This mess makes me give more credit to the people that are heavily against these ideas. I thought to myself how can we fix this? why we don’t have a convention for this? is it even possible to have a simple structure following “Clean Architecture”?

This thoughts was with me for months then I clicked with a possible solution; what if we take a well structured framework like symfony and make it follow these ideas with the minimum changes possible? who said that we have to follow all the terminology that have been proposed to us in these ideas?

We can just take the idea itself and make our framework follow that idea. so here’s my proposal for making your symfony project follow these ideas:

The proposal (see the github repository for more details)

Clean architecture tailored for symfony. (other layers like presentation and adapters can be added but I’m trying to focus on simplicity here)

Only two changes are needed to a normal symfony project:

  1. use an UseCase class to do whatever you’re doing in your controller methods.
  2. make your repositories implement an interface and only use that interface for dependency injection into your use cases.(for more details refer to Repository pattern the SOLID way)

If you’re using any other external api the UseCase should only use abstraction instead of the implementation just like the Repositories.

Here’s what the structure would look like:

Now lets checkout one example, Here’s what our Enity looks like:

<?phpnamespace App\Entity;use App\Repository\PostRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $body = null;
... setters and getters ... public function toArray():array
return [

as you can see the database related information is provided as attributes which acts as metadata and doesn’t affect our Entity itself , then our entity is a plain Class that doesn’t use any outer layers.

Here’s what the controller’s update method looks like:

#[Route('/posts/{id}', name: 'post_update', methods: ['PUT'])]
public function update(int $id, Request $request, UpdateUseCase $updateUseCase): JsonResponse
$post = new Post();
return $this->json($updateUseCase->execute($post));

Symfony’s auto-wire was used here to inject the UseCase, you should make sure to do not put any business logic here and move them to the UseCase,

Now lets see the UseCase:


namespace App\UseCase\Post;

use App\Entity\Post;
use App\Repository\PostRepositoryInterface;

final class UpdateUseCase
protected PostRepositoryInterface $postRepository;

public function __construct(PostRepositoryInterface $postRepository)
$this->postRepository = $postRepository;

public function execute(Post $post): array
return $this->postRepository->update($post)->toArray();

Here the PostRepository is being provided with symfony’s dependency injection (auto-wire) and we are just calling the repository’s update method that is defined on the Repository’s Interface, just remember that if any external resources (for example cache) was needed in this stage access them through Interfaces (abstraction) instead of the implementation using Dependency injection.

Here’s our repository’s interface:


namespace App\Repository;

use App\Entity\Post;

interface PostRepositoryInterface{
public function getAll():array;
public function add(Post $post): ?Post;
public function findOneById(int $id):?Post;
public function update(Post $post): ?Post;
public function delete(int $id): bool;

We have a Doctrine implementation for this interface which is configured to get injected using symfony’s dependency injection. we also have a Fake implementation for testing purposes.(see the using Repository pattern without connecting to database for more detailed explanation)

Now lets take a look at how the test would look like:

class UpdateUseCaseTest extends TestCase{
public function testExecuteNormalCase()
$postRepository = new PostRepository();
$indexUseCase = new UpdateUseCase($postRepository);
$post = new Post();
$post->setTitle('new test title');
$post->setBody('new test body');

$fetchedPost = $postRepository->findOneById(1);
$this->assertEquals('new test title',$fetchedPost->getTitle());
$this->assertEquals('new test body',$fetchedPost->getBody());

as you can see we are testing our business logic without having to send a http request.

Some would say this is oversimplified and could not represent Clean architecture or any other of the ideas, but why can’t we just take the mindset and purpose of those ideas and apply them to our project? Does this small changes seriously provide us with the benefits of those ideas?(doubt it!)
In order to evaluate this proposal lets refer to the Uncle bob’s blog post:

Each of these architectures produce systems that are:

1. Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.

2. Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.

3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.

4. Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.

5. Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.

  1. The only feature that this proposal doesn’t fully provide is this one that happens to be the first one here, but we have accepted the fact that we are using symfony in the first place and even if we decide to change our framework we’ll definitely need less changes than the normal way of doing things. after all there is trade-off between having a complex implementation that everyone on team needs to agree on or having a simple solution that doesn’t fully provide this feature but provides all of other ones. one good example is the dependency injection feature of symfony that we shouldn’t use in order to be independent of the framework which make our code base unnecessary bigger than when we have the dependency injection, I’d say one side of this trade-off has more weight to it.
  2. We have discussed the testing in our example.
  3. We are definitely independent of the UI, I think this point makes more sense in other forms of applications like windows application which the business logic can easily get coupled with the UI.
  4. We’ve achieved this using the Repository Pattern(but in the SOLID way!)
  5. As I talked about this before by just using dependency injection with abstraction this can be easily achieved.

Thank you for reading, if you have any problems or improvements in mind let me know.