Experimentation with hexagonal architecture for Laravel / PHP
In building large scale web applications MVC seems like a good solution in the initial design phase. However after having built a few large apps that have multiple entry points (web, cli, api etc) you start to find that MVC breaks down. Enter Hexagonal Architecture.
One of the big tennants of the architecture presented here is SOLID principles. Each class tries to only do a single responsibility. This makes for much simpler testing, later extention and modification.
I won’t go into the specifics of what Hexagonal Architecture IS in this readme, the references below do a great job of that. Instead this readme will just outline the example of the architecture outlined in this Laravel 4.1 app.
References:
Please make pull requests and create issues for discussion and improvement.
The controllers, models and views live in their default out-of-the-box locations for a Laravel application.
A app/lib
directory has been added and is referenced in the classmap
entry of the composer.json file so it will be loaded corrrectly with no top level namespace.
The lib folder contains:
File | Description |
---|---|
Contracts\Instances\InstanceInterface.php | Should be implemented by your models |
Contracts\Notification\CreatorInterface.php | Should be implemented by a class (controller) that creates things |
Contracts\Notification\UpdaterInterface.php | Should be implemented by a class (controller) that updates things |
Contracts\Notification\DestroyerInterface.php | Should be implemented by a class (controller) that destroys things |
Contracts\Repositories\RepositoryInterface.php | For each entity you have you should have a repository interface for it that extends this class. Custom methods from the class will go in there, but common methods are provided by RepositoryInterface. |
Providers\RepositoriesServiceProvider.php | Each entity that you add needs an entry added to this service provider to tell Laravel which concrete implementation is needed when you inject an interface. |
Repositories\DbRepository.php | Each entity that you add needs a DbRepository class added that implements the coresponding Repository interface. (i.e. DbOrderRepository implements Contracts\Repositories\OrderRepositoryInterface ) The entity specific repository and corresponding interface serve as a place to add custom repository methods. An example for orders might be getReturnedOrdersThisWeek() |
Services\PluralizedEntityName \EntityName Creator.php |
Each entity will have an associated service object that will be used for creating instances of that object. The namespaceing and class name is specific to the entity name. |
Services\PluralizedEntityName \EntityName Updater.php |
Each entity will have an associated service object that will be used for updating instances of that object. The namespaceing and class name is specific to the entity name. |
Services\PluralizedEntityName \EntityName Destroyer.php |
Each entity will have an associated service object that will be used for destroying instances of that object. The namespaceing and class name is specific to the entity name. |
Validators\EntityName Validator.php |
Each entity will have an associated validator object that will be used for validating instances of that object. The class name is specific to the entity name, and should extend Validator.php. Each Validator class just needs to specify validation rules at a minimum. |
In the documentation of this request flow we’ll look at the store order use case
The Router (routes.php) receives the request and hands it off to a Controller.
In our Orders example the controller can create, update, and destroy orders so the class implements the CreatorInterface
, UpdaterInterface
, and DestroyerInterface
.
SOLID: To adhere to the “I” of SOLID (Interface Segmentation) the 3 interests are split out into seperate interfaces. So a controller that only created things whouldn’t implement the other 2 interfaces and thus be required to implment those methods.
The appropriate controller action is invoked based on the routing (store
in our example).
Note: For actions that present a view they simply do so, using a simple
View::make()
. For constructive or destructive methods we need to hand off to a service object for it’s Single Responsibility
An instance of OrderCreator
is resolved from the IoC Container and given the necessary arguments to create the Order.
Note: See that we are returning the result from the
OrderCreator::create()
function. In the method call stack this will actually be the return value from thecreationSucceeded
orcreationFailed
controller methods.
The OrderCreator
service object takes an OrderValidator
argument in the constructor. Because we resolved the OrderCreator
from the IoC Container, Laravel went ahead and created an OrderValidator
instance for us as an argument too.
In the create()
method the OrderCreator
hands off responsibility to the OrderValidator
to do the validation and creates the Order
if validation succeeds.
Based on the success or failure of the validation and subsequent creation (or not) of the Order. The OrderCreator
will call interface methods on the CreatorInterface $listener
, this is actually the OrdersController
which passed itself in to the OrderCreator::create()
method.
In the Controller’s implementation of the creationSucceeded
and creationFailed
methods, the controller can decide what it wants to do if the service object succeded or failed at creating the order.
In this architecture and example of create order we’ve separated concerns as follows:
The controllers just request the operation of the appropriate party and respond with the result.
The service objects handle the action of create, update, and destroy independently
Note: Typical “CRUD” is listed as CrUD above since it is really Create, Update, and Delete only, no Read.
When a CrUD Service or Controller needs to fetch an object or a collection it uses a Repository which implements a corresponding Repository Interface.
Whenever a CrUD Service needs validation it hands off responsibility to a dedicated validator.
Controllers (or any other class could too) implement Notification interfaces so that they can be updated on the success or failoure of the requested CrUD action.
As a best practice we try to “code to an interface”, which is why our Eloquent model implmements the InstanceInterface
. That way we can type hint that interface in other functions, and later if we decide to have models that aren’t Eloquent they can implment that interface too and everything should still work.