Singleton (Anti-Pattern)
debt(d5/e7/b7/t7)
Closest to 'specialist tool catches it' (d5), because detection_hints lists phpstan and phpmd — both specialist static analysis tools. The code_pattern (getInstance() static method, private constructor with static $instance) is detectable but requires these tools to flag it systematically; a default linter won't catch it.
Closest to 'cross-cutting refactor across the codebase' (e7), because the quick_fix says to replace Singletons with dependency injection, which requires touching every call site that references the global instance, updating constructors to accept injected dependencies, and potentially wiring a DI container — a cross-cutting change across the codebase rather than a localised fix.
Closest to 'strong gravitational pull' (e7), because applies_to covers web, cli, and queue-worker contexts and the misconception confirms Singletons create hidden global state that couples all consuming code to a specific implementation. Every component that depends on the Singleton is shaped by that choice, making it a strong gravitational burden on future changes and test isolation.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7), because the misconception explicitly states developers believe Singletons are a clean sharing mechanism, when in reality they introduce hidden global state, break test isolation (state persists between tests), and hide dependencies — behaviours that contradict the expectation of a well-scoped design pattern. The 'obvious' use is regularly wrong in practice.
Also Known As
TL;DR
Explanation
The Singleton pattern guarantees only one instance of a class exists and provides a global access point. While it solves real problems (single database connection, single logger), it introduces global state that makes classes dependent on Singleton internals, complicates testing (can't substitute mock implementations easily), and violates Dependency Inversion. The preferred alternative is using a Dependency Injection container to manage instance lifetime — register services as shared/singleton in the container, inject them via constructors.
Common Misconception
Why It Matters
Common Mistakes
- Using Singleton for services that should be injected — it prevents swapping implementations in tests.
- Singleton that is not thread-safe in async or multi-process contexts.
- Singletons that accumulate state across tests — tests must reset singleton state to be isolated.
- Using Singleton when dependency injection containers already manage instance lifetime.
Code Examples
class Config {
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self {
self::$instance ??= new self();
return self::$instance;
}
// Testing is impossible — can't inject a different Config
// Shared mutable state across the application
}
// Use DI container to manage single instance instead
// config/app.php (Laravel service container)
$app->singleton(Config::class, fn() => new Config(getenv('APP_ENV')));
// Injected wherever needed — mockable in tests
class AppController {
public function __construct(private Config $config) {}
}