Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Notes on's Laravel beyond CRUD

Laravel beyond CRUD

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.

Chapter 1: 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.

    |── Actions
    ├── QueryBuilders
    ├── Collections
    ├── DataTransferObjects
    ├── Events
    ├── Exceptions
    ├── Listeners
    ├── Models
    ├── Rules
    └── States

    // etc

Application layer:

// The admin HTTP application
    ├── Controllers
    ├── Middlewares
    ├── Requests
    ├── Resources
    └── ViewModels

// The REST API application
    ├── Controllers
    ├── Middlewares
    ├── Requests
    └── Resources

// The console application
    └── 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.

Chapter 2: Working With Data

Structuring unstructured 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:

Chapter 3: Actions

Chapter 4 Models

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;

Chapter 5: Models with the State Pattern

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.

Chapter 6: Managing Domains

How to start using domains, to identify them, and to manage them long term?

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):

Chapter 7: Entering the Application Layer

Connect the User to the Domain

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)

	├── Controllers
	├── Filters
	├── Middleware
	├── Queries
	├── Requests
	├── Resources
	└── ViewModels

Brent calls these Application Modules.

Chapter 8: View Models


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment