Kaum ein Entwurfsmuster wird so häufig eingesetzt und gleichzeitig so hart kritisiert wie das Singleton. Das liegt daran, dass es ein echtes Problem löst und dabei ein neues schafft. Dieses Tutorial zeigt die saubere Implementierung in PHP, die typischen Einsatzfelder und die Stellen, an denen ein DI-Container die bessere Wahl ist.
Was ist das PHP Singleton Pattern?
Das PHP Singleton Pattern gehört zu den bekanntesten Entwurfsmustern und sorgt seit Jahren für hitzige Diskussionen. Im Kern garantiert ein Singleton, dass eine Klasse während der Laufzeit der Anwendung genau ein Objekt besitzt, und stellt einen globalen Zugriffspunkt darauf bereit. Typische Beispiele sind ein Logger, der nur einmal initialisiert werden soll, eine Konfiguration, die nicht bei jedem Aufruf neu geladen wird, oder eine Datenbankverbindung, die für den gesamten Request gebündelt wird.

Bevor die Implementierung mit getInstance() und privatem Konstruktor folgt, ein Blick darauf, warum dieses Pattern so beliebt und gleichzeitig so umstritten ist.
Das PHP Singleton Pattern ist gleichzeitig sehr einfach zu implementieren und gleichzeitig kritisch zu bewerten. Wer das Pattern nutzt, sollte die Stolperfallen kennen, denn nicht ohne Grund wird ein Singleton in modernen PHP-Projekten oft durch einen Dependency-Injection-Container ersetzt. Dieses Tutorial zeigt die saubere Implementierung, die typischen Anwendungsfälle und nennt klar die Argumente für und gegen den Einsatz.
Klassische Implementierung von PHP Singleton mit getInstance()
Die Standard-Implementierung von PHP Singleton beruht auf drei Bausteinen: einer privaten statischen Eigenschaft, die die einzige Instanz hält, einem privaten Konstruktor, der externe Aufrufe von new verhindert, und einer öffentlichen statischen Methode getInstance(), die die Instanz beim ersten Aufruf erzeugt und danach nur noch zurückgibt. So sieht das Gerüst für eine Konfigurations-Klasse aus.
<?php
final class Config
{
private static ?Config $instance = null;
private array $settings = [];
private function __construct()
{
$this->settings = require __DIR__ . '/config.php';
}
public static function getInstance(): Config
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get(string $key): mixed
{
return $this->settings[$key] ?? null;
}
}
echo Config::getInstance()->get('app_name');
Wichtig ist das Schlüsselwort final vor der Klassendefinition. Es verhindert, dass jemand die Singleton-Klasse erweitert und dadurch den Mechanismus der einzigen Instanz aushebelt. Der private Konstruktor sorgt zusammen mit getInstance() dafür, dass Aufrufe wie new Config() von aussen mit einem Fatal Error scheitern.
Der Ablauf dahinter bleibt immer derselbe, egal welche Klasse das Muster nutzt.
flowchart TD
A[Aufruf getInstance] --> B{Instanz da?}
B -->|Nein| C[Konstruktor ausfuehren]
C --> D[Instanz speichern]
D --> E[Instanz zurueckgeben]
B -->|Ja| E
Lazy Initialization und private Magic Methods
Im obigen Beispiel ist bereits eine Lazy Initialization eingebaut: Die teure Initialisierung im Konstruktor läuft erst beim ersten Aufruf von getInstance(). Wenn das Singleton in einem Request gar nicht gebraucht wird, entstehen auch keine Kosten. Damit niemand das PHP Singleton über Umwege umgehen kann, sollten die magischen Methoden __clone() und __wakeup() privat sein oder eine Exception werfen.
<?php
final class Config
{
private static ?Config $instance = null;
/* ... wie zuvor ... */
private function __clone(): void
{
/* leerer privater Klon-Schutz */
}
public function __wakeup(): void
{
throw new \Exception('Singleton kann nicht deserialisiert werden');
}
}
Mit dem privaten __clone() schlägt jeder Versuch, das Objekt per clone $config zu duplizieren, fehl. Die Methode __wakeup() wird beim Aufruf von unserialize() automatisch aufgerufen und sollte eine Exception werfen, damit ein gespeichertes Singleton nicht zu einer zweiten Instanz wird. Reflection lässt sich aus PHP heraus zwar nicht komplett verbieten, aber die genannten Schutzmaßnahmen reichen für den normalen Anwendungscode aus.
Anwendungsfall Logger
Der Klassiker für das PHP Singleton ist der zentrale Logger. In einer Anwendung soll genau eine Logger-Instanz die Schreibzugriffe auf die Logdatei bündeln. Das vermeidet doppelte Datei-Handles und sorgt dafür, dass die Reihenfolge der Einträge stimmt.
<?php
final class Logger
{
private static ?Logger $instance = null;
private $stream;
private function __construct()
{
$this->stream = fopen(__DIR__ . '/app.log', 'a');
}
public static function getInstance(): Logger
{
return self::$instance ??= new self();
}
public function info(string $message): void
{
fwrite($this->stream, '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL);
}
}
Logger::getInstance()->info('Anwendung gestartet');
Die Schreibweise self::$instance ??= new self() mit dem Null-Coalescing-Assignment-Operator ist seit PHP 7.4 möglich und kürzt die typische if (self::$instance === null)-Prüfung auf eine Zeile. Ähnlich aufgebaute Klassen findet man in vielen Projekten für eine zentrale Konfiguration, eine geteilte Cache-Instanz oder eine Datenbankverbindung.
Singleton für eine PDO-Datenbankverbindung
Der wohl häufigste Praxis-Einsatz für ein PHP Singleton ist eine Datenbankverbindung. Jede Verbindung kostet Zeit (DNS-Lookup, TCP-Handshake, Authentifizierung), darum soll sie pro Request nur einmal aufgebaut und an alle Stellen weitergereicht werden, die sie brauchen. Die folgende Klasse kapselt einen PDO-Aufbau und liefert beim wiederholten Aufruf immer dieselbe Verbindung.
<?php
final class Database
{
private static ?Database $instance = null;
private PDO $pdo;
private function __construct()
{
$this->pdo = new PDO(
'mysql:host=localhost;dbname=shop;charset=utf8mb4',
'user',
'pass',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
}
public static function getInstance(): Database
{
return self::$instance ??= new self();
}
public function pdo(): PDO
{
return $this->pdo;
}
}
$users = Database::getInstance()->pdo()->query('SELECT id FROM users')->fetchAll();
Die Klasse macht zwei Dinge richtig: Sie zeigt das PDO-Objekt nicht direkt über eine public-Eigenschaft, sondern über eine Methode, was späteres Logging oder Wrapping erleichtert. Und sie setzt im Konstruktor sinnvolle Defaults wie ERRMODE_EXCEPTION und EMULATE_PREPARES = false, damit alle Aufrufer dieselbe sichere Konfiguration bekommen. Wer trotzdem die Test-Kritik aus dem nächsten Abschnitt ernst nimmt, kann zusammen mit dieser Klasse einen Setter Database::setTestInstance(...) einbauen, der ausschliesslich in Tests genutzt wird.
Trait-basierte Singleton-Implementierung
Wer das Singleton-Verhalten in mehreren Klassen wiederverwenden möchte, kann es in ein Trait auslagern. Das spart Boilerplate und verhindert Tippfehler beim wiederholten Implementieren.
<?php
trait SingletonTrait
{
private static ?self $instance = null;
public static function getInstance(): self
{
return self::$instance ??= new self();
}
private function __clone(): void {}
public function __wakeup(): void
{
throw new \Exception('Singleton kann nicht deserialisiert werden');
}
}
final class Cache
{
use SingletonTrait;
/* Konstruktor bleibt hier privat, weil das Trait keinen
privaten Konstruktor erzwingen kann */
private function __construct() { /* ... */ }
}
Wichtig ist der Hinweis im Code: Ein Trait kann den privaten Konstruktor nicht erzwingen, weil private function __construct() im Trait die Sichtbarkeit nicht in der nutzenden Klasse durchsetzt. Jede Klasse, die das Trait nutzt, muss den Konstruktor selbst privat halten. Das ist der einzige spürbare Nachteil gegenüber der Klassen-Implementierung. Im Gegenzug spart das Trait-Pattern in grösseren Codebasen bei sechs oder mehr Singleton-Klassen viele Zeilen.
Threading und das PHP-Request-Modell
Diskussionen um Singletons aus der Java- oder C#-Welt drehen sich oft um Thread-Sicherheit und Double-Checked Locking. In klassischem PHP mit dem Standard-Request-Modell (eine Anfrage, ein Prozess oder ein Worker) entsteht dieses Problem nicht. Jeder Request bekommt seine eigene Singleton-Instanz, beim Skript-Ende wird sie weggeworfen. Synchronisierung über Threads ist nicht nötig.
Die Lage ändert sich nur in zwei Spezialfällen: ReactPHP, Swoole oder Roadrunner führen das Skript dauerhaft aus und teilen Singletons zwischen Requests. Hier kann eine Database-Instanz mit aufgebrauchten Verbindungen schnell zur Falle werden. Und in selten genutzten Multi-Thread-Setups mit pthreads greift kein PHP-Singleton automatisch über Threads hinweg, weil jeder Thread sein eigenes Memory-Segment hat. Beide Fälle sind Nischen, sollten aber bei der Architekturwahl berücksichtigt werden, sobald sie ins Spiel kommen.
Die Kritik: Hidden Dependencies und Test-Probleme
Das PHP Singleton wirkt auf den ersten Blick elegant, hat aber drei harte Schattenseiten. Erstens entstehen sogenannte Hidden Dependencies. Eine Methode, die irgendwo im Code Logger::getInstance() ruft, gibt von aussen nicht zu erkennen, dass sie einen Logger braucht. Das macht den Code schwer lesbar und versteckt Abhängigkeiten.
Zweitens werden Unit-Tests schwierig. Da die Instanz statisch im Speicher hängt, kann sie zwischen Tests nicht sauber zurückgesetzt werden. Mocks oder Test-Doubles lassen sich nicht ohne weiteres einschleusen. Drittens widerspricht ein Singleton dem OOP-Prinzip, dass Objekte ihre Abhängigkeiten klar im Konstruktor erhalten. Globaler Zustand ist im Code-Review oft ein Warnsignal.
<?php
class OrderService
{
public function place(int $orderId): void
{
Logger::getInstance()->info("Bestellung $orderId");
/* Hidden Dependency: niemand sieht von aussen,
dass OrderService den Logger benoetigt. Im Unit-Test
laesst sich der Logger nicht durch ein Mock ersetzen,
weil getInstance() global laeuft */
}
}
In dem Beispiel erkennt niemand auf den ersten Blick, dass OrderService einen Logger braucht. Wer den Service testen will, muss entweder die echte Logdatei akzeptieren oder den Singleton tricksen. Beides ist hässlich.
PHP Singleton vs. DI-Container
Die moderne Antwort auf das PHP Singleton-Problem ist Dependency Injection über einen DI-Container. Er garantiert ebenfalls, dass eine teure Klasse pro Lebenszyklus nur einmal erzeugt wird, gibt das Objekt aber sauber per Konstruktor weiter. Damit verschwinden die Hidden Dependencies, und die Klasse bleibt im Test gut isolierbar. In modernen Frameworks ersetzt Dependency Injection klassische Singletons fast komplett.
<?php
class Container
{
private array $instances = [];
private array $factories = [];
public function set(string $id, callable $factory): void
{
$this->factories[$id] = $factory;
}
public function get(string $id): mixed
{
if (!isset($this->instances[$id])) {
$this->instances[$id] = ($this->factories[$id])($this);
}
return $this->instances[$id];
}
}
$container = new Container();
$container->set(Logger::class, fn() => new Logger('/var/log/app.log'));
$logger = $container->get(Logger::class);
Der Container hält die Instanz wie ein Singleton im Speicher, gibt sie aber nur an Klassen weiter, die ihre Abhängigkeit explizit deklarieren. Genau das verlangt auch der weit verbreitete Standard PSR-11 Container Interface, den nahezu jeder DI-Container in der PHP-Welt umsetzt.
Best Practices für den Alltag
Trotz aller Kritik sind PHP Singletons nicht grundsätzlich verboten. Es gibt Fälle, in denen sie legitim bleiben: ein Logger in einer kleinen Skript-Anwendung, eine Konfiguration in einem Plugin, das in einem fremden Framework läuft, oder eine Cache-Instanz, die nur einmal pro Request initialisiert werden soll. Wer ein Singleton einsetzt, sollte aber drei Regeln beachten.
Erstens sollte die Klasse final sein, damit niemand das Pattern durch Vererbung umgeht. Zweitens müssen __clone() und __wakeup() geschuetzt sein. Drittens sollte das Singleton nur für Querschnittsthemen wie Logging oder Konfiguration genutzt werden, niemals für Domain-Logik. Sobald Geschäftsregeln im Spiel sind, ist die saubere Variante mit Konstruktor-Injection und einem Container vorzuziehen.
Eine Erweiterung, die in seltenen Fällen sinnvoll ist, ist das Multiton. Es speichert mehrere benannte Instanzen, etwa eine pro Datenbankverbindung mit unterschiedlichen Hosts. Wer einen DB-Connection-Pool plant, sollte aber prüfen, ob nicht das normale Request-Lifecycle in PHP bereits ausreicht, um Verbindungen sinnvoll zu bündeln.
Ein weiteres oft übersehenes Detail betrifft die Vererbung. Wer eine Singleton-Klasse später doch erweitern möchte, muss static:: statt self:: verwenden, damit die abgeleitete Klasse ihre eigene Instanz erhält (Late Static Binding). In der Praxis sorgt das aber regelmäßig für Verwirrung, weil dann pro Subklasse eine eigene Instanz entsteht. Das genau zu verhindern war ja der ursprüngliche Grund, final zu setzen. Wer also nicht ganz genau weiß, was er tut, bleibt besser bei der finalen Klasse und akzeptiert, dass das Singleton nicht erweiterbar ist. Ein guter Test, ob man das PHP Singleton überhaupt braucht, ist die Frage: Würde es weh tun, wenn während des Requests zwei Instanzen entstünden? Wenn die Antwort nein ist, ist das Pattern vermutlich übertrieben und ein normaler Service mit Constructor Injection reicht völlig aus.
Fazit zum PHP Singleton Pattern
Das PHP Singleton Pattern ist schnell implementiert und liefert genau das, was es verspricht: eine einzige Instanz mit globalem Zugriff. Im Alltag wiegt aber die Kritik schwer. Hidden Dependencies, schwer testbarer globaler Zustand und das Risiko von Mehrfachinstanzen über clone, unserialize oder Vererbung machen das Pattern in modernen PHP-Projekten eher zur Ausnahme. Wer ein Singleton braucht, sollte konsequent final setzen, die magischen Methoden absichern und den Einsatz auf wenige Querschnittsklassen beschränken. Für alles andere ist ein DI-Container die nachhaltigere Lösung, weil er den gleichen Effekt erzielt, ohne die OOP-Prinzipien zu verletzen.