Global State and Singletons
Warning: The following constructs are symptoms of poorly designed code:
Foo::getInstance()
DB::insert(...)
Article::setDb($db)
ClassName::$var
orstatic::$var
Do you encounter any of these constructs in your code? If so, you have an opportunity to improve it. You might think these are common constructs, often seen in sample solutions of various libraries and frameworks. If that's the case, their code design is flawed.
We're not talking about some academic purity here. All these constructs have one thing in common: they utilize global state. And this has a destructive impact on code quality. Classes are deceptive about their dependencies. The code becomes unpredictable. It confuses developers and reduces their efficiency.
In this chapter, we'll explain why this is the case and how to avoid global state.
Global Interlinking
In an ideal world, an object should only communicate with objects that were directly passed to it. If I create two objects
A
and B
and never pass a reference between them, then neither A
nor B
can
access or modify the other's state. This is a highly desirable property of code. It's akin to having a battery and a light bulb;
the bulb won't light up until you connect it to the battery with a wire.
However, this doesn't hold true for global (static) variables or singletons. Object A
could wirelessly
access object C
and modify it without any reference passing, by calling C::changeSomething()
. If object
B
also taps into the global C
, then A
and B
can influence each other through
C
.
Using global variables introduces a new form of wireless coupling that's not externally visible. It creates a
smokescreen that complicates understanding and using the code. To truly understand the dependencies, developers have to read every
line of the source code, rather than just familiarizing themselves with class interfaces. Moreover, this entanglement is entirely
unnecessary. Global state is used because it's easily accessible from anywhere and allows, for instance, writing to a database
through a global (static) method DB::insert()
. However, as we'll see, the benefit it offers is minimal, while the
complications it introduces are severe.
In terms of behavior, there is no difference between a global and a static variable. They are equally harmful.
The Spooky Action at a Distance
“Spooky action at a distance” – that's what Albert Einstein famously called a phenomenon in quantum physics that gave him the creeps in 1935. It is quantum entanglement, the peculiarity of which is that when you measure information about one particle, you immediately affect another particle, even if they are millions of light years apart. which seemingly violates the fundamental law of the universe that nothing can travel faster than light.
In the software world, we can call a “spooky action at a distance” a situation where we run a process that we think is isolated (because we haven't passed it any references), but unexpected interactions and state changes happen in distant locations of the system which we did not tell the object about. This can only happen through the global state.
Imagine joining a project development team that has a large, mature code base. Your new lead asks you to implement a new feature and, like a good developer, you start by writing a test. But because you're new to the project, you do a lot of exploratory “what happens if I call this method” type tests. And you try to write the following test:
function testCreditCardCharge()
{
$cc = new CreditCard('1234567890123456', 5, 2028); // your card number
$cc->charge(100);
}
You run the code, maybe several times, and after a while you notice notifications on your phone from the bank that each time you run it, $100 was charged to your credit card 🤦♂️
How on earth could the test cause an actual charge? It's not easy to operate with credit card. You have to interact with a third party web service, you have to know the URL of that web service, you have to log in, and so on. None of this information is included in the test. Even worse, you don't even know where this information is present, and therefore how to mock external dependencies so that each run doesn't result in $100 being charged again. And as a new developer, how were you supposed to know that what you were about to do would lead to you being $100 poorer?
That's a spooky action at a distance!
You have no choice but to dig through a lot of source code, asking older and more experienced colleagues, until you understand
how the connections in the project work. This is due to the fact that when looking at the interface of the CreditCard
class, you cannot determine the global state that needs to be initialized. Even looking at the source code of the class won't tell
you which initialization method to call. At best, you can find the global variable being accessed and try to guess how to
initialize it from that.
The classes in such a project are pathological liars. The payment card pretends that you can just instantiate it and call the
charge()
method. However, it secretly interacts with another class, PaymentGateway
. Even its interface
says it can be initialized independently, but in reality it pulls credentials from some configuration file and so on. It is clear
to the developers who wrote this code that CreditCard
needs PaymentGateway
. They wrote the code this
way. But for anyone new to the project, this is a complete mystery and hinders learning.
How to fix the situation? Easy. Let the API declare dependencies.
function testCreditCardCharge()
{
$gateway = new PaymentGateway(/* ... */);
$cc = new CreditCard('1234567890123456', 5, 2028);
$cc->charge($gateway, 100);
}
Notice how the relationships within the code are suddenly obvious. By declaring that the charge()
method needs
PaymentGateway
, you don't have to ask anyone how the code is interdependent. You know you have to create an instance
of it, and when you try to do so, you run into the fact that you have to supply access parameters. Without them, the code wouldn't
even run.
And most importantly, you can now mock the payment gateway so you won't be charged $100 every time you run a test.
The global state causes your objects to be able to secretly access things that aren't declared in their APIs, and as a result makes your APIs pathological liars.
You may not have thought of it this way before, but whenever you use global state, you're creating secret wireless communication channels. Creepy remote action forces developers to read every line of code to understand potential interactions, reduces developer productivity, and confuses new team members. If you're the one who created the code, you know the real dependencies, but anyone who comes after you is clueless.
Don't write code that uses global state, prefer to pass dependencies. That is, dependency injection.
Brittleness of the Global State
In code that uses global state and singletons, it is never certain when and by whom that state has changed. This risk is already present at initialization. The following code is supposed to create a database connection and initialize the payment gateway, but it keeps throwing an exception and finding the cause is extremely tedious:
PaymentGateway::init();
DB::init('mysql:', 'user', 'password');
You have to go through the code in detail to find that the PaymentGateway
object accesses other objects
wirelessly, some of which require a database connection. Thus, you must initialize the database before
PaymentGateway
. However, the smokescreen of global state hides this from you. How much time would you save if the API
of each class did not lie and declare its dependencies?
$db = new DB('mysql:', 'user', 'password');
$gateway = new PaymentGateway($db, ...);
A similar problem arises when using global access to a database connection:
use Illuminate\Support\Facades\DB;
class Article
{
public function save(): void
{
DB::insert(/* ... */);
}
}
When calling the save()
method, it is not certain whether a database connection has already been created and who
is responsible for creating it. For example, if we wanted to change the database connection on the fly, perhaps for testing
purposes, we would probably have to create additional methods such as DB::reconnect(...)
or
DB::reconnectForTest()
.
Consider an example:
$article = new Article;
// ...
DB::reconnectForTest();
Foo::doSomething();
$article->save();
Where can we be sure that the test database is really being used when calling $article->save()
? What if the
Foo::doSomething()
method changed the global database connection? To find out, we would have to examine the source
code of the Foo
class and probably many other classes. However, this approach would provide only a short-term answer,
as the situation may change in the future.
What if we move the database connection to a static variable inside the Article
class?
class Article
{
private static DB $db;
public static function setDb(DB $db): void
{
self::$db = $db;
}
public function save(): void
{
self::$db->insert(/* ... */);
}
}
This doesn't change anything at all. The problem is a global state and it doesn't matter which class it hides in. In this case,
as in the previous one, we have no clue as to what database is being written to when the $article->save()
method
is called. Anyone on the distant end of the application could change the database at any time using Article::setDb()
.
Under our hands.
The global state makes our application extremely fragile.
However, there is a simple way to deal with this problem. Just have the API declare dependencies to ensure proper functionality.
class Article
{
public function __construct(
private DB $db,
) {
}
public function save(): void
{
$this->db->insert(/* ... */);
}
}
$article = new Article($db);
// ...
Foo::doSomething();
$article->save();
This approach eliminates the worry of hidden and unexpected changes to database connections. Now we are sure where the article is stored and no code modifications inside another unrelated class can change the situation anymore. The code is no longer fragile, but stable.
Don't write code that uses global state, prefer to pass dependencies. Thus, dependency injection.
Singleton
Singleton is a design pattern that, by definition from the famous Gang of Four publication, restricts a class to a single instance and offers global access to it. The implementation of this pattern usually resembles the following code:
class Singleton
{
private static self $instance;
public static function getInstance(): self
{
self::$instance ??= new self;
return self::$instance;
}
// and other methods that perform the functions of the class
}
Unfortunately, the singleton introduces global state into the application. And as we have shown above, global state is undesirable. That's why the singleton is considered an antipattern.
Don't use singletons in your code and replace them with other mechanisms. You really don't need singletons. However, if you
need to guarantee the existence of a single instance of a class for the entire application, leave it to the DI container. Thus, create an application singleton, or
service. This will stop the class from providing its own uniqueness (i.e., it won't have a getInstance()
method and a
static variable) and will only perform its functions. Thus, it will stop violating the single responsibility principle.
Global State Versus Tests
When writing tests, we assume that each test is an isolated unit and that no external state enters it. And no state leaves the tests. When a test completes, any state associated with the test should be removed automatically by the garbage collector. This makes the tests isolated. Therefore, we can run the tests in any order.
However, if global states/singletons are present, all these nice assumptions break down. A state can enter and exit a test. Suddenly, the order of the tests may matter.
To test singletons at all, developers often have to relax their properties, perhaps by allowing an instance to be replaced by
another. Such solutions are, at best, hacks that produce code that is difficult to maintain and understand. Any test or method
tearDown()
that affects any global state must undo those changes.
Global state is the biggest headache in unit testing!
How to fix the situation? Easy. Don't write code that uses singletons, prefer to pass dependencies. That is, dependency injection.
Global Constants
Global state is not limited to the use of singletons and static variables, but can also apply to global constants.
Constants whose value does not provide us with any new (M_PI
) or useful (PREG_BACKTRACK_LIMIT_ERROR
)
information are clearly OK. Conversely, constants that serve as a way to wirelessly pass information inside the code are
nothing more than a hidden dependency. Like LOG_FILE
in the following example. Using the FILE_APPEND
constant is perfectly correct.
const LOG_FILE = '...';
class Foo
{
public function doSomething()
{
// ...
file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND);
// ...
}
}
In this case, we should declare the parameter in the constructor of the Foo
class to make it part of the API:
class Foo
{
public function __construct(
private string $logFile,
) {
}
public function doSomething()
{
// ...
file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
// ...
}
}
Now we can pass information about the path to the logging file and easily change it as needed, making it easier to test and maintain the code.
Global Functions and Static Methods
We want to emphasize that the use of static methods and global functions is not in itself problematic. We have explained the
inappropriateness of using DB::insert()
and similar methods, but it has always been a matter of global state stored
in a static variable. The DB::insert()
method requires the existence of a static variable because it stores the
database connection. Without this variable, it would be impossible to implement the method.
The use of deterministic static methods and functions, such as DateTime::createFromFormat()
,
Closure::fromCallable
, strlen()
and many others, is perfectly consistent with dependency injection.
These functions always return the same results from the same input parameters and are therefore predictable. They do not use any
global state.
However, there are functions in PHP that are not deterministic. These include, for example, the htmlspecialchars()
function. Its third parameter, $encoding
, if not specified, defaults to the value of the configuration option
ini_get('default_charset')
. Therefore, it is recommended to always specify this parameter to avoid possible
unpredictable behavior of the function. Nette consistently does this.
Some functions, such as strtolower()
, strtoupper()
, and the like, have had non-deterministic behavior
in the recent past and have depended on the setlocale()
setting. This caused many complications, most often when
working with the Turkish language. This is because the Turkish language distinguishes between upper and lower case I
with and without a dot. So strtolower('I')
returned the ı
character and strtoupper('i')
returned the İ
character , which led to applications causing a number of mysterious errors. However, this problem
was fixed in PHP version 8.2 and the functions are no longer locale dependent.
This is a nice example of how global state has plagued thousands of developers around the world. The solution was to replace it with dependency injection.
When Is It Possible to Use Global State?
There are certain specific situations where it is possible to use global state. For example, when debugging code and you need to dump the value of a variable or measure the duration of a specific part of the program. In such cases, which concern temporary actions that will be later removed from the code, it is legitimate to use a globally available dumper or stopwatch. These tools are not part of the code design.
Another example is the functions for working with regular expressions preg_*
, which internally store compiled
regular expressions in a static cache in memory. When you call the same regular expression multiple times in different parts of
the code, it is compiled only once. The cache saves performance and is also completely invisible to the user, so such usage can be
considered legitimate.
Summary
We've shown why it makes sense
- Remove all static variables from the code
- Declare dependencies
- And use dependency injection
When contemplating code design, keep in mind that each static $foo
represents a problem. In order for your code to
be a DI-respecting environment, it is essential to completely eradicate global state and replace it with dependency injection.
During this process, you may find that you need to split a class because it has more than one responsibility. Don't worry about it; strive for the principle of one responsibility.
I would like to thank Miško Hevery, whose articles such as Flaw: Brittle Global State & Singletons form the basis of this chapter.