PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Singleton and Builder in PHP – creational patterns

by Henryk Tews / Tuesday, 17 May 2022 / Published in Wzorce projektowe

Singleton and Builder are two more creational patterns from the GoF catalogue. Singleton is probably the most controversial – often misused, but legitimate in specific contexts. Builder shines when constructing complex objects with many optional parameters. I show both with PHP implementations, pitfalls, and practical examples.

Singleton – one instance, global access

<?php

declare(strict_types=1);

// Classic Singleton - private constructor, static instance
class Configuration
{
    private static ?self $instance = null;
    private array $data = [];

    private function __construct() {} // prevent direct instantiation
    private function __clone() {}     // prevent cloning

    public static function getInstance(): static
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    public function set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->data[$key] ?? $default;
    }
}

// Usage
Configuration::getInstance()->set('debug', true);
$debug = Configuration::getInstance()->get('debug'); // true - same instance

Why Singleton is often wrong – and when it is right

<?php

// The problems with Singleton:
// 1. Global state - hidden dependencies, hard to test
// 2. Tight coupling - cannot swap implementation without changing callers
// 3. Breaks single responsibility - manages its own lifecycle AND does its job

// Hard to test:
class OrderService
{
    public function process(): void
    {
        $debug = Configuration::getInstance()->get('debug'); // hidden dependency!
        // How do you test with different config values?
    }
}

// BETTER - inject the dependency (DI)
class OrderService
{
    public function __construct(private ConfigInterface $config) {}

    public function process(): void
    {
        $debug = $this->config->get('debug'); // visible dependency, mockable
    }
}

// When Singleton IS legitimate:
// - Logger (one handler, append-only, no state that affects behaviour)
// - Database connection pool (resource management, one set of connections)
// - In Magento 2: DI container manages shared instances - effectively singletons
//   without the global state problem, because they are injected not retrieved

Builder – step-by-step construction

<?php

declare(strict_types=1);

// Without Builder - constructor explosion
class Order
{
    public function __construct(
        int $customerId, array $items, string $shippingMethod,
        ?string $couponCode, ?string $giftMessage, bool $isGift,
        string $billingAddress, string $shippingAddress,
        ?string $paymentMethod, float $shippingCost
        // ... more fields
    ) {}
}

// With Builder - readable, optional steps
class OrderBuilder
{
    private int $customerId;
    private array $items           = [];
    private string $shippingMethod = 'flatrate';
    private ?string $couponCode    = null;
    private ?string $giftMessage   = null;
    private bool $isGift           = false;
    private float $shippingCost    = 0.0;

    public function forCustomer(int $customerId): static
    {
        $this->customerId = $customerId;
        return $this;
    }

    public function withItems(array $items): static
    {
        $this->items = $items;
        return $this;
    }

    public function addItem(string $sku, int $qty, float $price): static
    {
        $this->items[] = compact('sku', 'qty', 'price');
        return $this;
    }

    public function withShipping(string $method, float $cost = 0.0): static
    {
        $this->shippingMethod = $method;
        $this->shippingCost   = $cost;
        return $this;
    }

    public function withCoupon(string $code): static
    {
        $this->couponCode = $code;
        return $this;
    }

    public function asGift(string $message): static
    {
        $this->isGift      = true;
        $this->giftMessage = $message;
        return $this;
    }

    public function build(): Order
    {
        if (empty($this->customerId)) {
            throw new \LogicException('Customer ID is required');
        }
        if (empty($this->items)) {
            throw new \LogicException('Order must have at least one item');
        }
        return new Order(
            $this->customerId, $this->items, $this->shippingMethod,
            $this->couponCode, $this->giftMessage, $this->isGift,
            $this->shippingCost
        );
    }
}

// Readable construction
$order = (new OrderBuilder())
    ->forCustomer(42)
    ->addItem('MG-RED-M', 2, 49.99)
    ->addItem('MG-BLUE-L', 1, 79.99)
    ->withShipping('dhl', 14.99)
    ->withCoupon('SUMMER10')
    ->asGift('Happy birthday!')
    ->build();

Builder in Magento 2 – SearchCriteriaBuilder

<?php

// Magento 2's SearchCriteriaBuilder is a classic Builder
$searchCriteria = $this->searchCriteriaBuilder
    ->addFilter('status', 'active')
    ->addFilter('price', 100, 'gt')
    ->addSortOrder(
        $this->sortOrderBuilder->setField('name')->setAscendingDirection()->create()
    )
    ->setPageSize(20)
    ->setCurrentPage(1)
    ->create(); // build() equivalent

$result = $this->productRepository->getList($searchCriteria);

Summary

Singleton: use it only when you genuinely need one instance with global access and no testability requirements – most of the time DI is the better choice. Builder: use it when a constructor has more than 4-5 parameters, especially when many are optional. It makes construction intent clear and prevents invalid states by validating in build(). Both Magento 2’s DI container and SearchCriteriaBuilder demonstrate these patterns in a well-designed, practical form.

About Henryk Tews

What you can read next

Factory Method – Simple Factory, GoF, auto-generated factories in Magento 2
Observer and Strategy in PHP – behavioural patterns
Observer pattern in PHP and the Magento 2 event system

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}