PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Singleton i Builder w PHP – wzorce kreacyjne

by Henryk Tews / wtorek, 17 maja 2022 / Opublikowano w Wzorce projektowe

Singleton i Builder to dwa bardzo różne wzorce kreacyjne. Singleton jest jednym z najkrótszych wzorców GoF i jednym z najczęściej nadużywanych. Builder rozwiązuje zupełnie inny problem – budowanie złożonych obiektów krok po kroku, gdy konstruktor z dziesiątkami parametrów staje się nieczytelny. Pokazuję oba z przykładami i bez owijania w bawełnę mówię kiedy Singleton to zły pomysł.

Singleton

Singleton gwarantuje że klasa ma dokładnie jedną instancję i zapewnia globalny punkt dostępu do niej.

<?php

declare(strict_types=1);

class Configuration
{
    private static ?self $instance = null;
    private array $data = [];

    // Konstruktor prywatny - nikt nie może zrobić new Configuration()
    private function __construct()
    {
        // Ładujemy konfigurację raz przy pierwszym użyciu
        $this->data = parse_ini_file('/etc/app/config.ini', true) ?: [];
    }

    // Blokujemy klonowanie i deserializację
    private function __clone() {}
    public function __wakeup(): never
    {
        throw new \RuntimeException('Cannot deserialize singleton');
    }

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

        return static::$instance;
    }

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

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

// Użycie - zawsze ta sama instancja
$config = Configuration::getInstance();
$config->set('debug', true);

// W innym miejscu kodu - ta sama instancja, to samo 'debug'
$sameConfig = Configuration::getInstance();
var_dump($sameConfig->get('debug')); // bool(true)

Singleton – dlaczego jest problematyczny

Singleton to jeden z najbardziej kontrowersyjnych wzorców GoF. Problemy są realne:

<?php

// Problem 1: Singleton jest trudny do testowania
// Globalna instancja "wcieka" między testami

class UserService
{
    public function getUser(int $id): array
    {
        // Singleton ukrywa zależność - nie widać jej w konstruktorze
        $db = Database::getInstance();
        return $db->query("SELECT * FROM users WHERE id = {$id}");
    }
}

// W teście musisz "wyczyścić" singleton między testami
// albo mockować statyczne wywołania - obie opcje są brzydkie

// Problem 2: Singleton = globalny stan = ukryte zależności
// Nie widać z zewnątrz że UserService zależy od Database

// Problem 3: Naruszenie SRP - klasa zarządza swoją instancją ORAZ ma logikę biznesową

// LEPIEJ - wstrzyknij zależność zamiast używać Singletona
class UserService
{
    public function __construct(
        private DatabaseInterface $db  // widoczna zależność, łatwa do mockowania
    ) {}

    public function getUser(int $id): array
    {
        return $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
    }
}

Kiedy Singleton ma sens: Logger który musi pisać do jednego pliku, połączenie z bazą danych zarządzane przez connection pool, rejestr konfiguracji tylko do odczytu wczytywany raz przy starcie. Klucz: Singleton dla zasobów które z natury powinny być jedne, nie dla wygody dostępu do globalnego stanu.

Builder

Builder rozwiązuje problem konstruktorów z wieloma parametrami – tzw. „telescoping constructor”. Gdy obiekt ma opcjonalne atrybuty których kombinacje są różne, Builder pozwala budować go krok po kroku.

<?php

declare(strict_types=1);

// Problem - konstruktor z wieloma parametrami
class QueryBuilder_BAD
{
    public function __construct(
        string $table,
        array $conditions = [],
        array $columns = ['*'],
        ?int $limit = null,
        ?int $offset = null,
        ?string $orderBy = null,
        string $orderDirection = 'ASC',
        array $joins = [],
        bool $distinct = false
    ) {
        // Co to wszystko znaczy przy wywołaniu?
    }
}

// Wywołanie jest nieczytelne
$query = new QueryBuilder_BAD('users', [['id', '>', 5]], ['id', 'email'], 10, 0, 'created_at', 'DESC', [], false);
// Kto pamięta co jest na której pozycji?
<?php

declare(strict_types=1);

// Produkt - obiekt który budujemy
class Query
{
    public function __construct(
        public readonly string $table,
        public readonly array $columns,
        public readonly array $conditions,
        public readonly array $joins,
        public readonly ?int $limit,
        public readonly ?int $offset,
        public readonly ?string $orderBy,
        public readonly string $orderDirection,
        public readonly bool $distinct
    ) {}

    public function toSql(): string
    {
        $distinct = $this->distinct ? 'DISTINCT ' : '';
        $columns  = implode(', ', $this->columns);
        $sql      = "SELECT {$distinct}{$columns} FROM {$this->table}";

        foreach ($this->joins as $join) {
            $sql .= " {$join['type']} JOIN {$join['table']} ON {$join['condition']}";
        }

        if (!empty($this->conditions)) {
            $wheres = array_map(fn($c) => implode(' ', $c), $this->conditions);
            $sql .= ' WHERE ' . implode(' AND ', $wheres);
        }

        if ($this->orderBy) {
            $sql .= " ORDER BY {$this->orderBy} {$this->orderDirection}";
        }

        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }

