DEV Community

Cover image for Laravel & Design Patterns — Practice Series: Builder
Demian Kostelny
Demian Kostelny

Posted on

Laravel & Design Patterns — Practice Series: Builder

I was tired of seeing how many articles repeat, again and again, basic examples of using design patterns in development; examples based on shape figures/real-life objects are good for understanding when you are just starting to learn this concept of design patterns. But what about real practice? What about real examples with real code and frameworks to get a much better practical understanding and use?

And this is the reason why I decided to start writing a series of articles about full practice in the Laravel framework. Even if you’re not a PHP developer and you have some medium-level experience in web development, you can still understand what we are going to do and use it in your own development.

Builder pattern: Introduction

It was a good decision to start this series with one of the simplest patterns, named “builder,” from the creational patterns. Let’s begin with some graphical examples to remember how it works, and after that, let’s move on to writing some code for generating reports in different file formats.

Let’s begin with a definition:

The Builder design pattern is a creational pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is especially useful when an object needs to be created with many optional parts or configurations.

Real-life example

A simple real-life example of the Builder Pattern is how you order a burger at a fast-food restaurant:

  • Instead of giving you only fixed menu items, the restaurant lets you customize your burger.
  • You can add a patty, cheese, lettuce, tomato, sauces, bacon, etc., in the order you like.
  • The builder (the person assembling) constructs the burger step by step.
  • At the end, you get a fully built burger according to your specifications.

🥩 + 🧀 + 🥬 + 🍅 + 🍞 ➝ 🍔

UML Representation

Now, let’s take a quick look at the abstract UML schema to understand how we should build our classes' interactions:

Here is an explanation of how this UML schema works:

  • Everything begins with the interface IBuilder, which defines a typical build() method that will be used in the concrete class implementation for building the final class object
  • Next, what comes after this is ObjectBuilder, a class with a concrete implementation of how a class object can be built. There can be different methods, not only the concrete implementation of the build() method
  • TargetObject — is the final product of the object that we will get from our builder
  • And if we want, we can extend our builder and have a lot of OtherObject

This abstraction gives to use power for encapsulation of object creation in our builder, which makes your Laravel code cleaner, modular, and easier to extend or modify.

Let’s now take a look at a more concrete UML implementation, and after that, we are going to begin using this design pattern in the app:

I hope that this picture will give you more understanding of how the builder pattern works, and that’s what we are going to do now: we are going to use this pattern to create a report creation solution. Let’s begin.

Implementation

First, take a look at how our structure will be organized:

app/
└── Services/
    └── Report/
        ├── Builders/
        │   ├── IReportBuilder.php
        │   ├── ReportBuilder.php
        ├── Formats/
        │   ├── Formatter.php
        │   ├── PDFReport.php
        │   ├── JSONReport.php
        └── ReportService.php
Enter fullscreen mode Exit fullscreen mode

Of course, if you want, you can follow DDD (Domain-Driven-Design) pattern, and then your structure will look like this:

app/
└── Domains/
    └── Report/
        ├── Builders/
        │   ├── IReportBuilder.php # Our interface
        │   ├── ReportBuilder.php
        ├── Formats/
        │   ├── Formatter.php
        │   ├── PDFReport.php
        │   ├── JSONReport.php
        └── Report.php
Enter fullscreen mode Exit fullscreen mode

When we get the full picture of how our structure will look, we can start building and create a report entity in Report.php:

<?php

namespace App\Domains\Report\Entities;

class Report
{
    public function __construct(
        public string $title = '',
        public ?array $period = null,
        public array $rows = [],
        public array $totals = []
    ) {}

