DOMAIN-DRIVEN DESIGN WITH LARAVEL M A R T I N J O O The on l y des i gn app roach you need
Basic Concepts Domain-Driven Design Working With Data Value Objects Data Transfer Objects Repositories Custom Query Builders Services Actions ViewModels CQRS States And Transitions Domains And Applications Advantages And Disadvantages Designing an E-mail Marketing Software Overview Subscribers Broadcasts Sequences Automations Other Features Why E-mail Marketing? User Stories Data Modeling Subscribers Broadcasts SentMails A Quick Note On Performance Sequences Automations Domains Building an E-mail Marketing Software Setting Up Domains and Applications Subscribers Creating A New Subscriber Updating A Subscriber View Models Vue Component Get Subscribers The Real Power of DTOs and Actions Conclusion Broadcasts The Broadcast DTO Handling Filters Upserting A Broadcast Filtering Subscribers Sending A Broadcast Calculating The Performance Of A Broadcast Martin Joo - Domain-Driven Design with Laravel 1 / 258
Previewing A Broadcast Get Broadcasts Sequences Creating A Sequence Proceeding A Sequence Refactoring Updating The Subscriber's Status Calculating The Performance Of A Sequence Progress Of A Sequence Dashboard And Reports New subscriber counts All-Time Performance Subscribers Automations Upserting automations Running automations Conclusion Thank You Martin Joo - Domain-Driven Design with Laravel 2 / 258
Basic Concepts Domain-Driven Design First of all, we have to answer the most obvious question: what is Domain-Driven Design. DDD is a software development approach that tries to bring the business language and the code as close together as possible. This is the most critical attribute of this approach. But for some reason, DDD is one of the most misunderstood and overcomplicated topics in the developer community, so I'll try to make it easy to understand. DDD teaches us two main things: Strategic Design Technical Design In my honest opinion, strategic design is way more important than the technical aspects. It's hard to summarize it in one cool sentence, but you will see what I mean in the rest of the book. For now, these are the essential pillars: Domains and namespaces. Later, I'll talk about what a domain is, but DDD teaches us to structure our code very expressive and logical. Choosing the proper names. For example, if the business refers to the users as "customers" or "employees, " you should rename your User class to follow that convention. The classes and objects should express the intention behind them. In the most simple Laravel application, you have models and controllers. What do you think when you see a project with 50 models and 50 controllers? You can see the application's primary domain, but you have to dig deeper if you want to have a good understanding of the features, right? Now, what about 300 models and 500 controllers? You have no chance to reason about this kind of application. In most projects, developers prefer technical terms over business concepts. That's natural. After all, we're technical people. But I have a question: are those technical terms significant? Let me show you an example. This is a snippet from one of my applications I wrote on Oct 28, 2016, after finishing the Design Patterns book. Take a look at it (it's not Laravel): Martin Joo - Domain-Driven Design with Laravel 3 / 258
class Search_View_Container_Factory_Project { /** * @var Search_View_Container_Relation_Project "# private static $_relationContainer; /** * @param array $data * @return Search_View_Container_Project "# public static function createContainer(array $data) { if ($data['current'] "$ 'complex') { return self"%createComplex($data); } else { return self"%createSimple($data); } } /** * @param array $data * @return Search_View_Container_Project "# private static function createSimple(array $data) { $container = new Search_View_Container_Project('simple'); $container"&setSearchTerm(Arr"%get($data, 'search_term')); $relationContainer = new Search_View_Container_Relation_Project(); $industryModel = new Model_Industry(); $industries = $industryModel"&getAll(); foreach ($industries as $industry) { $industryItem = new Search_View_Container_Relation_Item( $industry, Martin Joo - Domain-Driven Design with Laravel 4 / 258
Today it's February 2, 2022. What do you think? After six years, do I have any clue what the heck is a Search_View_Container_Relation_Item? No, I have no idea what it is. I only know one thing for sure: it does not help me. This project is about freelancers and projects. This class does something with searching projects (I guess), but it does not reveal that intention. Did you ever hear a product manager saying: wow, we got so much positive feedback on the Search View Container Factory Project feature? Maybe if I take a step back and look at the file structure, I have a better idea. Search_View_Container_Relation_Item"%TYPE_INDUSTRY, false ); $relationContainer"&addItem($industryItem, Search_View_Container_Relation_Item"%TYPE_INDUSTRY); } $container"&setRelationContainer($relationContainer); return $container; } /** * @param array $models * @param int $type "# private static function addItems(array $models, $type) { foreach ($models as $model) { $item = new Search_View_Container_Relation_Item($model, $type, true); self"%$_relationContainer"&addItem($item, $type); } } } Martin Joo - Domain-Driven Design with Laravel 5 / 258
Nope, still have no idea. Here's my point: Martin Joo - Domain-Driven Design with Laravel 6 / 258
Technical terms and overused patterns suck when it comes to high-level business applications. So strategic design is all about not building projects like this one. And technical design gives you some valuable tools to achieve that. In the following pages, we'll talk about these concepts: Value Objects Data Transfer Objects Repositories Custom Query Builders Services Actions View Models CQRS States and Transitions Domains and Applications Martin Joo - Domain-Driven Design with Laravel 7 / 258
Working With Data Working with data is one of the most critical aspects of every business application. Unfortunately, PHP is not so good when it comes to this. In my opinion, one of the best and worst features of PHP is arrays. Especially associative arrays. The problems are: No type-hints Undocumented structure No restrictions. You can put product models, product IDs, and product arrays under the same key. Associative arrays are big unstructured blobs of data. Don't get me wrong; they can be helpful but very annoying at the same time. Initially, PHP arrays tried to solve every problem: queues, stacks, lists, hash maps, and trees. Everything. But with its weak type system, it's tough to maintain this kind of data structure. If you think about it, data plays a huge role in any business application: The request comes in. It contains the incoming data. The business layer processes this data. The database layer inserts this data into the DB. The response comes out. It includes the outgoing data. So you have to work with data in every single layer of your application. Fortunately, Laravel and DDD give us some very clever concepts. Martin Joo - Domain-Driven Design with Laravel 8 / 258
Value Objects Value Object is an elementary class that contains mainly (but not only) scalar data. So it's a wrapper class that holds together related information. Let's see an example: This class represents a percentage value. This simple class gives you three advantages: It encapsulates the logic that handles null values and represents them as percentages. You always have two decimal places (by default) in your percentages. Better types. An important note: business logic or calculation is not part of a value object. The only exception I make is basic formatting. By better types, I mean methods like this: class Percent { public readonly ?float $value; public readonly string $formatted; public function "'construct(float $value) { $this"&value = $value; if ($value ""( null) { $this"&formatted = ''; } else { $this"&formatted = number_format($value * 100, 2) . '%'; } } public static function from(?float $value): self { return new self($value); } } Martin Joo - Domain-Driven Design with Laravel 9 / 258
You take a float value and make it a first-class citizen using a Percent value object. You don't have to worry about anymore if a method in your app returns a formatted string or a float number. Every percentage value can be expressed as a Percent object from now on. So you know, it contains the float number and the formatted string value. The original definition of a value object states two more things: It's immutable. You have no setters and only read-only properties. It does not contain an ID or any other property related to the identification. Two value objects are equal only when the values are the same. What else can be expressed as a value object? Almost anything, to name a few examples: Addresses. In an e-commerce application where you have to deal with shipping, it can be beneficial to use objects instead of strings. You can express each part of an address as a property: City ZIP code Line 1 Line 2 Numbers. Any financial application can benefit from using value objects when calculating metrics or comparing numbers. You can express some very high-level concepts, for example, Margin. Top Line (such as revenue) Bottom Line (such as net profit) Margin (as a Percent, of course) Email addresses Or other application-specific concepts Let's take a closer look at the Margin example: private function averageClickRate(int $total): Percent { return Percent"%from( SentMail"%whereClicked()"&count() / $total ); } Martin Joo - Domain-Driven Design with Laravel 10 / 258
Suppose you've worked with financial applications that deal with publicly traded companies. You know that a number like revenue is given in millions (or billions in some cases, for example, the market cap). So when you query Apple's revenue (which is 378 billion at the time of writing) from a finance API, you don't get 378,323,000,000 but 378,323, so we can express it in the code as well: And we can use the Margin class like this: class Margin { public function "'construct( public readonly float $topLine, public readonly float $bottomLine, public readonly float $margin, ){} } class Margin { public function "'construct( public readonly Millions $topLine, public readonly Millions $bottomLine, public readonly Percent $margin, ) {} } Martin Joo - Domain-Driven Design with Laravel 11 / 258
In this example, I assume that revenue and netProfit are instances of Millions . But isn't IncomeStatement an Eloquent model? Glad you asked. It is. And we can write a custom cast to convert floats to Millions : class MetricsService { public function profitMargin(IncomeStatement $incomeStatement): Margin { return new Margin( topLine: $incomeStatement"&revenue, bottomLine: $incomeStatement"&net_profit, margin: new Percent( $incomeStatement"&net_profit"&value / $incomeStatement"&revenue- >value ), ); } } class MillionsCast implements CastsAttributes { /** * @param float $value "# public function get($model, $key, $value, $attributes) { return new Millions($value); } /** * @param Millions $millions "# public function set($model, $key, $millions, $attributes) { return [ $key ") $millions"&value, ]; } Martin Joo - Domain-Driven Design with Laravel 12 / 258
It can be used in an Eloquent model, and here's how it works: When you're accessing an attribute on the model, the get method will be called. So $incomeStatement->revenue will return an instance of Millions . When you're setting an attribute on the model, the set method will be called. So $incomeStatement- >revenue = new Millions(1000) will insert the value property (1000) from the Millions instance. The last part is to use the cast in the model: So, in a nutshell, this is how you use a value object. To summarize it: By using value objects, you can make objects from cohesive scalar data The main benefits: It makes your code more high-level. It clarifies things and helps to avoid confusion. For example, now you know exactly that Millions contains a number stored in millions. It helps you deal with nullable values. You don't have to write ?float $revenue anymore. You can write Millions $revenue . In the introduction, I wrote that data is a crucial part of any application. I gave you this list: The request comes in. It contains the incoming data. The business layer processes this data. The database layer inserts this data into the DB. The response comes out. It includes the outgoing data. As you can see in the cast and the other examples, a value object is mainly used inside (but not exclusively!) our application. In the next chapter, we'll discuss what happens at the boundaries (requests and responses). } protected $casts = [ 'revenue' ") MillionsCast"%class, 'net_profit' ") MillionsCast"%class, ]; Martin Joo - Domain-Driven Design with Laravel 13 / 258
Data Transfer Objects The following important concept is data transfer object, or DTO for short. This is also a simple concept: it's a class that holds data. This data is then transferred between components. What are these components? Your application as a whole Classes inside your application Let's take a look at a straightforward example: This is an oversimplified example, of course. I'm working on an e-learning system, and you can believe me, the request for creating a new course is overwhelming. After time this action will become more complicated, and you want to refactor it. Let's move this into a service (later, we'll talk about services in more detail. For now, it's a class that implements some business logic): class CourseController extends Controller { public function store(Request $request): Course { $course = Course"%create($request"&course); foreach ($request"&lessons as $lessson) { "* $lesson is an array $course"&lessons()"&create($lesson); } foreach ($request"&student_ids as $studentId) { $course"&students()"&attach($studentId); } return $course; } } Martin Joo - Domain-Driven Design with Laravel 14 / 258
It looks okay, but can you spot the problems? Arguments like these: array $data or array $lessons Lines like this: $data['lessons'] My biggest problem is that I don't want to maintain and debug massive associative arrays five years later. The above example is very basic. Now, please imagine your favorite legacy project, where you have to work with methods like this: class CourseService { public function create(array $data): Course { $course = Course"%create($data); $this"&createLessons($course, $data['lessons']); $this"&addStudents($course, $data['student_ids']); return $course; } public function createLessons(Course $course, array $lessons): void { foreach ($lessons as $lessson) { "* $lesson is an array $course"&lessons()"&create($lesson); } } public function addStudents(Course $course, array $studentIds): void { foreach ($studentIds as $studentId) { $course"&students()"&attach($studentId); } } } Martin Joo - Domain-Driven Design with Laravel 15 / 258
DTOs can solve this problem by structuring your unstructured data. The same CourseService with DTOs: public function createProduct(array $data) { "* Insert 673 lines of code here /** * You have to reverse-engineer this whole shit-show * just to get an idea about the shape of $data, right? "# } class CourseService { public function create(CourseData $data): Course { $course = Course"%create($data"&all()); $this"&createLessons($course, $data"&lessons); $this"&addStudents($course, $data"&student_ids); return $course; } /** * @param Collection<LessonData> $lessons "# public function createLessons(Course $course, Collection $lessons): void { foreach ($lessons as $lessson) { "* $lesson is an instance of LessonData $course"&lessons()"&create($lesson); } } public function addStudents(Course $course, Collection $studentIds): void { Martin Joo - Domain-Driven Design with Laravel 16 / 258
Now, instead of arrays, we have objects like CourseData and LessonData . Let's take a look inside CourseData : foreach ($studentIds as $studentId) { $course"&students()"&attach($studentId); } } } class CourseData { public function "'construct( public readonly int ?$id, public readonly string $title, public readonly string $description, /** @var Collection<LessonData> "# public readonly Collection $lessons, /** @var Collection<int> "# public readonly Collection $student_ids, ) {} public static function fromArray(array $data): self { $lessons = collect($data['lessons']) "&map(fn (array $lesson) ") LessonData"%fromArray($lesson)); return new self( Arr"%get($data, 'id'), $data['title'], $data['description'], $lessons, collect($data['student_ids']), ); } } Martin Joo - Domain-Driven Design with Laravel 17 / 258
The only place you have to deal with arrays with this approach is the DTO itself. Only the factory function will know anything about the ugly $data array. Every layer of your application will use a structured, type- hinted object. As you can see, this class does not interact with Request or any other class that is environment-dependent so that you can use DTOs anywhere, including: Controllers Console Commands Services or Actions (covered later in the book) Models or Query Builders (covered later in the book) And here's how you can use it from the CourseController : I think that's a much better approach, especially in larger projects. But now, we have another problem. Just imagine how many classes we need to create to store a course: CreateCourseRequest CourseData CourseResource LessonData LessonResource A few value objects here and there And in this example, we have only two models! What if the domain model of this feature is much more complex? You can quickly end up with 10-15 classes to implement the CRUD functionality for courses. It's not the end of the world, but it can be very frustrating. Fortunately, we have an elegant solution. But first, let's summarize what a DTO is: It's an object that holds and transfers the data of a model. It can be used inside your application between components. Like in the example, when we created a DTO in the controller from a request and passed it to a service. But it can also be used outside of your application. So instead of having a request, a resource, and a DTO for the course, why not just have one DTO to rule them all? class CourseController extends Controller { public function store( Request $request, CourseService $courseService ): Course { return $courseService"&create( CourseData"%fromArray($request"&all()) ); } } Martin Joo - Domain-Driven Design with Laravel 18 / 258
Enter the laravel-data package by Spatie. You can use one DTO to act as a: Request (with validation rules) Resource And a simple DTO Important note: If you want to use DTOs you don't need to go with laravel-data. You can write pure PHP objects, and you'll do just fine. But I find this package so helpful; I cannot imagine a large project without it. This is what a laravel-data DTO looks like: The basics are very similar to a pure PHP DTO, but we have this Lazy thing. I will talk about it later, but it's very similar to the whenLoaded method used in Laravel resources (it helps us avoid N+1 query problems). So we have a subscriber with a nested TagData collection and a nested FormData property. This package can create a DTO from a request automatically. Since it can be used as a request, we can define validation rules: class SubscriberData extends Data { public function "'construct( public readonly ?int $id, public readonly string $email, public readonly string $first_name, public readonly ?string $last_name, /** @var DataCollection<TagData> "# public readonly null|Lazy|DataCollection $tags, public readonly null|Lazy|FormData $form, ) {} } Martin Joo - Domain-Driven Design with Laravel 19 / 258
Comments 0
Loading comments...
Reply to Comment
Edit Comment