        if ($this->offset !== null) {
            $sql .= " OFFSET {$this->offset}";
        }

        return $sql;
    }
}

// Builder - fluent interface do budowania Query krok po kroku
class QueryBuilder
{
    private string $table = '';
    private array $columns = ['*'];
    private array $conditions = [];
    private array $joins = [];
    private ?int $limit = null;
    private ?int $offset = null;
    private ?string $orderBy = null;
    private string $orderDirection = 'ASC';
    private bool $distinct = false;

    public function from(string $table): static
    {
        $this->table = $table;
        return $this;
    }

    public function select(string ...$columns): static
    {
        $this->columns = $columns;
        return $this;
    }

    public function distinct(): static
    {
        $this->distinct = true;
        return $this;
    }

    public function where(string $column, string $operator, mixed $value): static
    {
        $this->conditions[] = [$column, $operator, $value];
        return $this;
    }

    public function join(string $table, string $condition, string $type = 'INNER'): static
    {
        $this->joins[] = ['table' => $table, 'condition' => $condition, 'type' => $type];
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): static
    {
        $this->orderBy        = $column;
        $this->orderDirection = strtoupper($direction);
        return $this;
    }

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

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

    public function build(): Query
    {
        if (empty($this->table)) {
            throw new \LogicException('Table name is required');
        }

        return new Query(
            table:          $this->table,
            columns:        $this->columns,
            conditions:     $this->conditions,
            joins:          $this->joins,
            limit:          $this->limit,
            offset:         $this->offset,
            orderBy:        $this->orderBy,
            orderDirection: $this->orderDirection,
            distinct:       $this->distinct
        );
    }
}

// Użycie - czytelne, każdy krok ma nazwę
$query = (new QueryBuilder())
    ->from('users')
    ->select('id', 'email', 'name')
    ->join('orders', 'orders.user_id = users.id', 'LEFT')
    ->where('users.status', '=', 'active')
    ->where('users.created_at', '>', '2022-01-01')
    ->orderBy('users.created_at', 'DESC')
    ->limit(20)
    ->offset(0)
    ->build();

echo $query->toSql();
// SELECT id, email, name FROM users
// LEFT JOIN orders ON orders.user_id = users.id
// WHERE users.status = active AND users.created_at > 2022-01-01
// ORDER BY users.created_at DESC LIMIT 20 OFFSET 0

Builder z Director – gdy kolejność kroków ma znaczenie

<?php

declare(strict_types=1);

// Director - enkapsuluje często używane konfiguracje buildera
class QueryDirector
{
    public function __construct(
        private QueryBuilder $builder
    ) {}

    public function buildUserReport(int $page, int $perPage): Query
    {
        return $this->builder
            ->from('users')
            ->select('id', 'email', 'name', 'created_at')
            ->where('status', '=', 'active')
            ->orderBy('created_at', 'DESC')
            ->limit($perPage)
            ->offset(($page - 1) * $perPage)
            ->build();
    }

    public function buildAdminList(): Query
    {
        return $this->builder
            ->from('users')
            ->select('id', 'email', 'role', 'last_login')
            ->where('role', '=', 'admin')
            ->orderBy('last_login', 'DESC')
            ->build();
    }
}

$director = new QueryDirector(new QueryBuilder());
$reportQuery = $director->buildUserReport(page: 2, perPage: 50);
$adminQuery  = $director->buildAdminList();

Kiedy Builder ma sens?

Builder jest uzasadniony gdy: konstruktor ma więcej niż 4-5 parametrów i część z nich jest opcjonalna, obiekt musi być walidowany jako całość po złożeniu wszystkich części, ten sam proces budowania ma tworzyć różne reprezentacje (np. Query dla MySQL i PostgreSQL). Jeśli obiekt ma 2-3 proste parametry – Builder to over-engineering.

Podsumowanie

Singleton jest prosty w implementacji ale łatwy do nadużycia – globalny stan utrudnia testowanie i ukrywa zależności. Używaj go świadomie i rozważnie. Builder rozwiązuje realny problem czytelności kodu przy złożonych obiektach – fluent interface sprawia że kod budowania obiektu czyta się niemal jak zdania w języku naturalnym.

About Henryk Tews

Co możesz przeczytać następne

Wzorzec Command – undo, CommandBus, makra, integracja z kolejką Magento 2
Wzorzec State – maszyna stanów dla zamówienia, serializacja, porównanie ze Strategy
Strategy w PHP – i jak Magento 2 używa go w cenach
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}