A blog series for PHP developers working on larger-than-average Laravel projects
Written for projects with a development lifespan of six to twelve months, with a team of three to six developers working on them simultaneously.
https://stitcher.io/blog/laravel-beyond-crud-01-domain-oriented-laravel
"Domain" comes from DDD. It describes a set of the business problems to be solved.
There often isn't a one-to-one mapping between controllers and models.
The domain is all the business logic.
Everything else (ish...) is code that uses/consumes that domain to integrate it with the framework and exposes it to the end-user.
The domain will hold classes like models, query builders, domain events, validation rules and more.
app/Domain/Invoices/
|── Actions
├── QueryBuilders
├── Collections
├── DataTransferObjects
├── Events
├── Exceptions
├── Listeners
├── Models
├── Rules
└── States
app/Domain/Customers/
// etc
Application layer:
// The admin HTTP application
app/App/Admin/
├── Controllers
├── Middlewares
├── Requests
├── Resources
└── ViewModels
// The REST API application
app/App/Api/
├── Controllers
├── Middlewares
├── Requests
└── Resources
// The console application
app/App/Console/
└── Commands
It's easier to change the namespacing to App/
, Domain/
and Support/
(Support is for everything that doesn't strictly belong anywhere else).
This can be done in composer.json
, and bootstrap/app.php
.
Start thinking in groups of related business concepts, rather than in groups of code with the same technical properties.
https://stitcher.io/blog/laravel-beyond-crud-02-working-with-data
Use DTOs to pass data back and forth. THe problem is mapping the data to and from DTOs.
With PHP7.4, typed properties can be used. Thus a strongly-typed application.
A dedicated factory is an option, but it adds application-specific logic in the Domain layer.
Spatie have a package for this: https://github.com/spatie/data-transfer-object
https://stitcher.io/blog/laravel-beyond-crud-03-actions
Don't store business logic in models. These small methods add up.
Model files can be too long, made up of many small business-related methods.
Move responsibilities to other classes.
Instead of keeping scopes in a Model class, use a custom Query Builder class.
This class can be loaded in to the model, overriding the default Query Builder.
namespace Domain\Invoices\QueryBuilders;
use Domain\Invoices\States\Paid;
use Illuminate\Database\Eloquent\Builder;
class InvoiceQueryBuilder extends Builder
{
public function wherePaid(): self
{
return $this->whereState('status', Paid::class);
}
}
// in the model
class Invoice extends Model
{
public function newEloquentBuilder($query): InvoiceQueryBuilder
{
return new InvoiceQueryBuilder($query);
}
}
The same can be done with Model Collections, by using the newCollection()
method.
namespace Domain\Invoices\Collections;
use Domain\Invoices\Models\InvoiceLines;
use Illuminate\Database\Eloquent\Collection;
class InvoiceLineCollection extends Collection
{
public function creditLines(): self
{
return $this->filter(function (InvoiceLine $invoiceLine) {
return $invoiceLine->isCreditLine();
});
}
}
// then in the model
class InvoiceLine extends Model
{
public function newCollection(array $models = []): InvoiceLineCollection
{
return new InvoiceLineCollection($models);
}
public function isCreditLine(): bool
{
return $this->price < 0.0;
}
}
https://stitcher.io/blog/laravel-beyond-crud-05-states
The state pattern is one of the best ways to add state-specific behaviour to models, while still keeping them clean.
States and transitions between them, are a frequent use case in large projects.
The state pattern treats "a state" as a first-class citizen of our codebase. Every state is represented by a separate class, and each of these classes acts upon a subject (model).
Write an abstract State class for the state, then subclass with all variations.
Transitions: a class which will take a model and change that model's state.
e.g. PendingToPaidTransition
changes the invoice from Pending to Paid states.
https://stitcher.io/blog/laravel-beyond-crud-06-managing-domains
Large projects not only have to manage the files and directory sizes, but also the massive amount of business logic.
It's about making large codebases easier to navigate and to keep the project healthier for longer.
It's healthy to keep iterating over your domain structure, to keep refactoring it.
A live doamin refactoring session (by Freek): https://www.youtube.com/watch?v=yPiMzw-lLF8
https://stitcher.io/blog/laravel-beyond-crud-07-entering-the-application-layer
One project can have several applications: HTTP, CLI. Also API, client/admin sides.
An application's goal is to take the user input, pass it to the domain and represent the output in a usable way for the user.
For large projects, rather than the usual Laravel app/
directory structure, this might be better (Invoices is one of many parts of the Admin domain)
App/Admin/Invoices
├── Controllers
├── Filters
├── Middleware
├── Queries
├── Requests
├── Resources
└── ViewModels
Brent calls these Application Modules.
TBA