Laravel Concepts 2023 Part 2 - Martin Joo
Laravel Concepts 2023 Part 2 - Martin Joo
1 / 123
Martin Joo - Laravel Concepts - Part II
Laracheck
deptrac
Working with OS Processes
Custom Query Builders
Scopes
Queries
Separation
Final Words
2 / 123
Martin Joo - Laravel Concepts - Part II
Let's say we are working on the backend of an e-commerce app, so we're building
dashboards and lists for business people. Often they want more sophisticated filters than
regular users. Now imagine you have 20 of those. It's easy to mess things up because
these things often start as a few where expressions in your controller. But as the number
of filters and their complexity grows your code becomes messier and you'll end up with a
"don't touch it" class. You know, when you say to new developers: "This controller takes
care of filtering products. It works, don't touch it!"
In this essay, I'd like to give some ideas about how to deal with these situations. Take a look
at the design:
3 / 123
Martin Joo - Laravel Concepts - Part II
Lifetime revenue
Average rating
And one bonus is called the "most popular." It returns products that have:
Lifetime revenue
Average rating
Quantity (units sold)
Revenue contribution
The plan is simple: implement these features in an easy to extend and maintainable
way. The best way to achieve this is to use:
One for filters another for sorters. They will act as factories.
And sometimes pipelines
4 / 123
Martin Joo - Laravel Concepts - Part II
Data modeling
The database layer is relatively straightforward:
orders
order_items
products
product_ratings
users
5 / 123
Martin Joo - Laravel Concepts - Part II
6 / 123
Martin Joo - Laravel Concepts - Part II
Filters
First, without any additional context or "architecture" let's just start with the Eloquent and
SQL queries. After that, we're going to talk about where to put and how to structure these
classes.
Revenue Filter
Users want to filter products that have a lifetime revenue above a certain threshold.
select
`products`.*,
(
select sum(`order_items`.`total_price`)
from `order_items`
where `products`.`id` = `order_items`.`product_id`
) as `total_revenue`
from `products`
group by `products`.`id`
having `total_revenue` !$ 990
Another way would be to join the order_items table instead of using a sub-query:
7 / 123
Martin Joo - Laravel Concepts - Part II
select
`products`.*,
sum(order_items.total_price) as total_revenue
from `products`
inner join order_items on order_items.product_id = products.id
group by `products`.`id`
having `total_revenue` !$ 990
There's not a big difference between these queries, however, there are some important
things:
return Product!%query()
!'select('products.*',
DB!%raw('SUM(order_items.total_price)'))
!'join('order_items', 'products.id', '=',
'order_items.product_id')
!'groupBy('products.id')
!'having(DB!%raw('SUM(order_items.total_price)'), '!$', 990);
It groups the rows by product ID and then adds a having clause using the sum of total
prices. Since it has a group by we need to use having instead of where .
8 / 123
Martin Joo - Laravel Concepts - Part II
If you set up relationships correctly (a Product has many OrderItem in this case) you can
use the withSum helper:
return Product!%query()
!'withSum('order_items as total_revenue', 'total_price')
!'having('total_revenue', '!$', 990);
9 / 123
Martin Joo - Laravel Concepts - Part II
This filter is very similar to the previous one. This is the SQL query:
select
`products`.*,
(
select avg(`product_ratings`.`rating`)
from `product_ratings`
where `products`.`id` = `product_ratings`.`product_id`
) as `avg_rating`
from `products`
group by `products`.`id`
having `avg_rating` !$ 4
order by `avg_rating` desc
return Product!%query()
!'withAvg('ratings as avg_rating', 'rating')
!'having('avg_rating', '!$', 4);
10 / 123
Martin Joo - Laravel Concepts - Part II
$numberOfProducts = Product!%count();
$averageRevenue = Order!%sum('total_price') /
$numberOfProducts;
$averageRating = ProductRating!%avg('rating');
$averageNumberOfRatings = ProductRating!%count() /
$numberOfProducts;
It's very straightforward. After we have these values we can apply them in the actual query:
Product!%query()
!'withSum('order_items as total_revenue', 'total_price')
!'withAvg('ratings as avg_rating', 'rating')
!'withCount('ratings as count_ratings')
!'having('count_ratings', '!$', $averageNumberOfRatings)
!'having('avg_rating', '!$', $averageRating)
!'having('total_revenue', '!$', $averageRevenue);
We'll get back to filters soon, but first, let's discuss the sorters.
11 / 123
Martin Joo - Laravel Concepts - Part II
Sorters
Revenue Contribution Sorter
This sorter will sort products by their revenue contribution. If the total revenue is $1000 and
'Product A' made $300 in sales while Product B made $700, then the contributions are:
Product A: 30%
Product B: 70%
$totalRevenue = Order!%sum('total_price');
Product!%query()
!'selectRaw("SUM(order_items.total_price) / $totalRevenue as
revenue_contribution")
!'join('order_items', 'products.id', '=',
'order_items.product_id')
!'groupBy('products.id')
!'orderBy('revenue_contribution');
"SUM(order_items.total_price) / $totalRevenue as
revenue_contribution"
It sums up the order items associated with a product and then divides this number by the
total revenue. Since the $totalRevenue is a variable outside of the query, I choose to use
a join , since it's the most simple way to write this query.
12 / 123
Martin Joo - Laravel Concepts - Part II
This will sort the products based on their average ratings. This and the revenue sorter are
the most simple ones:
!( Average rating
Product!%query()
!'withAvg('ratings as avg_rating', 'rating')
!'orderBy('avg_rating');
!( Total revenue
Product!%query()
!'withSum('order_items as sum_revenue', 'total_price')
!'orderBy('sum_revenue');
13 / 123
Martin Joo - Laravel Concepts - Part II
Strategy
If you think about it all filters are the same, and so as all sorters. They do the same thing,
but with a different "strategy." For example, the revenue filter and average rating filter. They
both filter out products based on some values. This means they can have the same
interface but different implementations. The same is true for sorters.
Before we jump into the details, let's imagine how we want to use these classes. Let's say
we follow the JSON API standard, so the request URL looks something like that:
/api/products?filter[revenue]=90&filter[avg_rating]=3.7
This request means the user wants to see every product that has:
class ProductController
{
public function index(Request $request)
{
/**
* [
* 'revenue' !) 90,
* 'avg_rating' !) 3.7,
* ]
!#
$filters = $request!'collect('filters');
!( select * from products
14 / 123
Martin Joo - Laravel Concepts - Part II
$query = Product!%query();
foreach ($filters as $name !) $value) {
!( Each filter has a class such as RevenueFilter
$filter = FilterFactory!%create($name, $value);
/**
* Each filter class will append
* clauses to the base query
!#
$filter!'handle($query);
}
return $query!'get();
}
}
So I'd like to see a simple for loop where we can go through the filters, create a class based
on the filter's name, and then call a function. Something like $filter->handle($query)
and each of these handle calls will append the appropriate withSum , withAvg , and
having clauses to the base query.
From this little example, we know how to filter interface will look like:
15 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Filters;
use Illuminate\Contracts\Database\Eloquent\Builder;
namespace App\Filters;
16 / 123
Martin Joo - Laravel Concepts - Part II
Whenever you need a new implementation (a new filter in this case) you just have to
add a new class. So you don't have to modify existing classes and functions. The only
place you need to change is the factory (we'll talk about it in a minute). This is the letter
"O" in SOLID.
Separation of concerns. Each filter has its own class. This is a huge benefit in a large
application! Just think about the MostPopularFilter . If it's going to change in the
future (and gets much more complicated) there's a guarantee that you won't cause
bugs in other filters. They are completely separated.
Single responsibility. Each filter class does only one thing: filter by X criteria. They don't
know anything about the outside world, they just do their well-defined job. This is the
letter "S" in SOLID.
Easy interaction. The controller in the above example only interacts with the interface of
the Filter abstract class. It doesn't matter if the user wants to order by revenue or
ratings. It doesn't matter. The interaction is always the same. This is the letter "D" in
SOLID.
Small interface. Since filter classes are so well-defined they only require one method
and a constructor to work. It's easy to use, and easy to maintain. This is the letter "I"
from SOLID.
From a practical point of view the biggest benefit is that if you need to handle a new kind of
filter, you just create a new class from scratch, and add a new line to a factory.
17 / 123
Martin Joo - Laravel Concepts - Part II
Enums
I talked about factories on the previous pages but in fact, in PHP 8.1 we can use enums to
achieve the same. For example, this is the Filters enum:
namespace App\Enums;
Fortunately, enums can contain methods, and the createFilter will behave just like a
factory. It can be used such as:
$filter = Filters!%from('revenue')!'createFilter(99);
$filter!'handle();
18 / 123
Martin Joo - Laravel Concepts - Part II
As you can imagine, the values 'revenue' and 99 will come from the request and will be
handled by the controller.
namespace App\Enums;
It follows the same logic. The only difference is that a sorter doesn't need a value. It only
needs a direction (asc or desc). This is what the SortDirection is:
19 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Enums;
By the way, I didn't list it here but the Sorter class and the subclasses follow exactly the
same structure as the Filter classes. They have only one method that takes a query and
modifies it (adds the order by clause to it).
20 / 123
Martin Joo - Laravel Concepts - Part II
Pipelines
Right now, with the Filter Sorter class and enums we can write something like this:
class ProductController
{
public function index(Request $request)
{
$filters = $request!'collect('filters');
$query = Product!%query();
foreach ($filters as $name !) $value) {
$filter = Filters!%from($name)!'createFilter($value);
$filter!'handle($query);
}
return $query!'get();
}
}
It's very clean. However, we can use Laravel pipelines to make it even more "generic:"
21 / 123
Martin Joo - Laravel Concepts - Part II
use Illuminate\Pipeline\Pipeline;
class ProductController
{
public function index(Request $request)
{
$filters = $request!'collect('filters')
!'map(fn (int $value, string $name) !)
Filters!%from($name)!'createFilter($value)
)
!'values();
return app(Pipeline!%class)
!'send(Product!%select('products.*'))
!'through($filters)
!'thenReturn()
!'get();
}
}
A pipeline has multiple "stops" or pipes. These pipes are classes that do something with
the initial value. In this example, these pipes are the filter classes, and the initial value is a
product query builder object. You can of the whole flow like this:
22 / 123
Martin Joo - Laravel Concepts - Part II
After we have the builder, the get will run the actual query and returns a collection.
First of all, it's not better. It's just a different approach. However, I think it has one
advantage. A foreach is very "hackable." What I mean by this is that it encourages
developers to write if-else statements, nested loops, break statements, and other "stuff" to
handle edge cases, or apply a "quick fix" here and there. By using a pipeline you cannot do
any of those! The whole flow is "closed." So if you need a "quick fix" because your new
Filter class is not compatible with the current architecture you have to think about why is it
the case, and how you can solve it. You need to make it compatible with the current
solution, or you need to restructure the existing filters and drop the pipeline approach. So I
think using a Pipeline helps us follow the Open-Closed Principle from SOLID. It's open for
new filters but closed for "quick fixes."
namespace App\Filters;
use Closure;
23 / 123
Martin Joo - Laravel Concepts - Part II
The handle method now takes a second argument called $next and returns a Builder
instance. The $next is very similar to a middleware. Each middleware calls the next one
via a closure. The same applies here as well. Each pipe triggers the next one by invoking
the $next closure:
namespace App\Filters;
use Closure;
return $next($query);
}
}
And it also needs to return the result of $next . The same logic applies to Sorter classes.
Now that we have everything ready it's time to implement the controller and an action.
24 / 123
Martin Joo - Laravel Concepts - Part II
api/products
?filter[revenue]=90
&filter[avg_rating]=3.7
&sort=revenue
&sort_direction=asc
Which means:
namespace App\Http\Controllers;
use App\Actions\FilterProductsAction;
use App\Http\Requests\GetProductsRequest;
25 / 123
Martin Joo - Laravel Concepts - Part II
Those getters in the request convert strings to enums and apply some default values:
return SortDirections!%from($this!'sort_direction);
}
return Sorters!%from($this!'sort);
26 / 123
Martin Joo - Laravel Concepts - Part II
}
}
The last piece of the puzzle is the FilterProductsAction . You have already seen this
class without knowing it. This is the one where construct the pipeline:
namespace App\Actions;
class FilterProductsAction
{
/**
* @param Collection<string, int> $filterValues
* @return Collection<Product>
!#
public static function execute(
Collection $filterValues,
Sorters $sort,
SortDirections $sortDirection
): Collection {
$filters = $filterValues
!'map(fn (int $value, string $name) !)
Filters!%from($name)!'createFilter($value)
)
!'values();
return app(Pipeline!%class)
!'send(Product!%select('products.*'))
!'through([
!!+$filters,
27 / 123
Martin Joo - Laravel Concepts - Part II
$sort!'createSorter($sortDirection)
])
!'thenReturn()
!'get();
}
}
[
'revenue' !) 90,
'avg_rating' !) 3.7,
];
So it contains the filter names and the values associated with them. In the pipeline setting
there's only one new thing, this line:
!'through([!!+$filters, $sort!'createSorter($sortDirection)])
Here we want to send the query through not only the filter but also the sorter. This is why I
create a new array that contains both.
28 / 123
Martin Joo - Laravel Concepts - Part II
So what are these boundaries and why did I leave them out of the book? In domain-driven
design we have domains. A domain is something like a module. It groups classes together.
But only classes that are related to each other.
Let's see an example. We are working on a project management application where we can
track our time and bill a customer. So the app has models like these:
29 / 123
Martin Joo - Laravel Concepts - Part II
Class Group
Project Project
Milestone Project
Task Project
Timelog Project
Invoice Invoice
InvoiceLineItem Invoice
Client Client
30 / 123
Martin Joo - Laravel Concepts - Part II
These groups are the domains in the application. The models and other classes inside the
'Project' domain can be grouped together and isolated from the other domains. These
classes are responsible for handling project management-related user stories. Nothing else
matters to them (Metallica shout-out).
And each domain contains only business logic related classes. This means no
controllers, commands, migrations, or anything specific to the application or infrastructure.
For example, a controller is specific to a web or API application, meanwhile, a command is
specific to a console application. They can only exist in the context of those applications.
On the other hand, the Project model does not care about if it's being used by a
command, API controller, web controller, Inertia controller, migration, SPA, or MVC app. It's
context-independent by nature. So it lives inside the domain folder:
31 / 123
Martin Joo - Laravel Concepts - Part II
These are domains in a nutshell. However, in this article, I'd like to focus on boundaries
specifically. If this is the first time you hear about domains and applications you can read
this article which describes them in great detail.
32 / 123
Martin Joo - Laravel Concepts - Part II
Project -> has many -> Milestone -> has many -> Task -> has many -> Timelog
Project -> has many Invoice
Invoice -> belongs to one -> Project
Invoice -> has many -> InvoiceLineItem
Project -> belongs to one -> Client
namespace Domain\Invoice\Actions;
class CreateMilestoneInvoiceAction
{
public function execute(Milestone $milestone): Invoice
{
$invoice = Invoice!%create([
'client_id' !) $milestone!'project!'client_id,
'client_name' !) $milestone!'project!'client!'full_name,
'project_id' !) $milestone!'project_id,
]);
$invoiceAmount = 0;
33 / 123
Martin Joo - Laravel Concepts - Part II
$lineItem = $invoice!'addLineItem($task);
$invoiceAmount += $lineItem!'total_amount;
}
$invoice!'total_amount = $invoiceAmount;
$invoice!'save();
return $invoice;
}
}
return InvoiceLineItem!%create([
'invoice_id' !) $this!'id,
'task_id' !) $task!'id,
'item_name' !) $task!'name,
'item_quantity' !) $hoursLogged,
'total_amount' !) $hoursLogged * 30,
]);
}
}
This is a very straightforward class. This is the usual Laravel code you're probably used to.
It violates boundaries at least four times:
34 / 123
Martin Joo - Laravel Concepts - Part II
Violation #1: This class is inside the Invoice domain, so it should not access the
Milestone model directly.
Violation #2: It should not use the project relationship from the Milestone model,
and the client relationship from the Project model. We are exposing too much
information to the Invoice domain. It should not know anything about the inner
structures of models from another domain.
Violation #3: The Invoice model should not access the Task model and its
timelogs relationship. Same problem as Violation #2.
Violation #4: Under the hood, the client_id and project_id columns in the
invoices table are foreign keys to the clients and projects table. A table in the
invoice domain should not reference another table from the client or project domain.
So if you're writing code like this, you will never enter the gates of DDD Walhalla. Now let's
make this code DDD-compatible.
35 / 123
Martin Joo - Laravel Concepts - Part II
First, let's get rid of the Milestone argument. We cannot pass models across domains.
Fortunately, we can express every model as a DTO or data transfer object.
namespace Domain\Project\DataTransferObjects;
use Spatie\LaravelData\Data;
36 / 123
Martin Joo - Laravel Concepts - Part II
),
'tasks' !) Lazy!%whenLoaded(
'tasks',
$milestone,
fn () !) TaskData!%collection($milestone!'tasks)
),
]);
}
}
In this example, I use the laravel-data package by Spatie. If you're confused about
these Lazy things, please check out the documentation, but it's not important for this
article. In a nutshell, Lazy::whenLoaded is very similar to Laravel's Resource whenLoaded
function. So in the example above, the project will only be included if the relationship is
already eager-loaded in the $milestone instance. It helps us to avoid N+1 query
problems.
namespace Domain\Invoice\Actions;
use Domain\Project\DataTransferObjects\MilestoneData;
class CreateMilestoneInvoiceAction
{
public function execute(MilestoneData $milestone)
{
!(!!+
}
}
37 / 123
Martin Joo - Laravel Concepts - Part II
So after this small refactor we're no longer exposing too much information from the project
domain. DTOs are meant to be transferring data between components. They don't have
functions like:
delete
update
relationships
They don't contain every column and behavior such as a Model. So it's a lot safer to use
them, and it's harder to write fragile applications.
38 / 123
Martin Joo - Laravel Concepts - Part II
Violation #2
Earlier we used the milestone's project relationship to access client information. Instead of
accessing the client directly through relationships, we have to introduce a ClientService
that takes an integer ID and returns a ClientData DTO:
namespace Domain\Client\Services;
class ClientService
{
/**
* @throws ClientNotFoundException
!#
public function getClientById(int $clientId): ClientData
{
$client = Client!%find($clientId);
if (!$client) {
throw new ClientNotFoundException(
"Client not find with id: $clientId"
);
}
return ClientData!%from($client);
}
}
39 / 123
Martin Joo - Laravel Concepts - Part II
With this class we can eliminate the relationships from the action:
namespace Domain\Invoice\Actions;
class CreateMilestoneInvoiceAction
{
public function !*construct(
private readonly ClientService $clientService
) {}
$invoice = Invoice!%create([
'client_id' !) $client!'id,
'client_name' !) $client!'full_name,
'project_id' !) $milestone!'project!'resolve()!'id,
]);
!(!!+
}
}
Now instead of accessing relationships and exposing a Client instance we only use DTOs
and an integer ID. The resolve function is laravel-data specific. It resolves the value
from a Lazy instance, so it just returns a ProjectData DTO.
40 / 123
Martin Joo - Laravel Concepts - Part II
Violation #3
The next violation was in the Invoice model. The addLineItem method accessed the
Task model and the Timelog model through a relationship. I think you can already guess
the solution: use DTOs.
namespace Domain\Invoice\Models;
return InvoiceLineItem!%create([
'invoice_id' !) $this!'id,
'task_id' !) $task!'id,
'item_name' !) $task!'name,
'item_quantity' !) $hoursLogged,
'total_amount' !) $hoursLogged * 30,
]);
}
}
So I changed the Task model to a TaskData DTO. The resolve and toCollection
methods come from the laravel-data package. It returns the timelogs as a Laravel
collection.
41 / 123
Martin Joo - Laravel Concepts - Part II
I won't list the solution to violation #3 but you can in the repository that I removed the
foreign keys that cross the boundaries. The resulting action looks like this:
namespace Domain\Invoice\Actions;
class CreateMilestoneInvoiceAction
{
public function !*construct(
private readonly ClientService $clientService
) {}
$invoice = Invoice!%create([
'client_id' !) $client!'id,
'client_name' !) $client!'full_name,
'project_id' !) $milestone!'project!'resolve()!'id,
]);
$invoiceAmount = 0;
$invoiceAmount += $lineItem!'total_amount;
42 / 123
Martin Joo - Laravel Concepts - Part II
}
$invoice!'total_amount = $invoiceAmount;
$invoice!'save();
return InvoiceData!%from($invoice!'load('line_items'));
}
}
Please notice that the action returns an InvoiceData instead of an Invoice model. This
is a general rule you should follow if you want to respect boundaries: most actions,
services, and repositories should return DTOs instead of models.
43 / 123
Martin Joo - Laravel Concepts - Part II
Conclusion
Why did I leave this technique out of the book? Because I truly believe it's overkill for 90%
of Laravel projects. Just think about it:
You cannot use relationships in some situations. In this example, laravel-data did a
good job, but in the long run, you'll miss out on a lot of Eloquent features. For example,
you cannot replace withAvg with a DTO. You have two options:
No foreign keys. I mean, you still can use them, but not if they violate your boundaries.
The lack of foreign keys and the fact that you'll have more N+1 problems will result in
very poor performance.
But here's the most important thing and the real reason why I left this out from the book.
It's easy to ruin a project with this approach. Using this approach is not natural in Laravel
or PHP. Try to implement this with six other developers where three of them are juniors. It's
almost like a guarantee for failure.
And it even gets worse. This was a simplified example of boundaries. In real DDD there are
three different concepts:
Domain
Subdomain
Bounded context
We only used domains, so it gets even harder to identify boundaries and abstractions. Even
DDD gods are arguing about these concepts. Just search for "bounded context" or
"bounded context vs domain" and you'll see that every Java/C# DDD developer has a
slightly different definition in their head.
44 / 123
Martin Joo - Laravel Concepts - Part II
In my opinion, if you're using the technical aspects of DDD and you pay attention to the
strategic design you'll do fine in a larger project. So instead of boundaries, I try to focus on
smaller concepts like:
DTOs
Value Objects
Services
Actions
Domains
Applications
And generally speaking, I'm trying to write code that reflects the business language and
domain. However, if you're working on Shopify-scale applications you almost definitely
need boundaries and those more advanced concepts. Just to be clear, when I say Shopify-
scale I'm not referring to the millions of users they have. I'm talking about the 2.8 million
lines of code and 500,000 commits in one monolith! These numbers come from their
engineering blog.
If you want to learn more about boundaries and monoliths, please check out this video from
Laracon 2022, presented by Ryuta Hamasaki. It's a great talk!
45 / 123
Martin Joo - Laravel Concepts - Part II
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. Here's an example:
class Percent
{
public readonly ?float $value;
public readonly string $formatted;
46 / 123
Martin Joo - Laravel Concepts - Part II
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.
That's it. This is a value object. It's an object that contains some values. The original
definition of a value object states two more things:
47 / 123
Martin Joo - Laravel Concepts - Part II
Data Modeling
To really understand value objects, we'll implement a very basic financial app. Something
like Seekingalpha, Morningstar, Atom Finance, or Hypercharts. If you don't know these
apps, here's a simplified introduction:
In the sample application, I'll only implement a handful of metrics, and I'll only store the
income statements (no balance sheets or cash flows). This is more than enough to illustrate
to use of value objects.
As you can see, it's quite easy. This is a sample row from the companies table:
48 / 123
Martin Joo - Laravel Concepts - Part II
price_per_share is the current share price of the company's stock. It's stored in cent
value, so 14964 is $149.64 . This is a common practice in order to avoid rounding
mistakes.
Each item on the income statement has its own column such as revenue or gross_profit.
One row in this table describes a year for a given company. And as you can probably guess,
these numbers are also in millions. So 386017 means $386,017,000,000 or $386B for
short.
If you're wondering why to store these numbers in millions, the answer is pretty simple: it's
easier to read. Just check out Apple's page on Seekingalpha, for example:
49 / 123
Martin Joo - Laravel Concepts - Part II
Each metric has its own column, and each row represents a year for a given company. Most
metrics are percentage values stored as decimals. The pe_ratio stands for
"price/earnings ratio." If a company's share trades at $260 and its earnings are $20 per
share, then the P/E ratio is 13.00. It's a decimal number stored as an integer.
Maybe you're asking "why not call it price_per_earnings_ratio?" It's a good question! In my
opinion, our goal as software developers should be to write code that is as close to the
business language as possible. But in the financial sector, nobody calls it "price per
earnings ratio." It's just the "PE ratio." So, in fact, this is the correct language, in my
opinion.
50 / 123
Martin Joo - Laravel Concepts - Part II
API
We want to implement three APIs.
GET /companies/{company}
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
It'll also return the price and market cap data in human-readable formats.
GET /companies/{company}/income-statements
51 / 123
Martin Joo - Laravel Concepts - Part II
{
"data": {
"years": [
2022,
2021
],
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
},
"eps": {
"2022": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
},
"2021": {
"cent": 620,
"dollar": 6.2,
"formatted": "$6.20"
}
}
52 / 123
Martin Joo - Laravel Concepts - Part II
}
}
The right data structure will heavily depend on the exact use case and UI. This structure is
pretty good for a layout similar to Seekingalpha's (the screenshot from earlier). This API also
formats the values.
GET /companies/{company}/metrics
{
"data": {
"years": [
2022
],
"gross_margin": {
"2022": {
"value": 0.43,
"formatted": "43.00%",
"top_line": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"bottom_line": {
"value": 167231000000,
"millions": 167231,
"formatted": "167,231"
}
}
53 / 123
Martin Joo - Laravel Concepts - Part II
},
"pe_ratio": {
"2022": {
"value": "24.32"
}
}
}
}
Each margin contains the top and bottom line information as well. In the case of gross
margin, the top line is the revenue and the bottom line is the gross profit.
54 / 123
Martin Joo - Laravel Concepts - Part II
Ratio. It's a simple number expressed as a float. Right now, the PE ratio is the only
ratio-type data in the app.
Margin. It has a raw value, a percentage, a top line, and a bottom-line value. Gross
margin, operating margin, and profit_margin will use this data type.
Price. It has a cent, dollar, and formatted value. Both price_per_share and eps
(which is earnings per share) use this data type.
Market Cap. It's a unique one because it has three different formats: 2.42T , 242B ,
and 577M . All of these are valid numbers to express a company's market
capitalization. When a company hits the trillion mark we don't want to use 1000B but
rather 1T . SO we need to handle these cases.
Millions. Every item in the income statement is expressed as millions so it makes sense
to use a value object called Millions .
Now, take a look at these value object names! We're working on a financial app, and we'll
have classes like Millions , Margin , or MarketCap .
This is the kind of codebase that makes sense. Even after five years.
55 / 123
Martin Joo - Laravel Concepts - Part II
Price seems the most obvious so let's start with that one. The class itself is pretty
straightforward:
class Price
{
public readonly int $cent;
public readonly float $dollar;
public readonly string $formatted;
56 / 123
Martin Joo - Laravel Concepts - Part II
Every value object has public readonly properties. readonly makes sure they are
immutable, while public makes them easy to access, so we don't need to write
getters or setters.
A lot of value object has a from factory function. It fits the overall style of Laravel very
well.
$company = Company!%first();
$price = Price!%from($company!'price_per_share);
The next question is: how do we use this object? There are two paths we can take:
Casting in models
We have at least two possible solutions to cast attributes to value objects in the models.
57 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
It's an excellent solution and can work 95% of the time. However, right we are in the
remaining 5% because we have 10+ attributes we want to cast. In the IncomeStatement
model we need to cast almost every attribute to a Millions instance. Just imagine how
the class would look like with attribute accessors:
namespace App\Models;
58 / 123
Martin Joo - Laravel Concepts - Part II
So in our case, using attribute accessors is not optimal. Fortunately, Laravel has a solution
for us! We can extract the casting logic into a separate Cast class:
namespace App\Models\Casts;
use App\ValueObjects\Price;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
59 / 123
Martin Joo - Laravel Concepts - Part II
}
}
get is called when you access a property from the model and it transforms the integer
into a Price object.
set is called when you set a property in the model before you save it. It should
transform a Price object into an integer. But as you can see, I just left it as is because
we don't need this for the example. If you return $value from the set method, Laravel
won't do any extra work. So there's no attribute mutation.
The last step is to actually use this Cast inside the Company model:
protected $casts = [
'price_per_share' !) PriceCast!%class,
];
}
60 / 123
Martin Joo - Laravel Concepts - Part II
$company = Company!%first();
!( $127.89
echo $pricePerShare!'formatted;
!( 127.89
echo $pricePerShare!'dollar;
!( 12789
echo $pricePerShare!'cent;
namespace App\Http\Resources;
61 / 123
Martin Joo - Laravel Concepts - Part II
}
}
Since these value objects contain only public properties Laravel will automatically transform
them into arrays when converting the response into JSON. So this resource will result in the
following JSON response:
{
"data": {
"id": 1,
"ticker": "AAPL",
"name": "Apple Inc.",
"price_per_share": {
"cent": 14964,
"dollar": 149.64,
"formatted": "$149.64"
},
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
}
}
This is how we can cast values in Eloquent models. But we can skip this setup and cast the
values directly inside resources.
62 / 123
Martin Joo - Laravel Concepts - Part II
Casting in resources
This is much more simple than the previous one. All we need to do is create a Price
object inside the resource:
namespace App\Http\Resources;
Now the Company model does not have any casts, so we just instantiate a Price and a
MarketCap object from the integer values.
However, if you only need these values in the API, then maybe you can skip the whole
Cast thing and just create a value object in resources.
But if you need these values to handle other use-cases as well it's more convenient to
use Eloquent casts. Some examples:
63 / 123
Martin Joo - Laravel Concepts - Part II
Notifications. For example, a new income statement just came out, and you want
to notify your users and include some key values in the e-mail. Another example
can be a price notification.
Queue jobs. For example, you need to recalculate price-dependent metrics and
values on a scheduled basis.
Broadcasting via websocket. For example, the price is updated in real-time on the
FE.
Each of these scenarios can benefit from using Eloquent Cast because otherwise
you end instantiating these value objects in every place.
In general, I think it's a good idea to use these objects in models. It makes your
codebase more high-level, and easier to maintain.
64 / 123
Martin Joo - Laravel Concepts - Part II
MarketCap
As discussed earlier, the market cap is a bit more unique, so it has its own value object. We
need this data structure:
"market_cap": {
"millions": 2420000,
"formatted": "2.42T"
}
The formatted property will change based on the market cap of the company, for
example:
"market_cap": {
"millions": 204100,
"formatted": "204.1B"
}
"market_cap": {
"millions": 172,
"formatted": "172M"
}
namespace App\ValueObjects;
65 / 123
Martin Joo - Laravel Concepts - Part II
class MarketCap
{
public readonly int $millions;
public readonly string $formatted;
!( Trillions
if ($millions !$ 1_000_000) {
$this!'formatted = number_format(
$this!'millions / 1_000_000, 2
) . 'T';
}
!( Billions
if ($millions < 1_000_000 !- $millions !$ 1_000) {
$this!'formatted = number_format(
$this!'millions / 1_000, 1
) . 'B';
}
!( Millions
if ($millions < 1_000) {
$this!'formatted = number_format($this!'millions) . 'M';
}
}
66 / 123
Martin Joo - Laravel Concepts - Part II
We need to check the value of $millions and do the appropriate division and use the
right suffix.
namespace App\Models\Casts;
Once again, we don't need to do anything in set . The last thing is to use this cast:
67 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Models;
protected $casts = [
'price_per_share' !) PriceCast!%class,
'market_cap' !) MarketCapCast!%class,
];
}
I won't list the other Cast classes because all of them are the same. You can check them
out in the repository.
68 / 123
Martin Joo - Laravel Concepts - Part II
Millions
namespace App\ValueObjects;
class Millions
{
public readonly int $value;
public readonly int $millions;
public readonly string $formatted;
$this!'millions = $millions;
69 / 123
Martin Joo - Laravel Concepts - Part II
As JSON:
"revenue": {
"2022": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
}
}
Millions is used in the IncomeStatement model, and this is where we benefit from using
Eloquent Casts :
70 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Models;
protected $casts = [
'revenue' !) MillionsCast!%class,
'cost_of_revenue' !) MillionsCast!%class,
'gross_profit' !) MillionsCast!%class,
'operating_expenses' !) MillionsCast!%class,
'operating_profit' !) MillionsCast!%class,
'interest_expense' !) MillionsCast!%class,
'income_tax_expense' !) MillionsCast!%class,
'net_income' !) MillionsCast!%class,
'eps' !) PriceCast!%class,
];
}
71 / 123
Martin Joo - Laravel Concepts - Part II
Margin
namespace App\ValueObjects;
class Margin
{
public readonly float $value;
public readonly string $formatted;
public readonly Millions $top_line;
public readonly Millions $bottom_line;
$this!'top_line = $topLine;
$this!'bottom_line = $bottomLine;
72 / 123
Martin Joo - Laravel Concepts - Part II
This shows another great feature of value objects: they can be nested. In this example, the
top_line and bottom_line attributes are Millions instances. These numbers describe
how the margin is calculated. For example, the gross margin is calculated by dividing the
revenue (top line) by the gross profit (bottom line). This will look like this in JSON:
"gross_margin": {
"2022": {
"value": 0.68,
"formatted": "68.00%",
"top_line": {
"value": 192557000000,
"millions": 192557,
"formatted": "192,557"
},
"bottom_line": {
"value": 132345000000,
"millions": 132345,
"formatted": "132,345"
}
}
}
73 / 123
Martin Joo - Laravel Concepts - Part II
However, if you take a look at the make method, you can see we expect two additional
parameters: $topLine and $bottomLine . This means we can use this object like this:
$company = Company!%first();
$incomeStatement = $company!'income_statements()
!'where('year', 2022)
!'first();
$grossMargin = Margin!%make(
$metrics!'gross_margin,
$incomeStatement!'revenue,
$incomeStatement!'gross_profit,
);
Since we are using Eloquent Casts we need the revenue and gross profit (in this specific
example) in the MarginCast class. We can do something like this:
74 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Models\Casts;
75 / 123
Martin Joo - Laravel Concepts - Part II
As you can see, the model, in this case, is a Metric model (this is where the cast will be
used) so we can query the appropriate income statement for the same year. After that, we
need a method that can return the top and bottom line for a particular metric:
namespace App\Models;
76 / 123
Martin Joo - Laravel Concepts - Part II
This method simply returns the right items from the income statement based on the metric.
The logic is quite simple, but it's much more complicated than the other ones, so I
recommend you to check out the source code and open these classes.
You may be asking: "Wait a minute... We are querying companies and income statements in
the MarginCast for every attribute??? That's like 10 extra queries every time we query a
simple Metric, right?"
Good question! The answer is: nope. These casts are lazily executed. This means the get
function will only be executed when you actually access the given property. But as you
might already guess we'll access every property in a resource, so a bunch of extra queries
will be executed. What can we do about it?
Eager load relationships when querying a metric. This will prevent us from running into
N+1 query problems.
Cache the income statements. After all, they are historical data, updated once a year.
This will also prevent extra queries.
If performance is still an issue, you can drop the whole MarginCast class, and use the
object in the resource directly. In this case, you have more flexibility. For example, you
can query every important data in one query, and only interact with collections when
determining the top and bottom line values.
77 / 123
Martin Joo - Laravel Concepts - Part II
PeRatio
After all of these complications, let's see the last and probably most simple VO:
namespace App\ValueObjects;
class PeRatio
{
public readonly string $value;
This class can also be used to cover other ratio-type numbers, but right now PE is the only
one, so I decided to call the class PeRatio .
78 / 123
Martin Joo - Laravel Concepts - Part II
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": {
"2022": {
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
"2021": {
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
}
}
}
79 / 123
Martin Joo - Laravel Concepts - Part II
class IncomeStatementResource
{
public $preserveKeys = true;
public function toArray(Request $request)
{
$data = [];
Arr!%set(
$data,
"items.{$attribute}.{$incomeStatement!'year}",
$incomeStatement!'{$attribute}
);
80 / 123
Martin Joo - Laravel Concepts - Part II
}
}
return $data;
}
}
Are you having a hard time understanding what's going on? It's not your fault! It's mine.
This code sucks. I mean, it's very "dynamic" so it'll work no matter if you have four columns
in the income_statements or 15. But other than that it seems a bit funky to me. Moreover,
it has no "real" form, so it's very weird to put it in a resource.
Don't get me wrong, sometimes you just need solutions like this. But an income statement
has a finite amount of items (columns), and it's not something that is subject to change.
namespace App\Http\Resources;
return [
'years' !) $years,
'items' !) [
81 / 123
Martin Joo - Laravel Concepts - Part II
'revenue' !) $this!'getItem(
'revenue',
$years
),
'cost_of_revenue' !) $this!'getItem(
'cost_of_revenue',
$years
),
'gross_profit' !) $this!'getItem(
'gross_profit',
$years
),
'operating_expenses' !) $this!'getItem(
'operating_expenses',
$years
),
'operating_profit' !) $this!'getItem(
'operating_profit',
$years
),
'interest_expense' !) $this!'getItem(
'interest_expense',
$years
),
'income_tax_expense' !) $this!'getItem(
'income_tax_expense',
$years
),
'net_income' !) $this!'getItem(
'net_income',
82 / 123
Martin Joo - Laravel Concepts - Part II
$years
),
'eps' !) $this!'getItem(
'eps',
$years
),
]
];
}
/**
* @return array<int, int>
!#
private function getItem(
string $name,
Collection $years
): array {
$data = [];
return $data;
}
}
83 / 123
Martin Joo - Laravel Concepts - Part II
Can you see the difference? It's easy to understand, readable has a real form, and does not
require more code at all (all right, in this PDF it seems much longer, but in the repository,
each item is one line). However, it's called IncomeStatementsSummaryResource , and
there's a reason why. This resource requires a Collection<IncomeStatement> so it can be
used like this:
namespace App\Http\Controllers;
We pass all the income statements of a company as a Collection. So this line in the
resource won't run additional queries:
Without this Laravel will override the array keys and it'll convert the years to standard zero-
based array indices:
84 / 123
Martin Joo - Laravel Concepts - Part II
"data": {
"years": [
2022,
2021
],
"items": {
"revenue": [
{
"value": 386017000000,
"millions": 386017,
"formatted": "386,017"
},
{
"value": 246807000000,
"millions": 246807,
"formatted": "246,807"
}
]
}
}
As you can see the year-based object becomes a JSON array. This is why I used the
$preserveKeys property from the parent JsonResource class.
85 / 123
Martin Joo - Laravel Concepts - Part II
Metrics Summary
The metrics summary API is basically the same as the income statement. So not
surprisingly the Resource looks almost the same:
namespace App\Http\Resources;
return [
'years' !) $years,
'items' !) [
'gross_margin' !) $this!'getItem(
'gross_margin',
$years
),
'operating_margin' !) $this!'getItem(
'operating_margin',
$years
),
'profit_margin' !) $this!'getItem(
'profit_margin',
$years
),
86 / 123
Martin Joo - Laravel Concepts - Part II
'pe_ratio' !) $this!'getItem(
'pe_ratio',
$years
),
]
];
}
return $data;
}
}
87 / 123
Martin Joo - Laravel Concepts - Part II
namespace App\Http\Controllers;
88 / 123
Martin Joo - Laravel Concepts - Part II
Conclusion
It was a longer exclusive, I know. Give it some time, maybe read it again later.
Value objects are awesome, in my opinion! I almost use them in every project, no matter if
it's old, new, DDD, or not DDD, legacy, or not. It's pretty easy to start using them, and you'll
have a very high-level, declarative codebase.
I often got the question: "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 and percents. As we've seen.
Email addresses.
GPS coordinates.
EndDate and StartDate. They can be created from a Carbon but ensure that a StartDate
is always at 00:00:00 meanwhile an EndDate is always at 23:59:59.
89 / 123
Martin Joo - Laravel Concepts - Part II
Static Analysis
In general, a static analysis tool helps you avoid:
Bugs
Too complex methods and classes
Lack of type-hints
Poorly formatted code
There are an infinite amount of tools out there, but in the following pages, I'd like to show
my three favorite tools.
90 / 123
Martin Joo - Laravel Concepts - Part II
phpinsights
This is my number #1 favorite tool. It gives you an output like this:
Code. It checks the general quality of your code. It doesn't involve any style of format,
but only quality checks such as:
91 / 123
Martin Joo - Laravel Concepts - Part II
By default, it doesn't require any configuration at all. Of course, if you don't want to use
some rules, you can disable them. Check out the documentation for more information.
phpinsights can be run by issuing this command:
./vendor/bin/phpinsights analyse
It gives you an excellent summary and an interactive terminal where you can see every
issue. But it also ships with a 'non-interactive' mode, and you can also define the minimum
scores you want to have:
92 / 123
Martin Joo - Laravel Concepts - Part II
The --no-interaction flag means that the terminal window does not expect any input; it
just gives you the summary and every error message. The other --min-xy flags make it
possible to define the minimum scores for each category. For example, if the complexity
score drops below 90%, the command will yield a non-zero output and an error message.
The minimum complexity score is always 90% for me.
93 / 123
Martin Joo - Laravel Concepts - Part II
larastan
Larastan is a Laravel specific tool built on top of phpstan. These two use the same
configuration format and rule system. It ships with a default ruleset (very strict) and has a
config parameter called level . This parameter determines how strict it is and how many
rules are applied. If you want to learn more about these rules, check the documentation.
includes:
- ./vendor/nunomaduro/larastan/extension.neon
parameters:
paths:
- app
- src
level: 5
ignoreErrors:
- '#PHPDoc tag @var#'
excludePaths:
- 'app/Http/Kernel.php'
- 'app/Console/Kernel.php'
- 'app/Exceptions/Handler.php'
checkMissingIterableValueType: false
noUnnecessaryCollectionCallExcept: ['pluck']
94 / 123
Martin Joo - Laravel Concepts - Part II
level goes from 1..9. Basically, you need to experiment with what level is best for
you, but here's my general rule:
Legacy project: start with 1. In my opinion, you have no other options if the static
analysis is new to the project.
Fresh application: somewhere between 4 and 6, but it heavily depends on the team
and the project.
Never reach for level 9. Seriously, it gets pretty hard above level 5. My all-time best
was level 7, and I was dying during the process. It's like a tough game where you
cannot beat the final boss.
Under the excludePaths you can list files or directories that you want to exclude.
Sometimes I exclude default Laravel files such as the ones above.
You can browse the full config in the phpstan.neon file (root directory of the sample app).
./vendor/bin/phpstan analyse
95 / 123
Martin Joo - Laravel Concepts - Part II
Laracheck
I'm a bit biased toward Laracheck because it's my product so this chapter is going to be an
evil sales pitch
It's a code review tool available on your GitHub repos and it performs the following check
when you open a PR:
Anytime you write a foreach loop or call a Collection method it will look for potential N+1
problems.
You access a relationship that is not eager-loaded either in the body of the current
function (using with() or load()) or in the model itself (using the $with property).
You call DB functions in the loop such as DB::table()
You call static Model functions in the loop such as Product::find()
You call Model functions in the loop such as $product->save()
Incorrect dependencies
There are different layers in every Laravel application. Layers such as HTTP, Business Logic,
Database, etc. Each layer has its own dependencies. For example, the database layer
should not depend on the HTTP layer. If it does, Laracheck will show you a warning.
96 / 123
Martin Joo - Laravel Concepts - Part II
Job HTTP
Command HTTP
Service HTTP
As you can see, one of the most common issues I used to face is when a class depends on
an HTTP-related class. Such as a model using a request. It's a bad practice in my opinion
because we couple the transportation layer (HTTP) to the database layer. One of the
problems it causes is the lack of reusability. For example, you cannot use this model
function from a command or job because they don't have requests.
The inner layers of your application (such as models) should not depend on outer layers
(such as HTTP).
There are some typical classes that should not contain too much business logic since their
main purpose is to hold data. These classes are:
Resources
Requests
DataTransferObjects (DTO)
Value Objects
Mail
Notification
If you have a class that contains too much business logic, Laracheck will warn you. "Too
much" means that the cyclomatic complexity of the class is larger than 3.
97 / 123
Martin Joo - Laravel Concepts - Part II
In Laravel, it's a best practice to use env('MY_ENV_VALUE') calls only in config files. There
are two reasons.
Often config values are cached in production environment using the php artisan
config:cache command. If you don't know about this command, you should consider
using it. It'll cache every config file into memory. So whenever you use them with
config('app.my_value') it'll retrieve the value from memory instead of touching the
.env file on the disk.
If you have env() calls in your code (outside of config files), this config caching can break
your production environment! Or at least it can cause bugs.
The other reason is that config values can be "mocked" in tests pretty easily. All you have
to do is this:
98 / 123
Martin Joo - Laravel Concepts - Part II
/** @test !#
public function it_should_return_n_products()
{
$products = $this!'getJson(route('products.index', [
'per_page' !) 20
]))
!'json('data');
$this!'assertCount(20, $products);
}
}
This way you can test multiple config values, you can easily turn on and off feature flags,
and so on.
I'm not gonna go into more detail about the other checks but here's a list of the most
important ones:
99 / 123
Martin Joo - Laravel Concepts - Part II
Custom Checks
100 / 123
Martin Joo - Laravel Concepts - Part II
deptrac
This tool helps you to clean up your architecture. In the book, I used several classes. The
important ones are:
Controllers
Action
ViewModels
Builders
Models
DTOs
Each class has its purpose. For example, I don't want a controller to start implementing
business logic. It has three responsibilities, in my opinion:
Accepting a request.
Calling the necessary methods from another class.
Returning a response.
Another important rule is to keep the models lightweight. If the models are sending
notifications, dispatching jobs, or calling APIs, it's a bad design, in my opinion.
If you think about it, these architectural rules can be enforced by simply defining which
class a Model can reference, right? Something like that:
deptrac does precisely that. If these rules are not followed, and a Model uses a Job deptrac
will throw a huge red screen in your face.
101 / 123
Martin Joo - Laravel Concepts - Part II
parameters:
paths:
- ./app
- ./src
exclude_files:
- '#.*test.*#'
- '#.*Factory\.php$#'
layers:
- name: Action
collectors:
- type: className
regex: .*Actions\\.*
This tells deptrac that the project has a layer called Action, and the files can be collected
using this regex .*Actions\\.* It means that every file inside the Actions folder is an
action class. After the layers are created, we can define the rulesets:
ruleset:
Controller:
- Action
- ViewModel
- Model
- DTO
- ValueObject
Action:
- Event
- Model
- DTO
102 / 123
Martin Joo - Laravel Concepts - Part II
- Builder
- ValueObject
Model:
- Builder
- Model
- DTO
- ValueObject
DTO:
- Model
- DTO
- ValueObject
ValueObject:
- ValueObject
Other models.
Query builders.
DTOs.
Value objects.
If anything else is referenced inside a model, it will throw an error. You can find the config in
the deptrac.yaml inside the sample application. If you want to run it, just run this
command:
./vendor/bin/deptrac analyse
So these are my favorite static analysis tools. But the true power comes when you integrate
these tools into your CI/CD pipeline.
I highly recommend using this tool in new projects. It requires only 15-30 minutes to set up,
but it provides value for the next 3-5 years. Also, if you have a legacy project that you want
to clean up, this package can be really helpful!
103 / 123
Martin Joo - Laravel Concepts - Part II
git
terragrunt/terraform
These are programs that are installed on the host machine (or in the Dockerfile) but don't
have an SDK or a function such as file_get_contents() .
In these cases, we can use the amazing Symfony component called Process:
use Symfony\Component\Process\Process;
$process!'run();
The constructor of the Process class takes an array. Each element is a part of the
command as you can see.
To get the output of the process we can use the getOutput method:
104 / 123
Martin Joo - Laravel Concepts - Part II
use Symfony\Component\Process\Process;
$process!'run();
$output = $process!'getOutput();
It returns a string and it contains the exact output from the git process. This is the same
that you see in your terminal.
use Symfony\Component\Process\Process;
if (!$process!'isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process!'getOutput();
105 / 123
Martin Joo - Laravel Concepts - Part II
These are the basics of the Process component. Now we have everything to create a
general GitService that can run anything:
namespace App\Services;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class GitService
{
/**
* @param array $command
* @return string
* @throws ProcessFailedException
!#
public function runCommand(array $command): string
{
$process = new Process($command);
$process!'run();
if (!$process!'isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process!'getOutput();
}
}
106 / 123
Martin Joo - Laravel Concepts - Part II
If you look at this class it's a little bit weird. We call it GitService but is has no git-specific
logic. Even worse, we need to pass the word git as an argument. The problem is that
GitService is not a real GitService at this point. It's just a generic process wrapper or
something like that.
class GitService
{
public function pull(): string
{
return $this!'runCommand(['git', 'pull']);
}
return $this!'runCommand(
['git', 'commit', '-m', $message]
);
}
107 / 123
Martin Joo - Laravel Concepts - Part II
public function push(): string
{
return $this!'runCommand(['git', 'push']);
}
}
Now it's much better. Each git command has its own method which is a good practice in
my opinion. After these changes, we don't really want to access the runCommand method
outside of this class so it can be private.
Another minor problem you might notice is that we are using arrays just because Symfony
Process expects us to pass arrays:
However, parsing the command becomes tricky. For example, consider this:
108 / 123
Martin Joo - Laravel Concepts - Part II
We want to make an array from this string that looks like this:
[
"git",
"commit",
"-m",
"Add git service",
]
We can use the explode function, but if the message contains spaces the result looks like
this:
[
"git",
"commit",
"-m",
"'Add",
"git",
"service'",
]
And the command will fail. So using strings might look 7% better, it just doesn't worth the
potential bugs and complexity in my opinion.
Another great feature of the Process class is that it can give us real-time output. It's pretty
useful when you're working with long-running processes (such as terragrunt init or
apply ). To get logs as they come, we can use a loop:
109 / 123
Martin Joo - Laravel Concepts - Part II
$process!'start();
So if you ever need to work with OS processes, just forget about exec and go with
Symfony Process. It's a great component!
110 / 123
Martin Joo - Laravel Concepts - Part II
protected $casts = [
'published_at' !) 'datetime',
];
111 / 123
Martin Joo - Laravel Concepts - Part II
/**
* Create a new Eloquent query builder for the model.
*
* @param \Illuminate\Database\Query\Builder $query
* @return \Illuminate\Database\Eloquent\Builder|static
!#
public function newEloquentBuilder($query)
{
return new Builder($query);
}
If you check the base Model class it doesn't have methods like where , whereBetween , or
anything like that. All of these functions come from the Builder class. When you write
your query, for example, Article::where(...) Laravel first calls the
newEloquentBuilder method. It returns a Builder instance which has functions such as
112 / 123
Martin Joo - Laravel Concepts - Part II
where .
Since the newEloquentBuilder method is defined in the Model class, we can override it:
use App\Builders\ArticleBuilder;
protected $casts = [
'published_at' !) 'datetime',
];
And we can create a class called ArticleBuilder that extends the base Builder class:
113 / 123
Martin Joo - Laravel Concepts - Part II
<?php
namespace App\Builders;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
114 / 123
Martin Joo - Laravel Concepts - Part II
Scopes
Did you know that model scope is just syntactic sugar around query builders? Here's how
you can use them without magic:
I like to start every scope with where because it seems more expressive in a query. The
important thing is that you have to return an ArticleBuilder instance from every method
since we want to chain these methods. Notice that there is no get() or all() or anything like
that after the where() calls.
115 / 123
Martin Joo - Laravel Concepts - Part II
class ArticleController
{
public function myArticles(Request $request)
{
return Article!%query()
!'whereAuthor($request!'user)
!'wherePublished()
!'orderBy('created_at', 'desc')
!'get();
}
}
In the ArticleBuilder class you have no limitations, you can build any query you want.
Here's one with some where groups:
116 / 123
Martin Joo - Laravel Concepts - Part II
Queries
Of course, you don't have to write only scope-like functions that are chainable. Here's a
standard query:
This method simply returns a list of Articles just as a regular model query would be:
$oldArticles = Article!%getOldArticles();
117 / 123
Martin Joo - Laravel Concepts - Part II
$articles = Article!%query()
!'wherePublished()
!'orderByRatings()
!'get();
All the relationship aggregate functions are available such as withCount or withAvg .
One important thing though. If you want to write a method that manipulates a concrete
Article record, you need to do this:
118 / 123
Martin Joo - Laravel Concepts - Part II
$this!'model!'published_at = now();
$this!'model!'save();
}
}
So in a Builder you can access the model instance as $this->model . The publish
function can be used in a straightforward way:
$article = Article!%first();
$article!'publish();
119 / 123
Martin Joo - Laravel Concepts - Part II
Separation
Custom query builders are a great way to make your models smaller and simpler. However,
all we did in this example, is we moved code from the Article class to the
ArticleBuilder class. As you can imagine, in the long term the result will be the same,
but in this case, the ArticleBuilder will become a huge class.
By "static" I mean functions that don't interact with one particular record such as these
functions:
120 / 123
Martin Joo - Laravel Concepts - Part II
$this!'published_at = now();
$this!'save();
}
$this!'save();
}
}
121 / 123
Martin Joo - Laravel Concepts - Part II
$articles = Article!%query()
!'whereAuthor($request!'user())
!'wherePublished();
Or another approach would be to write your queries inside Builder classes and use
Actions or Services to handle user stories. This way, your models only represent a
record in the database without any business logic.
122 / 123
Martin Joo - Laravel Concepts - Part II
Final Words
Thank you very much for reading this book! I hope you liked it. If you have any question just
send me an e-mail and I try to reply as soon as possible.
If you want to learn more about Laravel and software engineering in general, check out my
blog. I also published other books:
123 / 123