DEV Community

Cover image for Design Patterns in PHP 8: Builder
Max Zhuk
Max Zhuk

Posted on • Edited on

Design Patterns in PHP 8: Builder

Hello, fellow developers!🧑🏼‍💻

In the realm of software development, creating complex objects often feels like trying to solve a Rubik's cube. It's a step-by-step process, where every move is crucial and affects the final outcome. But what if we had a guide, a blueprint that could help us solve this puzzle effortlessly? In the world of programming, the Builder pattern serves as this guide.

The Builder pattern, a member of the esteemed creational design patterns family, is like a master craftsman in the world of object-oriented programming. It knows the intricacies of creating multifaceted objects, handling the construction process with finesse and precision. The beauty of the Builder pattern lies in its ability to construct diverse representations of an object, all while keeping the construction logic shielded from the client code.

In this article, we'll embark on a journey to explore the Builder pattern, unraveling its capabilities with PHP 8. We'll delve into its structure, understand its nuances, and see it in action with real-world examples. Whether you're building an intricate e-commerce platform or a simple blog, the Builder pattern can be your secret weapon to handle complex object creation with ease and elegance.

Imagine you're building an e-commerce application where you have a Product class. The Product class has many details like id, name, price, description, manufacturer, inventory, discount, etc.

Creating a Product object is a multi-step and sequential process. If we create multiple constructors for each attribute, it will lead to a large number of constructor parameters. This is known as the telescoping constructor anti-pattern.

class Product
{
    public int $id;
    public string $name;
    public int $price;
    public string $description;
    public string $manufacturer;
    public string $inventory;
    public int $discount;

