By @ryan · 2021-10-17 23:03 (edited)
Most web application frameworks use the Model-View-Controller (MVC) pattern. This pattern was first introduced in the 70s and has been able to stand the test of time.
Based on the introduction date, it's clear that it was never designed or architected for web development. The web wasn't really a thing until the late 80s / early 90s.
Despite this, it has been the defacto way to build a server-driven web application for years and no doubt will be for years to come.
Outside of the MVC world there are plenty of other design patterns, but they're often unused or rarely mentioned in the Laravel universe.
I'd like to show you how to use the Action-Domain-Responder (ADR) pattern in a Laravel application and how it differs from the traditional MVC pattern.
Action
?When it comes to ADR, an Action
class is comparable to a single-action controller.
Each Action
should only handle a single action in your application.
Imagine you're building a "show" page for a User
. In a typical Laravel application your route definition might look something like this:
Route::get('/users/{user}', [UsersController::class, 'show']);
You have a UsersController
that has multiple methods, each handling a different route.
You could also have a dedicated
ShowUserController
(or similar).
If we were to convert this route into an Action
, we would do something like this:
Route::get('/users/{user}', ShowUserAction::class);
Action
classes get defined?A normal Laravel application would store Controller
classes in app/Http/Controllers
.
Since we're using ADR, we can group our business-logic more tightly and use a more domain-driven (the D
in ADR) folder structure.
In this instance I'd create an app/Users
folder. This Users
folder is going to store all of the components related to the user-oriented parts of my application.
The Action
class could then be created in a new file app/Users/Actions/ShowUserAction.php
.
Action
need to have?To use an Action
class for a route, you need to define an __invoke
method on the class.
namespace App\Users\Actions;
class ShowUserAction
{
public function __invoke()
{
//
}
}
When you hit the /users/{user}
endpoint, this method will be invoked.
Action
generate a Response
?Now that we know what Action
classes are, we can look at returning Response
objects.
The letter R
in ADR stands for Responders
. As the name suggests, these classes are responsible for generating a Response
object.
Responder
classes get stored?Similar to Action
classes, we can create a dedicated folder for Responder
classes.
Let's go with app/Users/Responders
.
For our /users/{user}
route, we should probably create a new ShowUserResponder
class too.
namespace App\Users\Responders;
class ShowUserResponder
{
}
Responder
need to have?The naming conventions here are completely up to you, but I generally create a respond
method to generate the response.
namespace App\Users\Responders;
use Illuminate\Http\Response;
class ShowUserResponder
{
public function respond()
{
}
}
Other methods names I've seen include send
, generate
and even __invoke
.
Now that we've created our Responder
class, we can get an instance of ShowUserResponder
injected inside of the ShowUserAction
constructor.
namespace App\Users\Actions;
use App\Users\Responders\ShowUserResponder;
class ShowUserAction
{
public function __construct(
protected ShowUserResponder $responder
) {}
public function __invoke()
{
return $this->responder->respond();
}
}
At the moment, the ShowUserResponder
isn't actually returning a Response
.
We're showing a User
, so we can use route-model binding to retrieve a User
instance inside of the ShowUserAction::__invoke
method.
namespace App\Users\Actions;
use App\Users\Responders\ShowUserResponder;
class ShowUserAction
{
public function __construct(
protected ShowUserResponder $responder
) {}
public function __invoke(User $user)
{
return $this->responder->respond($user);
}
}
We also want to pass the User
object through to the ShowUserResponder::respond
method so that we can generate a valid Response
.
namespace App\Users\Responders;
use Illuminate\Http\Response;
class ShowUserResponder
{
public function respond(User $user)
{
return view('users.show', [
'user' => $user,
]);
}
}
Now that we have the User
object, we can use Laravel's view
helper to create a View
object which Laravel will automagically convert into a valid Response
object.
Responder
do?Apart from generating a valid Response
(or Responsable
object), the Responder
should be responsible for modifying anything response related.
This could be adding / modifying headers or changing the response format (i.e. use JSON instead of HTML via content-negotiation).
Here's the list of things I think ADR does better than MVC:
The ADR pattern was specifically designed with domain-driven design in mind. The letter D literally stands for domain.
I think the pattern lends itself to domain separation better than MVC.
You can choose to group based on target instead of component type. You'll no longer have 6 or 7 folders inside of app/Http/Controllers
, but instead will have 6 or 7 folders with an Http/Controllers
folder.
Depending on the size and scale of your application though, this level of abstraction can be overkill. You end up spending more time figuring out where things should go, instead of what the things should do.
One thing that's common in MVC is bloating your controllers with lots of business logic (I'm guilty of this sometimes).
With ADR, it's clear that your Action
classes are only responsible for handling the business logic and that your Responder
classes should handle the response generation.
If you find yourself using response()
or view()
inside of the Action
, you're doing ADR wrong.
I think this pattern also encourages the use of middleware and actual request interceptors too.
You can avoid authorization checks inside of your Action
by using middleware to run those checks instead. These could be reusable middleware handlers registered on the global middleware stack or route specific handlers.
Keeping these checks out of the Action
will help you further separate your concerns.
By @devcircus · 2021-10-28 02:12
Really love the structure and clarity that ADR brings to a Laravel app. I used it on many projects until I switched to Livewire. I'm actually trying to work out an ADR-like structure for Livewire. Really just trying to work out ANY kind of consistent structure with Livewire as my components tend to be radically different from each other.
By @ryan · 2021-10-28 08:03
When it comes to Livewire, I tend to follow the same structure as my controllers in term of naming and location (when possible).
Alongside that, I'll use consistent method names for things like save()
, update()
, submit()
etc.
By @mwikala · 2021-10-26 20:12
Really interesting read! I've noticed a rise in adoption of the ADR pattern in the Laravel community for quite a bit now, especially with starter templates like Jetstream introducing it to most people who weren't aware of it.
I think it's amazing and hopefully it becomes a standard in the Lara community, I just think it'll take a while for people to notice it's value.
By @karip · 2022-01-31 11:00
You shuould look at this package. https://laravelactions.com/