I did DDD before in my previous job. When I came to Infoxchange, I was raising Domain Driven Development to seniors and manager, because our application has so many different user cases. But no one listening. Recently I’ve read another awesome e-book, about Laravel DDD. It refreshed me a lot memories in the past. And also I’ve got new learnings.
Domain Oriented Laravel
“Human think in categories, our code should be a reflection of that”.
- What is Domain? Domains describe a set of the business problems you’re trying to solve.
- Example: An application to manage hotel bookings. It has to manage customers, bookings, invoices, hotel inventories, etc.
- Traditional (but modern) way: take one of group of related concepts, and split them across multiple places throughout your code: controllers, models, views.
- The problem of that is: Client hasn’t told you work on a controller, model, view.
- Domain Driven Design way: Instead, they only ask you to work on Invoice, Bookings, Customer management. These are so called “Domains“.
- But Invoice is not handled in isolation. They need Customer to be sent to, they need Bookings to generate. So we need further distinction about what is Domain code, what is not.
- Traditional (but modern) way: take one of group of related concepts, and split them across multiple places throughout your code: controllers, models, views.
- Example: An application to manage hotel bookings. It has to manage customers, bookings, invoices, hotel inventories, etc.
- Domains and Applications
- We have Domain representing all the business logic, but on the other hand, we have code to consume the domain, and integrate it with the framework, then expose to end user. This is so called “Application“.
- Application provides the infrastructure for end-users to access and manipulate the domain functionality.
- Application layer can be one or several applications. Every application can be seen as an isolated app which is allowed to be use all of domain code.
- Example:
- One application can be HTTP admin panel.
- One application can be REST API.
- Also could think about an artisan console.
- Example:
- Applications don’t talk to each other directly.
- Domains in Practice
- Domain code will consist of classes like: Models, Query Builders, Domain Events, Validation Rules.
- Domain’s folder structure could be look like this:
- Application folder structure could be look like this:
- You can also have a Support folder out side of Applications and Domains folders. This Support will be a dumping ground for little helpers. But most of your common helpers, can be written into a Laravel Package.
- If you changed Laravel’s app default path to other folder, you may need to change
->useAppPath()
inbootstrap/app.php
Working with Data
- Data is a first-class citizen in our codebase.
- Models are the first thing we build in Laravel. In large projects, there are something called ERDs (Entity Relationship Diagrams) and other diagrams to conceptulaize what data will be handled by the application. Only when that’s clear, you can start building the entry points and hook for your data.
- The benefit of it is make sure all developers can write code that interacts with this data, in a predictable and safe way.
- Type System
- PHP is a weak type language.
- It DOES make sense, as a scripting language, who mainly works with HTTP requests. Variables mostly start as a string type.
- Modern PHP has type juggling, to restrict the type of function parameters, and return type of function. But you can still change variables type inside the function.
- PHP ONLY checks its types at runtime, so it might have type errors that crash the program. This makes PHP a dynamically typed language.
- Strong type system defines whether a variable can change its type or not after it was defined. Benefits:
- It’s mathematically provable that if a strongly typed program compiles, it’s impossible for that program to have a range of bugs which would be able to exist in weakly typed languages.
- Strong type systems allow developers to have much more insight (static analysis the code) into the program when writing the code, instead of having to run it.
- PHP Type Checking Tool:
- PHPStan, Phan, Psalm.
- PHP is a weak type language.
- Structuring Unstructured Data (DTO)
- PHP arrays are versatile and powerful but painful:
- Have you ever worked with an “array of stuff”?
- Do you feel pain about not knowing exactly what’s in that array?
- Do you feel pain about not knowing the array keys are just numeric or meaningful fields?
- Do you feel pain about not knowing which fields are expected to be there and are they actually available?
- To resolve that you have to to one of the four:
- Read other source code: but you may not know every place in the code where defines your array.
- Read the documentation: it may out of date.
- Dump your array: can you do that in PROD / UAT as well?
- Debug.
- Struct is exactly what we need, and fortunately PHP has DTOs(Data Transfer Objects). Then your IDE will tell you what data you are dealing with.
- DTOs are the entry point for data into the codebase. Data transfer objects offer you a way to work with data in a structured, type safe and predictable way.
- PHP arrays are versatile and powerful but painful:
- DTO Factories
- Use Data Factories to create DTO from given Request:
class BookingData extends DataTransferObjectInterface { public static function fromRequest(BookingRequest $request): self { return new self([ 'id' => $request->get('bookingId'), 'date' => Carbon::make($request->get('bookingDate')), ]) } } // in PHP8 it will be: class BookingData extends DataTransferObjectInterface { public function __construct( public int $id, public Carbon $date ) {} public static function fromRequest(BookingRequest $request): BookingData { return new BookingData( id: $request->get('bookingId'), date: Carbon::make($request->get('bookingDate')), ) } } // Usage of the code: // PHP 7 $booking = BookingData::fromRequest($bookingRequest); // PHP 8 $booking = BookingData::fromRequest(...$bookingRequest->validated());
Actions
- We don’t want our business functionality to be spread throughout random functions and classes.
- Start with an Example: A customer wants to make a Booking. General speaking, We’ll create a booking in the database. More specific steps are:
- Save the booking in database.
- Create an invoice.
- Send an email with booking confirmation and invoice to customer.
- The traditional Laravel practice is to create a “Fat Model” handle all these functionality.
- Instead of mixing functionality in Models or Controllers, or you do it in a so called Service, but mixed with Eloquent code, we can treat each user story as first class citizen of our project. This is so called “Actions“.
- What are Actions:
- They live in the Domain.
- They are just simple classes without any abstractions or interfaces.
- They only allow the constructor to have dependency injection capabilities.
- They just take input, do something, then give output.
class CreateBookingAction { private CalculatePriceAction $createInvoiceAction; private CreateInvoiceAction $createInvoiceAction; private CreatePaymentAction $createInvoiceAction; private SendConfirmationAction $createInvoiceAction; public function __construct( CalculatePriceAction $calculatePriceAction, CreateInvoiceAction $createInvoiceAction, CreatePaymentAction $createPaymentAction, SendConfirmationAction $sendConfirmationAction ) public function __invoke(BookingData $bookingData): Booking { // ... $price = ($this->calculatePriceAction)($bookingData); $invoice = ($this->createInvoiceAction)($price); $payment = ($this->createPaymentAction)($invoice); ($this->sendConfirmationAction)($booking, $invoice); // ... return $booking; } }
- Why Actions are useful?
- Resuability.
- When actions are splitted into small pieces, they are reusable. (But keep them relatively large enough to avoid overloaded of classes).
- When you start spending time thinking about what the application actually will do, you’ll notice that there are lots of actions which can be reused.
- Careful to Abstraction.
- We also need to be careful not to over-abstract our code. But it is always better to copy-paste little by little later on, instead of making Premature Abstractions.
- When making abstractions, should think about functionality instead of the technical code.
- Reduce the cognitive load.
- Actions allows developers to think in the ways that are closer to the real world. Not just the code.
- To avoid your logic spread across in Controller, Model, maybe Job and Event Listener. Developer needs to understand all those classes, before start to make change.
- Very easy to test.
- No need to worry about faking HTTP, making Facades etc. Just test the input and output with mocking some dependencies. (But be careful with Dependency Actions, should avoid Action chain.)
- Resuability.
- Alternatives to Actions
- Commands and Handlers have better flexibility than Actions.
- Event Driven System also has better flexibility than Actions.
Models
- Models expose data that’s persisted in a data store.
- Laravel’s Eloquent models do more than just represent the data in a data store. They also allow you to build queries, load and save data, have built-in event system, etc.
- Models ≠ Business Logic
- Laravel Eloquent has
mutator
andaccessor
, people usually think about doing like this:class Booking extends Model { /** * Give me a lever long enough and a fulcrum on which to place it, * and I shall move the world. */ public function getDiscountedPriceAttribute(): float { $discount = 1; // imagine more if-else logics here ... return $this->price * $discount; } }
Yeah, give you a
mutator
andaccessor
, your Model can even render HTML. Instead, calculating the total price of an invoice is another user story that should be represented by an Action. - The benefits of move logics into Actions are:
- Single responsibility.
- You only calculate once, when in need of data.
- Calculated data can be read directly from data store.
- Model grows really fast and really huge.
- Laravel Eloquent has
- Scale down your Models
- We only want to keep the data read from the database, simple accessors for stuff we can’t calculate beforehand, casts, and relations. Other responsibilities should be moved to other classes.
- Query Scopes could be easily moved to dedicated Query Builder classes. Query Builder classes are actually the normal way of using Eloquent; Scopes are simply syntactic sugar on top of them.
// Define Eloquent Collection. class BookingCollection extends Collection { public function paid(): self { return $this->filter(fn (Booking $booking) => $booking->isPaid() ); } } // Define QueryBuilder. class BookingQueryBuilder extends Builder { public function wherePaid(): self { return $this->where('status', Paid::class); } } // Use QueryBuilder in Model. class Booking extends Model { // register query builder public function newEloquentBuilder($query): BookingQueryBuilder { return new BookingQueryBuilder($query); } // register collection public function newCollection(array $models = []): BookingCollection { return new BookingCollection($models); } public function isPaid(): bool { return $this->status === Paid::class; } } // Usage of Paid Bookings: // Use QueryBuilder $paidBookings = Booking::query()->wherePaid(); // Use Collection $paidBookings = $booking->paid()->map(fn (Booking $booking) => // ... );
You don’t need to introduce new patterns like Repository Pattern. You can build upon what Laravel provides. Similar blog post: https://timacdonald.me/dedicated-eloquent-model-query-builders/
There are some blog post discussing about why Repository Pattern is a bit of over kill for Laravel. Because unlike Symfony, Entity just plain object represents data in data store. However, in Laravel, the Model actually is doing Repository’s job already. So, there’s no need to use Repository Pattern.
Same as Eloquent Collection.
- Event Driven Models
- Laravel Models will emit generic model events by default and you can use a Model Observer to handle it.
- Another way of doing it is as below:
// Models/Booking.php class Booking extends Model { // Map events within Model protected $dispatchesEvents = [ 'saving' => BookingSavingEvent::class, ]; } // Events/BookingSavingEvent.php class BookingSavingEvent { public Booking $booking; // Define mapped event. public function __construct(Booking $booking) { $this->booking = $booking; } } // Subscribers/BookingSubscriber.php class BookingSubscriber { private CalculateDiscountPriceAction $calculateDiscountPriceAction; public function __construct( CalculateDiscountPriceAction $calculateDiscountPriceAction ) { /* ... */ } public function saving(BookingSavingEvent $event): void { $booking = $event->booking; $booking->discount_price = ($this->calculateDiscountPriceAction)($booking); } public function subscribe(Dispatcher $dispatcher): void { $dispatcher->listen( BookingSavingEvent::class, self::class . '@saving' ); } } // Register subscriber class EventServiceProvider extends ServiceProvider { protected $subscribe = [ BookingSavingEvent::class, ]; }
- Use Data Factories to create DTO from given Request:
States
- As we moved business logic away from Models, there is a question: what to do with models?
- Example: a Booking has a status of Paid, Unpaid, Overdue.
- State Pattern:
- A naive Fat Model approach would do something like.
class Booking extends Model { const PENDING = 'pending'; const PAID = 'paid'; const OVERDUE = 'overdue'; // display a coloured badge based on current 'state' field value. public function getStateColour(): string { if ($this->state->equals(self::PENDING) { return 'orange'; } if ($this->state->equals(self::PAID) { return 'green'; } if ($this->state->equals(self::OVERDUE) { return 'red'; } return 'grey'; } }
- You may have other idea but the key idea behind is
- listing all available options,
- and checking if one of them all matches the current given one,
- then do something.
It just a big if/else no matter how you organise your code.
- The problem for that is: you are adding a irrelevant responsibility to Model class. The State Pattern is the other way around: every state is represented by a separate class, and each of the class acts upon their own subject.
// create an abstract state class abstract class BookingState { abstract public function colour(): string; } // some concrete classes represent the `state` class PendingBookingState extends BookingState { public colour(): string { return 'orange'; } } class PaidBookingState extends BookingState { public colour(): string { return 'green'; } } class OverdueBookingState extends BookingState { public colour(): string { return 'red'; } } // usage in Booking model class Booking extends Model { public function getStateAttribute(): InvoiceState { // `state_class` is a field of Booking table. return new $this->state_class($this); } }
- Try this package: https://github.com/spatie/laravel-model-states
- A naive Fat Model approach would do something like.
Enums
- The state pattern’s goal is to get rid of all those conditionals, and instead rely on the power of polymorphism to determine the program flow. But we should be aware that the state pattern comes with significant overhead: you need to create classes for each state, configure transitions between them, and you need to maintain them.
- If you need a collection of related values and there are little places where the application flow is actually determined by those values, then yes: feel free to use simple enums. But if you find yourself attaching more and more valuerelated functionality to them, I would say it’s time to start looking at the state pattern instead.
- Bear in mind, State can do transitions (such as registering a set of Transitions), but Enum doesn’t.So the code can be written in this way:
class BookingState extends Enum { public function getColour(): string { return match($this->value) { self::PENDING => 'orange', self::PAID => 'green', self::OVERDUE => 'red', } } }
- Try this package: https://github.com/myclabs/php-enum
Managing Domains
- How to start using Domains?
- Teamwork: diagrams, tutorials, pair programmings, etc.
- Don’t be scared of 300 Models, 500 Actions. When that happen, it is not the architecture problem. It’s the complexity of business logic.
- The main difficulty in these projects is not how the code is technically structured; rather it’s about the massive amount of business knowledge there is to understand.
- On the contrary: A better structured architecture helps developers to focus on the business logic instead.
- Should think about break the application into several pieces.
- How to identify Domains?
- Even though you’re a developer, your primary goal is to understand the business problem and translate that into code. The code itself is merely a means to an end; always keep your focus on the problem you’re solving.
- Make sure you’ve got face-to-face time with your client. It will take time to extract the knowledge that you require to write a working program.
- Depending on the size of your team, you might not need face-to-face interaction between all developers and the client, but nonetheless, all developers will need to understand the problems they are solving with code.
- You shouldn’t fear domain groups that change over time. It’s healthy to keep iterating over your domain structure to keep refactoring it. In summary: don’t be afraid to start using domains because you can always refactor them later. Domain code are very flexible due to minimal dependencies, it doesn’t take much effort to refactor it.
- Do Event Storming sessions with client.
Testing
- How to test Domains?
- As a Laravel developer, you would use Test Factory. Something like this:
// Define $factory->state(Booking::class, 'paid', [ 'status' => PaidBookingStatus::class, ]) // Use public function testCase() { $booking = factory(Booking::class) ->states('paid') // You don't know what states ara available ->create(); }
- Problem 1: IDE don’t what
$factory
is. It’s a magic. - Problem 2:
paid
, the state for the factory is a string. When you use it, you don’t know what states are available. - Problem 3: your IDE doesn’t know what will the factory produce. No type hinting for the output.
- Problem 1: IDE don’t what
- How about using DTOs or Requests in Test?
- The actual goal of these factory classes is to help you write integration tests, without having to spend too much time setting up the system for it. Not unit tests.
- When we test our Domains, we are actually testing the business logic. When we talking about business logic, it is not a single class, so it is a group of classes integration test.
- Another Way of Test Factory
- Instead of using Laravel default
$factory
, we try another way:// Definition class BookingFactory { // Name as `new` to avoid confusion of `make`, `create`. public static function new(): self { return new self(); } public function create(array $extraInfomation = []): Booking { return Booking::create(array_merge( [ 'status' => $this->status ?? PendingBookingStatus::class, // ... other default values. ], $extra )); } **// This is an alternative way of doing Laravel State.** public function overdue(): self { $clone = clone $this; $clone->status = OverdueBookingStatus::class; return $clone; } **// This is an alternative way of doing Laravel afterCreating().** public function paid(): self { $clone = clone $this; $clone->status = PaidBookingStatus::class; **// After creating a Booking with Paid status, // A payment should be created as well (with payment method).** $clone->paymentFactory = $paymentFactory ?? PaymentFactory::new(); return $clone; } } // Usage public function testCase() { $booking = BookingFactory::new()->create(); $overdueBooking = BookingFactory::new()->overdue()->create(); // Pass an PaymentFactory (with payment `type` of Paypal) // into BookingFactory. **// This is an alternative way of Laravel afterCreating() callback.** $paidBooking = BookingFactory::new()->paid( PaymentFactory::new()->method(PaypalPaymentType::class) )->create(); }
- Benefit 1: your IDE knows what the
XxxxFactory
is - Benefit 2: your know what
state
ortype
for your models created by factory - Benefit 3: type hinting is working now
- Benefit 4: these factories can also used as part of set up DTOs.
- Cautious: you are using an immutable factory.
$bookingA = $bookingFactory->paid()->create(); **// If `paid()` function is not immutable, // $bookingB will also be a paid booking, Instead of "pending"** $bookingB = $bookingFactory->create();
- Concern 1: writing more extra code.
- But the flexibility and clear structure worth it. Especially when you have 1000+ lines of Laravel
ModelFactory.php
, and you will be lost in huge amount of factorystates
. - Also you can write an abstract class for common functions.
- But the flexibility and clear structure worth it. Especially when you have 1000+ lines of Laravel
- Benefit 1: your IDE knows what the
- Instead of using Laravel default
- As a Laravel developer, you would use Test Factory. Something like this:
- How to test DTOs?
- There are not many things to test for DTOs:
- The happy flow normally just check if the DTO is created correctly or not, based on given Model.
- The exception flow just throwing Exceptions, when data are missing.
- Again! DTOs are strongly typed, and their purpose is just representing data.
- There are not many things to test for DTOs:
- How to test Actions?
- Majorly, there are two things to test:
- Whether or not, actions do what they suppose to do?
- Do actions use their sub-actions in the right way?
- Since Actions should be just simple input → do things → output. So it would be easily tested in this way: (pseudo code)
// Here just some pseudo code public function bookingTest() { // **setup** $bookingData = BookingDataFactory::new() ->addInvoiceDataFactory( InvoiceDataFactory::new() ->withDescription('test description') ->withPrice(100.00) )->create(); $action = app(CreateBookingAction::class); **// execute** $booking = $action->execute($bookingData); **// assert** $this->assertDatabaseHas($booking->getTable(), [ 'id' => $booking->id ]); }
- If you realize you need to do some really complicated mocking service, using globals, etc. Probably you are in a wrong direction. Instead of trying hard to write super smart testing code, you should refactor your untestable code.
- There is a caveat for testing Actions:
- If you are testing
CreateBookingAction
, inside this action, it may invoke another sub actionSendConfirmationEmailAction
for the booking. So we have already separated send email into another action. - But this will come up with: you are triggering sending emails every time in your tests for
SendConfirmationEmailAction
and all its “parents” actions. Other scenarios could be: some action is saving a file, some action is generating a PDF. All those are time consuming, and should NOT be tested in “parents” actions. - How we test this? We should create a
MockSendConfirmationEmailAction.php
under theTests\\Mocks\\Actions
folder.// Define a Mocked action. namespace Tests\\Mocks\\Actions; class MockSendConfirmationEmailAction extends SendConfirmationEmailAction { public static function setUp(): void { **// To register a shared binding // So that it will be always the mocked concrete.** app()->singleton( SendConfirmationEmailAction::class, fn () => new self() ); } public function execute(Mailer $mailer): void { // Just simply do nothing return; } } // Usage public function setUp() { MockSendConfirmationEmailAction::setUp(); // ... other setups }
- If you are testing
- Majorly, there are two things to test:
- How to test Model Collections?
public function testCase() { $factory = BookingFactory::new(); $paidBooking = $factory->paid()->create(); // Eloquent Collections take an array of model. $collection = new BookingCollection([$paidBooking]); $this->assertCount(1, $collection->paid()->count()); }
- How to test Query Builders?
public function testCase() { $factory = BookingFactory::new(); $paidBooking = $factory->paid()->create(); $this->assertEquals( 1, Booking::query() ->wherePaid() ->whereKey($paidBooking->id) ->count() );
- How to test Subscribers?
public function testCase() { $subscriber = app(BookingEventSubscriber::class); $event = new BookingSavingEvent(BookingFactory::new()->create()); $subscriber->saving($event); $booking = $subscriber->booking; // the model property inside subscriber $this->assertNotNull($booking->id); }