Design Patterns in Laravel: Builder Pattern Example

A lot of people want to learn design patterns in Laravel and PHP. What if I told you there's a pattern that you already use daily, without even noticing? Let's take a look at an example of a Builder pattern.

Generally, for learning design patterns in PHP, I suggest this great resource, but today I want to show you that you don't need to understand the fancy theory, to use some patterns. Take a look at this code.

1$user = User::query()
2 ->where('is_active', 1)
3 ->orderBy('updated_at', 'desc')
4 ->first();

Wait, it's a typical Eloquent query, it's not any Builder pattern, right? Well, not exactly. You see, this pattern is exactly about that - about building an object, adding methods to it, like in the example above.

So, we have an Eloquent model, and then we're building the query on top of it, finally calling the ->first() to retrieve the data.

If we take a look at the source of the ->where() method, we find this:

1/**
2 * Add a basic where clause to the query.
3 *
4 * @param \Closure|string|array|\Illuminate\Database\Query\Expression $column
5 * @param mixed $operator
6 * @param mixed $value
7 * @param string $boolean
8 * @return $this
9 */
10public function where($column, $operator = null, $value = null, $boolean = 'and')
11{
12 if ($column instanceof Closure && is_null($operator)) {
13 $column($query = $this->model->newQueryWithoutRelationships());
14 
15 $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
16 } else {
17 $this->query->where(...func_get_args());
18 }
19 
20 return $this;
21}

As you can see, it called some methods and returns the object itself: return $this;. This is exactly the behavior of the Builder pattern. And isn't it a coincidence that it's called a Query Builder?

So, essentially, you use a Builder pattern, if you have a class with internal methods that change the behavior of the class object and return the object itself.

It means that when using the class, you can chain the methods, building more and more functionality of the object.

Ok, cool, now you understand that Query Builder is following a Builder pattern, so what?

The thing is that you can choose to adopt this pattern in your classes.

Let's take a look at an example of a Service class to calculate the price.

app/Services/PricingService.php:

1class PricingService {
2 
3 function calculateTotalPrice($orderPrice, $discount, $taxPercent, $shippingFee) {
4 // calculations
5 }
6}

Then you would call this Service class in some Controller:

1public function show(Order $order, PricingService $service) {
2 $totalPrice = $service->calculateTotalPrice(
3 $order->price,
4 $order->discount,
5 $order->taxPercent,
6 $order->shippingFee
7 );
8}

But what if you have more parameters in the future? It's not fancy to have 5th, 6th, and other parameters, right?

Instead, you may refactor those parameters as their own setter methods, returning the same PricingService object.

1class PricingService {
2 
3 public $orderPrice;
4 public $discount;
5 public $taxPercent;
6 public $shippingFee;
7 
8 public function setOrderPrice($orderPrice): PricingService
9 {
10 $this->orderPrice = $orderPrice;
11 }
12 
13 public function setDiscount($discount): PricingService
14 {
15 $this->discount = $discount;
16 }
17 
18 public function setTaxPercent($taxPercent): PricingService
19 {
20 $this->taxPercent = $taxPercent;
21 }
22 
23 public function setShippingFee($shippingFee): PricingService
24 {
25 $this->shippingFee = $shippingFee;
26 }
27 
28 function calculateTotalPrice() { // See, no parameters!
29 // calculations from $this->orderPrice, $this->discount,
30 // $this->taxPercent and $this->shippingFee
31 }
32}

And then, in your Controller, you would build the object, method by method:

1public function show(Order $order, PricingService $service) {
2 $totalPrice = $service->setOrderPrice($order->price)
3 ->setDiscount($order->discount)
4 ->setTaxPercent($order->taxPercent)
5 ->setShippingFee($order->shippingFee)
6 ->calculateTotalPrice();
7 );
8}

See, the Builder pattern in action!

And the benefit is not just more elegant code. It gives you much more flexibility with the parameters:

  • Typically, you would be able to call them in whichever order you want
  • You may skip some parameters, without changing the Service class
  • If you want to add more parameters, just add a property and a setter method in the Service class, and you don't need to change the parameters of the main calculateTotalPrice() method

I hope this quick example will give you some ideas of potentially refactoring your classes into Builder pattern if it makes sense in your scenario.

avatar

But if calculateTotalPrice() is a empty function, why I need to call is in controller?

avatar

I didn't put in the contents of the function, with the idea that people would figure it out themselves.

Inside the calculateTotalPrice() there would be some operation with $this->price, $this->discount and others, returning the total price.

avatar

Very great approach in my opinion. But in that case while building the object. I'd need to add several conditions to calculatePrice() Since I could skip one or more setters while building the object. Is there any better way to eliminate the conditions in calculatePrice()

avatar

For example I can't rely on doing anything with $this-discount if It's null I will have to make sure it has some value in order to proceed.

avatar

Well, I think you can do whatever you want in calculatePrice(), check anything you want, I deliberately didn't show the implementation of this method.

👍 1
avatar

@Mohamed: In that case I'd propose to set default values to those properties which are optional. So in your calculatePrice() method you could first do an initial check if all required parameters have been provided (probably throw an exception otherwise) and then just do your calculation without requiring any additional checks since all should be good.

👍 2
avatar

Yeah sounds like a solution I'd propose as well. It is good to get to understand things I have been using for decades.

Like our articles?

Become a Premium Member for $129/year or $29/month

Written by

You might also like