DEV Community

Benjamin Delespierre
Benjamin Delespierre

Posted on

How to implement Clean Architecture with Laravel

Uncle Bob's Clean Architecture is quite the hype right now in the architects' world. But when it comes to actual implementations, nothing notable has been proposed for Laravel.

And that's understandable: Laravel's MVC architecture and its tendency to let you cross layers all the time using Facades doesn't help designing clean, decoupled software parts.

So today, I'm going to present you a working implementation of the Clean Architecture principles inside a Laravel app, as explained in The Clean Architecture by Robert C. Martin.

The complete, working implementation of the concepts explained here is available on my GitHub repository. I recommend you have a look at the actual code while reading this article.

For once, let's get our hands clean 👍

It all started with a diagram

image

The architecture must support the use cases. [...] This is the first concern of the architect, and the first priority of the architecture.

(The Clean Architecture chapter 16, p148.)

If you've never heard of use cases, you can think of it as a feature, the capacity of a system to do something meaningful. UML let you describe them using the well-named Use Case Diagrams.

In CA, use-cases are at the heart of the application. They're the microchip that controls the machinery of your app.

So, how are we supposed to implement those use cases then?

Glad you asked! Here's a second diagram:

image

Let me explain briefly, and we'll dive into the actual code.

The pink line is the flow of control; it represents the order in which the different components are being executed. First, the user changes something on the view (for instance, he submits a registration form). This interaction becomes a Request object. The controller reads it and produces a RequestModel to be used by the UseCaseInteractor.

The UseCaseInteractor then does its thing (for instance, creates the new user), prepares a response in the form of a ResponseModel, and passes it to the Presenter. Which in turn updates the view through a ViewModel.

Wow, that's a lot 😵 That's probably the main criticism made to CA; it's lenghty!

The call hierarchy looks like this:

Controller(Request)
  ⤷ Interactor(RequestModel)
      ⤷ Presenter(ResponseModel)
          ⤷ ViewModel
Enter fullscreen mode Exit fullscreen mode

What about the ports?

I can see you're quite the observer! For the low lever layers (the Use Cases and the Entities, often referred to as the Domain, and represented as the red and yellow circles in the schema above) to be decoupled from the high-level layers (the framework, represented as the blue circle), we need adapters (the green circle). Their job is to convey messages between high and low layers using their respective API and contracts (or interfaces).

Adapters are absolutely crucial in CA. They guarantee that changes in the framework won't require changes in the domain and vice-versa. In CA, we want our use cases to be abstracted from the framework (the actual implementation) so that both can change at will without propagating the changes on other layers.

A traditional PHP/HTML application designed with clean architecture can therefore be transformed into a REST API only by changing its controllers and presenters - the Use Cases would remain untouched! Or you could have both HTML + REST side by side using the same Use Cases. That's pretty neat if you ask me 🤩

To do that, we need to "force" the adapter to "behave" the way each layer needs it to behave. We're going to use interfaces to define inputs and output ports. They say, in essence, "if you want to talk to me, you're going to have to do it this way!"

Blah blah blah. I want to see some code!

Since the UseCaseInteractor will be at the heart of everything, let's start with this one:

class CreateUserInteractor implements CreateUserInputPort
{
    public function __construct(
        private CreateUserOutputPort $output,
        private UserRepository $repository,
        private UserFactory $factory,
    ) {
    }