    public function __construct(
      int $id,
      string $name,
      int $price,
      string $description,
      string $manufacturer,
      string $inventory,
      int $discount
    )
    {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
        $this->description = $description;
        $this->manufacturer = $manufacturer;
        $this->inventory = $inventory;
        $this->discount = $discount;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Builder pattern can solve this problem.

class Product
{
    private $id;
    private $name;
    private $price;
    private $description;
    private $manufacturer;
    private $inventory;
    private $discount;

    public function __construct(ProductBuilder $builder)
    {
        $this->id = $builder->getId();
        $this->name = $builder->getName();
        $this->price = $builder->getPrice();
        $this->description = $builder->getDescription();
        $this->manufacturer = $builder->getManufacturer();
        $this->inventory = $builder->getInventory();
        $this->discount = $builder->getDiscount();
    }

    // getters for product details will go here
}

class ProductBuilder
{
    private $id;
    private $name;
    private $price;
    private $description;
    private $manufacturer;
    private $inventory;
    private $discount;

    public function setId($id)
    {
        $this->id = $id;
        return $this;
    }

    public function setName($name)
    {
        $this->name = $name;
        return $this;
    }

    public function setPrice($price)
    {
        $this->price = $price;
        return $this;
    }

    public function setDescription($description)
    {
        $this->description = $description;
        return $this;
    }

    public function setManufacturer($manufacturer)
    {
        $this->manufacturer = $manufacturer;
        return $this;
    }

    public function setInventory($inventory)
    {
        $this->inventory = $inventory;
        return $this;
    }

    public function setDiscount($discount)
    {
        $this->discount = $discount;
        return $this;
    }

    // getters will go here

    public function build()
    {
        return new Product($this);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the ProductBuilder class is responsible for step-by-step construction of the Product object. The Product class itself has a constructor with a ProductBuilder argument. This way, we ensure that a product object is always created in a complete state.

$productBuilder = new ProductBuilder();
$product = $productBuilder
                ->setId(101)
                ->setName('iPhone 13')
                ->setPrice(999.99)
                ->setDescription('New iPhone 13 with A15 Bionic chip')
                ->setManufacturer('Apple Inc.')
                ->setInventory(1000)
                ->setDiscount(10)
                ->build();
Enter fullscreen mode Exit fullscreen mode

As we reach the end of our exploration of the Builder pattern, it's clear to see why it's considered a cornerstone in the world of object-oriented programming. The Builder pattern elegantly solves the problem of constructing complex objects, reducing the intricacy and enhancing the readability of our code.

One of the standout features of the Builder pattern is its ability to separate the construction and representation of an object. This separation not only makes our code more modular but also improves its maintainability and scalability. It's like having a personal architect who understands our vision and brings it to life, one brick (or in our case, one object) at a time.

On a personal note, the Builder pattern holds a special place in my toolbox of design patterns. Over the years, it has proven to be an invaluable ally, helping me construct complex objects in my projects with ease and precision. Its versatility and robustness make it my go-to pattern when dealing with intricate object creation. It's like a trusted friend who never lets me down, no matter how complex the task at hand.


P.S. Fellow developers, if you've found value in this article and are eager to deepen your understanding of design patterns in PHP and TypeScript, I have thrilling news for you! I am in the midst of crafting a comprehensive book that delves extensively into these topics, filled with practical examples, lucid explanations, and real-world applications of these patterns.

This book is being designed to cater to both novices and seasoned developers, aiming to bolster your understanding and implementation of design patterns in PHP and TypeScript. Whether you are aiming to refresh your existing knowledge or venture into new learning territories, this book is your perfect companion.

Moreover, your subscription will play a pivotal role in supporting the completion of this book, enabling me to continue providing you with quality content that can elevate your coding prowess to unprecedented heights.

I invite you to subscribe to my blog on dev.to for regular updates. I am eager to embark on this journey with you, helping you to escalate your coding skills to the next level!


Photo by Ravi Avaala on Unsplash

Top comments (7)

Collapse
 
devopsking profile image
UWABOR KING COLLINS

awesome

Collapse
 
lionelrowe profile image
lionel-rowe

it's clear to see why it's considered a cornerstone in the world of object-oriented programming

Is it? How does this give any advantage over, say, passing an associative array to the constructor? That seems a lot simpler:

class Product {
    public int $id;
    public string $name;
    public int $price;
    public string $description;
    public string $manufacturer;
    public string $inventory;
    public int $discount;

   /**
    * @param array{
    *   id: int,
    *   name: string,
    *   price: int,
    *   description: string,
    *   manufacturer: string,
    *   inventory: string,
    *   discount: int,
    * } $params
    */
    public function __construct(array $params)
    {
        foreach ($params as $key => $value) {
          $this->{$key} = $value;
        }
    }
}

$product = new Product([
    'id' => 1,
    'name' => 't-shirt',
    'price' => 20,
    'description' => "it's a t-shirt",
    'manufacturer' => 'Next',
    'inventory' => '...',
    'discount' => 5,
]);
Enter fullscreen mode Exit fullscreen mode

The Product class itself has a constructor with a ProductBuilder argument. This way, we ensure that a product object is always created in a complete state.

How does it ensure that?

$productBuilder = new ProductBuilder();
$product = $productBuilder
    ->setId(101)
    // all other properties are null
    ->build();
Enter fullscreen mode Exit fullscreen mode

I guess you could have some checks built into the build method that throw errors on missing properties, but you could equally have those checks directly built into the Product constructor, and that way you'd have less boilerplate and less indirection.

Collapse
 
zhukmax profile image
Max Zhuk

Your suggestion to use an array is intriguing, but it comes with its own set of challenges. The primary concern is the lack of control over the keys passed to the constructor. If an extraneous key is included, or a crucial key is omitted, we would need to implement checks to handle these scenarios.

However, these checks would all be bundled into a single constructor method. This leads us to the 'God Method' anti-pattern, where a single method becomes overly complex and difficult to maintain. Furthermore, it results in a lack of control over the data passed to the constructor, which can lead to potential issues down the line.

In contrast, the Builder pattern provides a more structured approach, allowing for better control and validation of the data used to construct the object. It also promotes cleaner, more maintainable code by avoiding the 'God Method' anti-pattern.

Moreover, the Builder pattern provides a more readable and self-documenting way of creating objects. When you see a chain of setter methods, it's clear what's being set without having to look at a large array structure.

I hope this provides a clearer perspective on the advantages of the Builder pattern. As with any design pattern, its effectiveness is determined by the context in which it's used.

Collapse
 
lionelrowe profile image
lionel-rowe

I'm still not entirely convinced — I can sort of see how lack of control over the keys passed to the constructor could be an issue with a language as weakly typed as PHP, and I can see how having separate setter methods for each property might help with that (surely there are linters or other tools for PHP that handle things like missing/unknown keys on associative arrays though? I don't know much about the PHP ecosystem).

But in any case, the Wikipedia article for the Builder pattern uses C# in its example, which is a strongly typed language. And tbh its example looks like a horrible mass of prematurely abstracted code that makes a simple task way more complicated than it needs to be. The "director" class seems especially unnecessary.

Moreover, the Builder pattern provides a more readable and self-documenting way of creating objects. When you see a chain of setter methods, it's clear what's being set without having to look at a large array structure.

Does it? If I have an associative array with keys that are directly used to set property names, I know there's a one-to-one correspondence. Whereas if I have a load of setX methods, I have to individually look at each setter to be sure what's actually being set. Maybe that's useful and desirable when you want to include logic in the setter, but seems unnecessary where the setter's just a way of making the property publicly settable.

Collapse
 
zhukmax profile image
Max Zhuk

You are right about checking data before or in build method. We can do it like this:

public function build()
{
    if (empty($this->id) || empty($this->name) || empty($this->price)) {
        throw new Exception('Required fields missing');
    }
    return new Product($this);
}
Enter fullscreen mode Exit fullscreen mode

But it is not about the pattern.

Collapse
 
rindraraininoro profile image
Raininoro Rindra

How about adding Product as ProductBuilder property ?

class ProductBuilder {
    private Product product;

    public function build() : Product {
        $newProduct = $this->product;
        $this->product = new Product();
        return $newProduct;   
    }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fjones profile image
FJones

The Builder pattern in PHP always confuses me. In other languages builders tend to use private constructors to prevent direct creation and use the builder as a means of standardizing polymorphism and optional attributes.

In PHP 8, named arguments make the latter part obsolete, and injecting the builder into the constructor (which I'll never understand) render the polymorphism applications moot.