Remove static variables

One discreet update of PHP 8.1 is the upgrade of PHP static variables behavior in classes : Static variables in methods inheritance. Let’s review what are static variables in PHP, why they were problematic so far, and strategies to upgrade the code to remove this problem.

PHP static variables

While static properties and methods are quite known, static variables are more of a secret. They look like this in the code :

<?php
function counter() {
     static $count = 0;
     
     return ++$count;
}

echo count(); // 1
echo count(); // 2
?>

Contrary to mundane variables, the static variable will not die at the end of the function call, and stays alive until the next call. This is how the function in the example keep returning $count, after incrementation, and it displays a different value each time.

Static variables are convenient when you need to keep some value across calls, and this value is only useful to this function.

Inherited static variables

The static variables are available in methods, though they used to behave quite surprinsingly. They are actually created for each class from where they are called. They might end up as multiple instances, even though, they are only defined once. See it from this example by Nikita Popov :

<?php
class A {
    public static function counter() {
        static $i = 0;
        return ++$i;
    }
}
class B extends A {}
 
var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // int(1)
var_dump(B::counter()); // int(2)
?>

The static variable is used in the A and B classes, yet the method is inherited so it is actually the same code which is called. Until PHP 8.0, there would be a distinct static variable depending on which class is called.

This was fixed in PHP 8.1, where the behavior is now the same than a static property : the variables are now shared between the methods B::counter() and A::counter, and both are now the same.

<?php
class A {
    public static function counter() {
        static $i = 0;
        return ++$i;
    }
}
class B extends A {}
 
var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // int(3)
var_dump(B::counter()); // int(4)
?>

This bug will bite when a method of a class is inherited by another method, and also uses static variables. As we mentionned initially, this is quite rare, and calling a static method with different classes is also quite rare, so this might be a rather infrequent problem.

Which means that it will be quite hard to understand when it arise. So, there is a dedicated Exakat rule called Inherited Static Variable to detect them in your code. This is part of the Migration PHP 8.1 ruleset.

Upgrading static variables to static properties

One of the main advantage of static variables is that they are invisible from the outside. The method acts as a black box from the outside.

This is usually nice in the initial version of the code, until the moment the need to watch the value itself emerges. Then, one is dependant on the method itself to provide a way to access it, which is never convenient.

So, one easy way to upgrade a static variable is to make it a property. The property is now stored outside the method, and inside the class but readily available across multiple calls. It is also available for extra forensic and sollicitations, like with a simple getter.

<?php

class A {
    private $i = 0;

    public function counter() {
        return ++$this->i;
    }

    // this gets the counter, but do not increment it
    public function getI() {
        return $this->i;
    }
}
?>

Note that this solution moves the static variable to a normal property : static variable to static property is possible. Here, it would share the counter across ALL objects, which is not a desirable effect.

Injecting the static variable

Another option is to move the static variable as an injection, and have it always provided as an argument to the method. Basically, instead of storing the variable in the current object, it is now stored in the calling context, which has the responsability to create it, and provided it each time.

<?php

class A {
    public function counter(int &$i) {
        return ++$i;
    }
}

$counter = 0;
$a = new A;
echo $a->counter($i); // 1
echo $a->counter($i); // 2

?>

This example is still the initial counter example, so it is a bit simplistic, and it also requires a reference to avoid loosing the value in the method. It would have been quite different with a Counter object.

The main inconvenient of this approach is the necessity for the calling context (here global) to maintain this object until completion.

One less bug in my code

PHP 8.1 brought quite a nice list of evolution and bug fixes : the inherited static variables is one of them. It is quite rare, but also quite puzzling. The alternative solution is also simple enough that you may actually prepare your code by auditing it for compatibility PHP 8.1 with exakat, and then, decide how to make this upgrade. Even if this upgrade is not your next step, it will make the code safer to upgrade later.

Happy PHP auditing!