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');
__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
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.