In many legacy PHP codebases, global state sneaks in quietly.
A config.php file is included everywhere.
A static helper class grows until it becomes a god object.
A database connection lives in a global variable “just for now”.
At first, it feels convenient.
Later, it becomes untestable, unpredictable, and painful to change.
Let’s look at how global state often appears in PHP projects, how developers usually try to “fix” it, and how refactoring toward a Singleton can help — but also why Singleton is frequently misused.
Previously article in this category: https://codecraftdiary.com/2026/02/07/builder-pattern-in-php/
The Code Smell: Hidden Global State
A typical example:
// config.php
return [
'db_host' => 'localhost',
'db_user' => 'root',
'db_pass' => 'secret',
];PHP// bootstrap.php
$config = require 'config.php';PHP// UserRepository.php
class UserRepository
{
public function find(int $id): array
{
global $config; $conn = new PDO(
'mysql:host=' . $config['db_host'],
$config['db_user'],
$config['db_pass']
); return $conn->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
}
}PHPThis code “works”.
It also hides a dependency.
UserRepository depends on configuration and a database connection, but nothing in its API communicates that. The dependency is invisible and implicit.
This leads to:
- unpredictable behavior in tests
- coupling between unrelated parts of the system
- accidental mutation of shared state
- code that is hard to reason about
This is a classic code smell: hidden dependencies via global state.
The Naive Fix: Static Helper Class
The next evolutionary step in many PHP projects looks like this:
class Config
{
private static array $data; public static function load(): void
{
self::$data = require 'config.php';
} public static function get(string $key): mixed
{
return self::$data[$key];
}
}PHPclass UserRepository
{
public function find(int $id): array
{
$conn = new PDO(
'mysql:host=' . Config::get('db_host'),
Config::get('db_user'),
Config::get('db_pass')
); return $conn->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
}
}PHPThis looks cleaner:
- no
global - no messy includes
- centralized config access
But architecturally, nothing really improved.
This is still:
- global state
- hidden dependency
- impossible to replace in tests
- tightly coupled to a static API
We replaced global with a static singleton-like façade.
This is not refactoring.
This is just changing syntax.
Refactoring Toward a Real Singleton
Let’s refactor this step by step into a proper Singleton.
final class Config
{
private static ?Config $instance = null;
private array $data; private function __construct()
{
$this->data = require 'config.php';
} public static function getInstance(): Config
{
if (self::$instance === null) {
self::$instance = new Config();
} return self::$instance;
} public function get(string $key): mixed
{
return $this->data[$key] ?? null;
}
}PHPUsage:
class UserRepository
{
public function find(int $id): array
{
$config = Config::getInstance(); $conn = new PDO(
'mysql:host=' . $config->get('db_host'),
$config->get('db_user'),
$config->get('db_pass')
); return $conn->query('SELECT * FROM users WHERE id = ' . $id)->fetch();
}
}PHPThis is now a “proper” Singleton:
- private constructor
- lazy initialization
- controlled access point
We improved:
- structure
- encapsulation
- testability (slightly)
But we did not fix the core design issue.
The dependency is still hidden.
The Real Problem: Singleton Is Global State in Disguise
A Singleton is not dependency injection.
It is global state with better manners.
You still cannot:
- pass different configurations in tests
- swap implementations easily
- reason locally about dependencies
From an architectural perspective, UserRepository still depends on Config, but that dependency is invisible in its constructor or method signature.
This creates tight coupling and invisible control flow.
The code is cleaner — but not better designed.
When Singleton Is Actually Legitimate
Singleton is not evil by definition.
It is evil when used as a default container for everything.
Legitimate use cases in PHP:
1. Immutable Application Configuration
final class AppConfig
{
private static ?AppConfig $instance = null;
private array $values; private function __construct(array $values)
{
$this->values = $values;
} public static function boot(array $values): void
{
self::$instance = new self($values);
} public static function getInstance(): AppConfig
{
return self::$instance;
} public function get(string $key): mixed
{
return $this->values[$key] ?? null;
}
}PHPBootstrapped once, read-only afterwards.
2. Logger
A logger is often a cross-cutting concern:
- shared
- stateless
- global by nature
3. Feature Flags / Environment Context
If the state is:
- immutable
- read-only
- environment-level
Singleton can be acceptable.
When Singleton Is a Design Smell
Singleton becomes a problem when:
- it contains mutable domain state
- it replaces proper dependency injection
- it hides real architectural boundaries
- it becomes a service locator
- business logic starts living inside it
If your Singleton has:
- 20 methods
- multiple responsibilities
- domain logic
…you created a god object with a private constructor.
A Better Refactoring Path
Often, Singleton should be treated as a transitional refactoring step, not a final architecture.
Better end state:
class UserRepository
{
public function __construct(
private PDO $connection
) {} public function find(int $id): array
{
return $this->connection
->query('SELECT * FROM users WHERE id = ' . $id)
->fetch();
}
}PHPAnd composition happens at the boundary of the application:
$config = Config::getInstance();$pdo = new PDO(
'mysql:host=' . $config->get('db_host'),
$config->get('db_user'),
$config->get('db_pass')
);$repo = new UserRepository($pdo);PHPNow:
- dependencies are explicit
- testing is trivial
- coupling is controlled
- architecture is visible
Conclusion
Singleton is not a goal.
It is a compromise.
It can be a useful refactoring step when removing global state from a legacy PHP codebase.
It can make dependencies slightly more explicit.
It can improve encapsulation.
But if you stop there, you only replaced chaos with a nicer-looking global variable.
Good design does not start with patterns.
It starts with making dependencies explicit and pushing composition to the edges of your system.
Singleton is a tool.
Use it deliberately — and sparingly.

