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 typicalbuild()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 thebuild()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
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
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
];
}
}
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();
}
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);
}
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());
}
}
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',
]);
}
}
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;
}
}
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
{
// ...
}
}
Almost done; now we should create a new controller — ReportController:
$ php artisan make:controller ReportController
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;
}
}
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']);
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!
This article was originally published by DEV Community and written by Demian Kostelny.
Read original article on DEV Community
