Singleton

Some objects in an application only make sense as a single, shared instance — a database connection pool, an application-wide configuration reader, a logger writing to one file. If any part of the code can freely run new DatabaseConnection(), you can end up with several independent connections, each with its own state, silently working against each other. The Singleton pattern solves this by guaranteeing a class has exactly one instance for the lifetime of the request, and gives every part of the code the same, single point to reach it from.

What Singleton Actually Guarantees

A Singleton class enforces two rules at the same time:

  • Only one instance of the class can ever exist.
  • That one instance is reachable from anywhere via a well-known access point (a static method).

The Classic Implementation

The textbook Singleton has three moving parts: a private static property to hold the one instance, a private constructor so nobody outside the class can create a new one with new, and a public static getInstance() method that creates the instance the first time it's called and simply returns the same one on every call after that.

<?php

class ConfigManager {
    private static ?ConfigManager $instance = null;
    private array $settings = [];

    // Private constructor: outside code can never do "new ConfigManager()"
    private function __construct() {
        $this->settings = parse_ini_file(__DIR__ . '/config.ini');
    }

    // Private clone: blocks "clone $config" from producing a second instance
    private function __clone() {}

    // Blocks unserialize() from creating a second instance too
    public function __wakeup() {
        throw new Exception('Cannot unserialize a singleton');
    }

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

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

// Usage — anywhere in the codebase, this always returns the SAME object
$config = ConfigManager::getInstance();
$dbHost = $config->get('db_host', '127.0.0.1');
Why the constructor AND __clone must both be locked down: a private constructor stops new ConfigManager(), but PHP's clone keyword copies an existing object without ever calling the constructor. Without a private (or blocking) __clone, clone ConfigManager::getInstance() would happily hand you a second, independent copy — quietly breaking the "only one instance" guarantee the whole pattern exists to provide.

A Realistic Worked Example: A Shared Database Connection

The most common real-world use of Singleton is a database connection wrapper, so every part of an application shares one open connection instead of opening a new one on every query.

<?php

class DatabaseConnection {
    private static ?DatabaseConnection $instance = null;
    private PDO $pdo;

    private function __construct() {
        $this->pdo = new PDO(
            'mysql:host=localhost;dbname=shop;charset=utf8mb4',
            'app_user',
            'secret'
        );
    }

    private function __clone() {}

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

    public function getPdo(): PDO {
        return $this->pdo;
    }
}

// In OrderRepository.php
$pdo = DatabaseConnection::getInstance()->getPdo();

// In UserRepository.php — same underlying connection, no second handshake
$pdo = DatabaseConnection::getInstance()->getPdo();

Where You'll See This in Real Frameworks

Laravel's service container itself behaves like a Singleton: app() always returns the same container instance for the life of the request, and services bound with singleton() are built once and reused everywhere they're resolved. Symfony's dependency injection container works the same way by default — services are shared unless you explicitly mark them shared: false.

The Honest Downsides

Note: Singleton is one of the most criticized GoF patterns, and the criticism is fair. It introduces global mutable state — any code, anywhere, can call getInstance() and change shared state, which makes it hard to trace who changed what. It also makes unit testing harder: because the constructor is private, you can't easily swap in a mock or a fresh instance between tests without extra work (like a resetInstance() escape hatch used only in test code). Many teams today prefer dependency injection — passing a single shared instance in through a constructor — over a hard-coded getInstance() call, because it keeps the "only one instance" behavior but leaves the class free of hidden global lookups.
Up next: Factory Method tackles a different problem — not "how many instances," but "which concrete class should get instantiated."