    public function createUser(CreateUserRequestModel $request): ViewModel
    {
        /* @var UserEntity */
        $user = $this->factory->make([
            'name' => $request->getName(),
            'email' => $request->getEmail(),
        ]);

        if ($this->repository->exists($user)) {
            return $this->output->userAlreadyExists(
                new CreateUserResponseModel($user)
            );
        }

        try {
            $user = $this->repository->create(
                $user, new PasswordValueObject($request->getPassword())
            );
        } catch (\Exception $e) {
            return $this->output->unableToCreateUser(
                new CreateUserResponseModel($user), $e
            );
        }

        return $this->output->userCreated(
            new CreateUserResponseModel($user)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

There are 3 things we need to pay attention to here:

  • The interactor implements the CreateUserInputPort interface,
  • The interactor depends on the CreateUserOutputPort,
  • The interactor doesn't make the ViewModel himself, instead it tells the presenter to do it,

Since the Presenter (abstracted here by CreateUserOutputPort) is located in the adapters (green) layer, calling it from the CreateUserInteractor is indeed an excellent example of inversion of control: the framework isn't controlling the use cases, the use cases are controlling the framework.

If you find it too boringly complicated, forget all that and consider that all the meaningful decisions are being made at the use case level - including choosing the response path (userCreated, userAlreadyExists, or unableToCreateUSer). The controller and the presenters are just obedient slaves, devoid of business logic.

We can never rehearse it enough so sing it with me: CONTROLLERS 👏 SHOULD 👏 NOT 👏 CONTAIN 👏 BUSINESS 👏 LOGIC 👏

So how does it look from the controller's perspective?

For the controller, life is simple:

class CreateUserController extends Controller
{
    public function __construct(
        private CreateUserInputPort $interactor,
    ) {
    }

    public function __invoke(CreateUserRequest $request)
    {
        $viewModel = $this->interactor->createUser(
            new CreateUserRequestModel($request->validated())
        );

        return $viewModel->getResponse();
    }
}
Enter fullscreen mode Exit fullscreen mode

You can see it relies on the CreateUserInputPort abstraction instead of the actual CreateUserInteractor implementation. It gives us the flexibility to change the use case at will and make the controller testable. More on that later.

Okay, that's very simple and stupid indeed. What about the presenter?

Again, very straightforward:

class CreateUserHttpPresenter implements CreateUserOutputPort
{
    public function userCreated(CreateUserResponseModel $model): ViewModel
    {
        return new HttpResponseViewModel(
            app('view')
                ->make('user.show')
                ->with(['user' => $model->getUser()])
        );
    }

    public function userAlreadyExists(CreateUserResponseModel $model): ViewModel
    {
        return new HttpResponseViewModel(
            app('redirect')
                ->route('user.create')
                ->withErrors(['create-user' => "User {$model->getUser()->getEmail()} alreay exists."])
        );
    }

    public function unableToCreateUser(CreateUserResponseModel $model, \Throwable $e): ViewModel
    {
        if (config('app.debug')) {
            // rethrow and let Laravel display the error
            throw $e;
        }

        return new HttpResponseViewModel(
            app('redirect')
                ->route('user.create')
                ->withErrors(['create-user' => "Error occured while creating user {$model->getUser()->getName()}"])
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Traditionally, all that code would have been ifs at the controller's end. Which would have forced the use case to find a way to "tell" the controller what happened (using $user->wasRecentlyCreated or by throwing exceptions, for example.)

Using presenters controlled by the use case allows us to choose and change the outcomes without touching the controller. How great is that?

So everything relies on abstractions, I imagine the container is going get involved at some point?

You're absolutely right, my good friend! It pleases me to be in good company today.

Here's how to wire all that in app/Providers/AppServiceProvider.php:

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // wire the CreateUser use case to HTTP
        $this->app
            ->when(CreateUserController::class)
            ->needs(CreateUserInputPort::class)
            ->give(function ($app) {
                return $app->make(CreateUserInteractor::class, [
                    'output' => $app->make(CreateUserHttpPresenter::class),
                ]);
            });


        // wire the CreateUser use case to CLI
        $this->app
            ->when(CreateUserCommand::class)
            ->needs(CreateUserInputPort::class)
            ->give(function ($app) {
                return $app->make(CreateUserInteractor::class, [
                    'output' => $app->make(CreateUserCliPresenter::class),
                ]);
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

I added the CLI variant to demonstrate how easy it is to swap the presenter to make the use case return different ViewModel instances. Have a look a the actual implementation for more details 👍

Can I test this?

Oh my! It's begging you to! Another good thing about CA is that it relies so much on abstractions it makes testing a breeze.

class CreateUserUseCaseTest extends TestCase
{
    use ProvidesUsers;

    /**
     * @dataProvider userDataProvider
     */
    public function testInteractor(array $data)
    {
        (new CreateUserInteractor(
            $this->mockCreateUserPresenter($responseModel),
            $this->mockUserRepository(exists: false),
            $this->mockUserFactory($this->mockUserEntity($data)),
        ))->createUser(
            $this->mockRequestModel($data)
        );

        $this->assertUserMatches($data, $responseModel->getUser());
    }
}
Enter fullscreen mode Exit fullscreen mode

The complete test class is available here.

I use Mockery for, well, mocking, but it will work with anything. It might seem like a lot of code, but it's actually quite simple to write, and it will give you 100% coverage of your use cases effortlessly.

Isn't this implementation slightly different from the book?

Yes, it is. You see CA has been designed by Java people. And, in most cases, in a Java program, if you want to update the view, you can do so directly from the Presenter.

But not in PHP. Because we don't fully control the view and because the frameworks are structured around the concept of controllers returning a response.

So I had to adapt the principles and make the ViewModel climb the call stack up to the controller to return a proper response. If you can come up with a better design, please let me know in the comments 🙏


Would you please let me know what you think in the comments? Your opinion matters to me, for I write those articles to challenge my vision and learn new things every day.

You are, of course, welcome to suggest changes to the demo repository by submitting a pull-request. Your contribution is much appreciated 🙏

This article took me four days of research, implementation, testing, and writing. I would really appreciate a like, a follow, and maybe a share on your social networks 🙏

Thanks, guys, you contribution helps to keep me motivated to write more articles for you 👍


Further reading:

Discussion (26)

Collapse
waynethorley profile image
Wayne Thorley

🤔 Theoretically it sounds great. But for something "clean" it's hugely verbose. Real-world I can see it adding unnecessary time and complexity to a project, compared to something like single action classes - which are still testable and keeps business logic out of the controllers.

Collapse
bdelespierre profile image
Benjamin Delespierre Author

Hey @waynethorley thanks for your comment 👍

The complexity is the price to pay for layer isolation. It is tempting to simply relocate business logic from controller to actions (that's what JetStream is doing).

But without proper isolation one of these two scenarios will inevitably happen:

Scenario 1 : the presentation logic will leak to the controller

class CreateUserController
{
    public function __invoke(CreateUserRequest $request)
    {
        try {
            $user = app(CreateUserAction::class)->handle($request->validate());
        } catch (UserAlreadyExists $e) {
            return redirect()->back()->with('error', "User already exists!");
        } catch (Exception $e) {
            return redirect()->back()->with('error', "Unexpected error");
        }

        if ($user->wasRecentlyCreated) {
            return view('user.show', compact('user'));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It might be fine for simple presentation logic but as your project grows, more logic will "leak" this way to the controller, defeating the purpose of isolating things in an action.

Scenario 2 : the response logic will leak into the use-case

class CreateUserController
{
    public function __invoke(CreateUserRequest $request)
    {
        return app(CreateUserAction::class)->handle($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

In that case, the action will have to take care of choosing the view response itself, making it effectively a controller. Again, defeating the purpose...

So you could just add a presenter between the controller and the action like so:

class CreateUserController
{
    public function __invoke(CreateUserRequest $request)
    {
        return app(CreateUserPresenter::class)->present(
            app(CreateUser::class)->handle($request);
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

And that's pretty much the same thing as the implementation I proposed, structured differently and without the interfaces, but very much the same concept (also I didn't bother mentionning the request/response/view model but you get the gist at that point.)

I encourage you to play a bit with these concepts and find the right balance for your projets. It is true that CA is complex. It is intended for large scale enterprise application software and require experts on your team to operate effectively and deliver the promised value.

It is not fitted for every project. It is not a silver bullet. Nothing is.

Collapse
altafhussain10 profile image
Altaf Hussain

Thank you for the article. In this implementation, framework is isolated but I can see a lot of complexities, which will cost a lot of time for adding any new use cases. Too many moving parts are involved.
I hope to see an updated implementation which is more simple compared to this one.

Collapse
bdelespierre profile image
Benjamin Delespierre Author

Yes, actually I plan on writing an article on how to structure use cases using Action, after I've finished the book Laravel Beyond CRUD. Follow me to stay tuned 👍

Collapse
myckhel profile image
Michael Ishola

Im seeing complexity here.
I hope the proposal is not introducing complexity to the framework.

Collapse
samnowakowski profile image
Sam Nowakowski

It is not about the framework. It's about letting the framework be a detail of the implementation - domain and use cases separate from the framework. For a deeper look, I can recommend the talk by Matthias Noback on Hexagonal Architectures.

Collapse
bdelespierre profile image
Benjamin Delespierre Author

The framework is left untouched. This implementation is completely compatible with the rest of the Laravel ecosystem

Collapse
bernardwiesner profile image
Bernard Wiesner • Edited on

Seems like overengineering for 95% of the apps out there.

I find many oop and design patterns add a lot of complexity with little to no benefit.

I am a big believer in YAGNI (you are not going to need it). Only implement design patterns when you have an immediate need of it, not just in case you might need it later or cause you want to just follow some best practice.

A big reason Laravel got so successful is because it makes things easy to read and keeps things simple.

I would say if you are working on some vendor package, or you are working in a very big team where you don't have influence on other development teams, then these oop and design patterns can be quite handy.

Collapse
bdelespierre profile image
Benjamin Delespierre Author • Edited on

Thanks for your comment Bernard.

As I quote in the article

The architecture must support the use cases. [...] This is the first concern of the architect, and the first priority of the architecture.

If Laravel's native architecture can support your use cases, which is likely to be the case for small / mid size or early project, then yes, maybe you don't need Clean Architecture.

As the system's architect (apointed or de-facto) it is your responsibiliy to pick the best architecture you can given the project, its scope, its constraints, and its team.

If CA is overkill, then pick something simpler 👍 Just make sure you can migrate later as the project grows.

Collapse
ngodinhcuong profile image
Ngo Dinh Cuong • Edited on

Thank you for your sharing. It's really interested post!
I had created lara-repository to automatically generate files (Interface, Repository, Model, Controller) for Laravel repository structure. I hope you can take a look and give some feedbacks
We can share and learn together. Thank you so much!
dev.to/ngodinhcuong/auto-generated...

Collapse
bdelespierre profile image
Benjamin Delespierre Author

Hey @ngodinhcuong thanks for your comment 👍

Sure I’ll have a look 👀

Collapse
emperorkonstantin profile image
Konstantin Anthony

I'm curious how you'll mitigate class AppServiceProvider bloat?

Also, isn't creating so many classes for every CRUD single operation a bit much? I think more thought should be lent here, but I've yet to read the Clean Architecture book, so I would like to hear your thoughts.

Collapse
bdelespierre profile image
Benjamin Delespierre Author

Hi @emperorkonstantin , thanks for your message 👍

On real projects I use many bounded contetxts and I create service providers for each of them.

Also, a use case is much more than a part of a CRUD and, on large scale projects, deserves a lot more attention. I don't believe most apps are CRUD, despite the apparences, especially in the early days of the project. but if it's indeed the case and what you have is a genuine CRUD app then 1. why are you using Laravel instead of a serverless solution like Firebase, which would make the CRUD operations for you and 2. a CRUD is little more than a scaffolder to reach the database layer from the view, it's very simple and doesn't need Clean Architecture.

Despite what is often say about CA, I don't believe it should be applied everywhere. It's indeed complex and involves a lot of abstraction, which is not for every project nor for every team. As an architect, it's you responsibility to make those tradeoffs.

Collapse
samnowakowski profile image
Sam Nowakowski

Thank you for your effort and this insight into this type of architecture. In your CreateUserInteractor, the method seems quite long. Doesn't that violate Uncle Bob's Do One Thing rule?

Collapse
bdelespierre profile image
Benjamin Delespierre Author

I don’t believe it does. Taking care of all the steps involved in account creation (in the example I used) can be considered one thing, it’s an account creation.

Plus, CA advocates against UC calling each other; they are vertical “slices” in your application layers and therefore should be isolated to avoid crossing boundaries.

This point in particular seems open to interpretation though and it’s a frequent topic on Stack Overflow.

DDD answers the issue of crossing boundaries with the notion of bouded contexts. You can read more about that on Martin Fowler’s blog.

At the end of the day, I believe you have the right, as an architect, to choose the trade offs that suits you the best 👍

Collapse
phcostabh profile image
Philippe Santana Costa

I'm on board with @bdelespierre here. If you think from the perspective of reasons to change the CreateUserInteractor is not violating SRP at all.

Collapse
phcostabh profile image
Philippe Santana Costa

As you mention the collaboration between interactors, what's your appoach in this cases? How do you share behavior between interactors without having them call each other?

Thread Thread
bdelespierre profile image
Benjamin Delespierre Author

Behavior shared amongst use cases should go into shared services if and only if it's not incidental dupplication.

Collapse
refuse profile image
Artur Wasilewski • Edited on

Hello,

wouldn't it be better if the interactor returned new CreateUserResponseModel($user)
and then controller, command, etc. could decide on the type of response? Those layers could also get injected with a proper object handling the response (Presenter). It looks a bit weird that InputPort "knows" about OutputPort.

Collapse
bdelespierre profile image
Benjamin Delespierre Author • Edited on

It can, but it wouldn't adhere to the specifications of this diagram:

With your proposal, the flow of control would return in the controller for it to make the decision on how to update the view, forcing you to change it when the use case changes.

In summary, you absolutely can, but it's no longer Clean Architecure, it's Laravel Actions.

I also don't believe it's weird to have the Interactor knowing the output port while implementing the input port since it should control both.

Collapse
tiagodevweb profile image
Tiago Lopes

Another issue that I find interesting is authentication and authorization, which is more pleasant, a simple if in the interactor or polymorphism of the interactor for each role of the system?

Collapse
bdelespierre profile image
Benjamin Delespierre Author

Can you provide examples for these?

Collapse
tiagodevweb profile image
Tiago Lopes • Edited on

Yes,
In a post creation case where the authenticated user can be a simple author or a moderator, where should publish permission be checked?

With a conditional in the interactor:

 <?php

 class CreatePostInteractor implements CreatePostInputPort
 {
       public function __construct(
          private CreatePostOutputPort $output,
          private PostRepository $repository,
          private PostFactory $factory,
          private IAM $iam
        ) {}

      public function createPost(CreatePostRequestModel $request): ViewModel
      {
          /* @var PostEntity */
          $post = $this->factory->make([
             'title' => $request->getTitle(),
             'body' => $request->getBody(),
             'published' => null
          ]);

         if ($this->iam->hasRole($request->getUserAuth(), 'moderator')) {
             $post->publish();
         }

         if ($this->repository->titleExists($request->getTitle())) {
             return $this->output->titleAlreadyExists(
                 new CreatePostResponseModel($post)
            );
        }

        try {
             $user = $this->repository->create($post);
        } catch (\Exception $e) {
             return $this->output->unableToCreatePost(
                 new CreatePostResponseModel($user), $e
             );
        }

         return $this->output->postCreated(
              new CreatePostResponseModel($user)
         );
     }
 }
Enter fullscreen mode Exit fullscreen mode

With derivations for each type of user:

 <?php

 abstract class AbstractCreatePostInteractor
 {
       public function __construct(
           private CreatePostOutputPort $output,
           private PostRepository $repository,
           private PostFactory $factory
        ) { }

       abstract protected function makePost(CreatePostRequestModel $request): PostEntity;

       public function createPost(CreatePostRequestModel $request): ViewModel
       {
             /* @var PostEntity */
             $post = $this->makePost($request);

            if ($this->repository->titleExists($request->getTitle())) {
                 return $this->output->titleAlreadyExists(
                     new CreatePostResponseModel($post)
                 );
            }

            try {
                  $user = $this->repository->create($post);
            } catch (\Exception $e) {
                  return $this->output->unableToCreatePost(
                       new CreatePostResponseModel($user), $e
                  );
            }

            return $this->output->postCreated(
                  new CreatePostResponseModel($user)
            );
       }
  }

  class CreatePostInteractor extends AbstractCreatePostInteractor implements CreatePostInputPort
  {

        protected function makePost(CreatePostRequestModel $request): PostEntity
        {
              return $this->factory->make([
                 'title' => $request->getTitle(),
                 'body' => $request->getBody(),
                 'published' => null
         ]);
     }
  }

 class CreatePostInteractorAsModerator extends AbstractCreatePostInteractor implements CreatePostInputPort
 {

      protected function makePost(CreatePostRequestModel $request): PostEntity
      {
              return $this->factory->make([
                 'title' => $request->getTitle(),
                 'body' => $request->getBody(),
                 'published' => new \DateTimeImmutable()
      ]);
   }
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
bdelespierre profile image
Benjamin Delespierre Author • Edited on

Both approaches could work just fine IMHO. The second one is better suited if the two roles have a lot of differences in their use-cases, justifying the existence of two distinct use-cases.

I also believe the keyword interactor should be a suffix of the class like CreatePostAsModeratorInteractor.

Also note that the interactor returning the entity directly is not a recommendation of the Clean Architecture as it will inevitably introduce coupling between the interactor and its callers.

Collapse
tiagodevweb profile image
Tiago Lopes

Congratulations for the post, my question is in the case of a REST API, the viewmodel would be needed? If not what would be the default return type for presenter methods?

Collapse
bdelespierre profile image
Benjamin Delespierre Author • Edited on

I believe in the case of REST API, the ViewModel would be a Laravel Resource wrapper:

class CreateUserJsonPresenter implements CreateUserOutputPort
{
    public function userCreated(CreateUserResponseModel $model): ViewModel
    {
        return new JsonResourceViewModel(
            new Resources\UserCreatedResource($model->getUser())
        );
    }

    public function userAlreadyExists(CreateUserResponseModel $model): ViewModel
    {
        return new JsonResourceViewModel(
            new Resources\UserAlreadyExistsResource($model->getUser())
        );
    }

    public function unableToCreateUser(CreateUserResponseModel $model, \Throwable $e): ViewModel
    {
        if (config('app.debug')) {
            // rethrow and let Laravel display the error
            throw $e;
        }

        return new JsonResourceViewModel(
            new Resources\UnableToCreateUserResource($e)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

I'll update the reference project with examples for a REST API and write another article about it. Stay tune!