    public function toArray(): array
    {
        return [
            'title' => $this->title,
            'period' => $this->period,
            'rows' => $this->rows,
            'totals' => $this->totals
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, let’s define our interface IBuilderInterface that will be used for our report builder:

<?php

namespace App\Domains\Report\Builders;

use App\Domains\Report\Entities\Report;
use App\Domains\Report\Formats\Formatter;

interface IReportBuilder
{
    public function reset(): static;

    public function setTitle(string $title): static;

    public function setPeriod(?array $period): static;

    public function setRows(array $rows): static;

    public function addRow(array $row): static;

    public function setTotals(array $totals): static;

    public function setFormatter(Formatter $formatter): static;

    public function getReport(): Report;

    public function build();
}
Enter fullscreen mode Exit fullscreen mode

Okay, now we need to define the Formatter interface that will be used for our Format classes:

<?php

namespace App\Domains\Report\Formats;

use App\Domains\Report\Entities\Report;

interface Formatter
{
    public function render(Report $report);
}
Enter fullscreen mode Exit fullscreen mode

After this, let’s create our first format class — and it will be JsonReport:

<?php

namespace App\Domains\Report\Formats;

use App\Domains\Report\Formats\Formatter;
use App\Domains\Report\Entities\Report;

class JsonReport implements Formatter
{
    public function render(Report $report)
    {
        return response()
            ->json($report->toArray());
    }
}
Enter fullscreen mode Exit fullscreen mode

And let’s also add another class that will be used to create a report in XML format:

<?php

namespace App\Domains\Report\Formats;

use App\Domains\Report\Entities\Report;

class XMLReport implements Formatter
{
    public function render(Report $report)
    {
        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><report></report>');

        $xml->addChild('title', $report->title);

        if ($report->period) {
            $periodNode = $xml->addChild('period');
            $periodNode->addChild('from', $report->period['from'] ?? '');
            $periodNode->addChild('to', $report->period['to'] ?? '');
        }

        $rowsNode = $xml->addChild('rows');
        foreach ($report->rows as $row) {
            $rowNode = $rowsNode->addChild('row');
            foreach ($row as $key => $val) {
                $rowNode->addChild($key, htmlspecialchars((string) $val));
            }
        }

        if (!empty($report->totals)) {
            $totalsNode = $xml->addChild('totals');
            foreach ($report->totals as $key => $val) {
                $totalsNode->addChild($key, (string) $val);
            }
        }

        return response($xml->asXML(), 200, [
            'Content-Type' => 'application/xml',
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Amazing, and the main class that will be used to build the report following the Builder pattern — this is ReportBuilder:

<?php

namespace App\Domains\Report\Builders;

use App\Domains\Report\Builders\IReportBuilder;
use App\Domains\Report\Entities\Report;
use App\Domains\Report\Formats\Formatter;

class ReportBuilder implements IReportBuilder
{
    protected Report $report;

    protected ?Formatter $formatter = null;

    public function __construct()
    {
        $this->reset();
    }

    public function reset(): static
    {
        $this->report = new Report();
        return $this;
    }

    public function setTitle(string $title): static
    {
        $this->report->title = $title;
        return $this;
    }

    public function setPeriod(?array $period): static
    {
        $this->report->period = $period;
        return $this;
    }

    public function setRows(array $rows): static
    {
        $this->report->rows = $rows;
        return $this;
    }

    public function addRow(array $row): static
    {
        $this->report->rows[] = $row;
        return $this;
    }

    public function setTotals(array $totals): static
    {
        $this->report->totals = $totals;
        return $this;
    }

    public function setFormatter(Formatter $formatter): static
    {
        $this->formatter = $formatter;
        return $this;
    }

    public function getReport(): Report
    {
        return $this->report;
    }

    public function build()
    {
        $response = $this->formatter->render($this->report);
        $this->reset();

        return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Before we are going to test how it works in the controller, we also need to register in the container our IBuilderInterface with the ReportBuilder class in app/Providers/AppServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domains\Report\Builders\IReportBuilder;
use App\Domains\Report\Builders\ReportBuilder;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(IReportBuilder::class, ReportBuilder::class);
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Almost done; now we should create a new controller — ReportController:

$ php artisan make:controller ReportController
Enter fullscreen mode Exit fullscreen mode

Finally, let’s use our ReportBuilder to create a JSON report:

<?php

namespace App\Http\Controllers;

use App\Domains\Report\Builders\ReportBuilder;
use App\Domains\Report\Formats\JsonReport;
use App\Domains\Report\Formats\XMLReport;
use Illuminate\Http\Request;

class ReportController extends Controller
{
    public function __construct(
        private ReportBuilder $reportBuilder
    ) {}

    public function getReport(Request $request, string $type)
    {
        $rows = [
            ['date' => '2025-08-01', 'orders' => 12, 'revenue' => 3400],
            ['date' => '2025-08-02', 'orders' => 9,  'revenue' => 2210],
        ];

        $totals = [
            'orders'  => array_sum(array_column($rows, 'orders')),
            'revenue' => array_sum(array_column($rows, 'revenue')),
        ];

        $period = ['from' => '2025-08-01', 'to' => '2025-08-02'];

        $formats = [
            'json' => JsonReport::class,
            'xml' => XMLReport::class,
        ];

        // Get selected formatter
        $formatter = app()->make($formats[$type]);

        $report = $this->reportBuilder
            ->reset()
            ->setTitle('JSON Report')
            ->setPeriod($period)
            ->setRows($rows)
            ->setTotals($totals)
            ->setFormatter($formatter)
            ->build();

        return $report;
    }
}
Enter fullscreen mode Exit fullscreen mode

So, we just created a flexible class to set all report data just by using special functions and following the Builder design pattern, and of course, we can easily select a format by creating separate classes that are responsible for report formats (remember about SOLID principles and try always to write your code clean).

Don’t forget also to include your report controller in routes/web.php to test how it works in the browser:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ReportController;


Route::get('/report/{type}', [ReportController::class, 'getReport']);
Enter fullscreen mode Exit fullscreen mode

Conclusion

As we can see, it’s easy and nice to use this pattern in case if you want to create objects of some classes flexibly and easily customizable. Laravel has a lot of internal classes that use this pattern for object creation (for example, DB, Mailable, and Http).

In the next series, we are going to check other creational patterns with practical examples in Laravel. Thanks for reading!

Top comments (0)