Static variables in inherited methods View as Markdown

Implemented (PHP 8.1)
Authors
Nikita Popov nikic@php.net
Date
2021-02-23
Target
PHP 8.1
Implementation
https://github.com/php/php-src/pull/6719
Source
wiki.php.net/rfc/static_variable_inheritance

Vote

Change static variable inheritance as proposed? closed
Yes
38
No
0
38 voters
asgrim Yes 2021-04-16 08:51
ashnazg Yes 2021-04-14 15:49
bwoebi Yes 2021-04-14 13:04
cmb Yes 2021-04-14 08:52
crell Yes 2021-04-14 15:09
cschneid Yes 2021-04-14 08:51
daverandom Yes 2021-04-21 21:44
derick Yes 2021-04-26 08:17
dharman Yes 2021-04-19 16:20
galvao Yes 2021-04-16 14:18
girgias Yes 2021-04-14 11:37
heiglandreas Yes 2021-04-24 06:07
jhdxr Yes 2021-04-19 18:28
kalle Yes 2021-04-15 15:49
kelunik Yes 2021-04-18 12:39
kguest Yes 2021-04-19 11:54
kinncj Yes 2021-04-22 21:42
kocsismate Yes 2021-04-14 08:32
lcobucci Yes 2021-04-15 10:42
marandall Yes 2021-04-14 10:27
mcmic Yes 2021-04-27 07:32
narf Yes 2021-04-14 11:24
nicolasgrekas Yes 2021-04-19 15:23
nikic Yes 2021-04-14 08:26
ocramius Yes 2021-04-19 16:06
patrickallaert Yes 2021-04-14 09:47
ramsey Yes 2021-04-14 18:15
reywob Yes 2021-04-15 08:15
sammyk Yes 2021-04-24 19:27
sebastian Yes 2021-04-14 08:32
sergey Yes 2021-04-14 08:33
sirsnyder Yes 2021-04-14 15:29
svpernova09 Yes 2021-04-14 15:27
tandre Yes 2021-04-14 16:46
theodorejb Yes 2021-04-14 12:56
trowski Yes 2021-04-15 21:13
twosee Yes 2021-04-19 06:55
yunosh Yes 2021-04-16 20:16

Discussions on the mailing list

[RFC] Static variables in inherited methods
by Nikita Popov · 7 messages
[VOTE] Static variables in inherited methods
by Nikita Popov · 2 messages

RFC text

Copied from wiki.php.net, last synchronized Oct 13, 2026 11:50 UTC

Introduction

When a method containing static variables is inherited, the inherited method currently uses an independent set of static variables. This RFC proposes to only have one set of static variables per method.

The current behavior of static variables is illustrated in the following example:

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)

When A::counter() is inherited into class B, a separate set of static variables is used, and A::counter() and B::counter() will manage an independent counter.

This RFC proposes to instead have only one set of static variables per method, regardless of whether it is inherited:

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)

Based on a perusal of bug reports relating to static variables, this appears to be the intuitively expected behavior. Notably, it matches how static class properties already behave:

class A {
    private static $i = 0;
    public static function counter() {
        return ++static::$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)

It also matches the behavior of other languages:

#include <iostream>
 
class A {
public:
    static int counter() {
        static int i = 0;
        return ++i;
    }
};
class B : public A {
};
 
int main() {
    std::cout << A::counter() << std::endl; // 1
    std::cout << A::counter() << std::endl; // 2
    std::cout << B::counter() << std::endl; // 3
    std::cout << B::counter() << std::endl; // 4
    return 0;
}

A problem with the current semantics is that there is no way to preserve them when overriding the method and invoking parent:::

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

The general expectation is that overriding a method and calling the parent method will not change program behavior. Under the previous semantics, it does, because there is no longer a separate static variable scope associated with B::counter(). There is no way to make the semantics match short of copying the implementation. Under the proposed semantics, the behavior is consistent.

Finally, there are some outright bugs in the handling of static variables. For example, if static variables are declared inside a constructor (and some other magic methods), then they do end up sharing a single static variable scope:

class A {
    public function __construct() {
        static $i = 0;
        var_dump(++$i);
    }
}
class B extends A {}
 
new A; // int(1)
new A; // int(2)
new B; // int(3)
new B; // int(4)

In this case, the buggy behavior under current semantics, coincides with correct behavior under the proposed semantics. Of course, if this proposal is not accepted, then the behavior should be fixed for current semantics.

Proposal

When a method containing static variables is inherited, the inherited method will share the same static variable scope:

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)

The behavior does not depend on whether the method is static or not:

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

However, static variables inside trait methods will continue to use a separate static variable scope for each use of the trait:

trait T {
    public static function counter() {
        static $i = 0;
        return ++$i;
    }
}
class A {
    use T;
}
class B {
    use T;
}
var_dump(A::counter()); // int(1)
var_dump(A::counter()); // int(2)
var_dump(B::counter()); // int(1)
var_dump(B::counter()); // int(2)

This is consistent with the general semantics of traits as “compiler-assisted copy and paste”. The code behaves as-if the method definition was literally pasted into both classes. This is also consistent with the behavior of static properties in traits. Additionally, it ensures that if a method is used multiple times under different aliases, they will all have distinct static variables.

Backward Incompatible Changes

The behavior of static variables in methods changes as described above. The current behavior of static variables is not documented. I consider it to be borderline buggy. Typical memoization use-cases for static variables will not be affected (apart from memoizing more effectively).

Code that intentionally relies on the current behavior can be made compatible by indexing the static variable by the class name:

class A {
    public function counter() {
        // This code works both for static and non-static methods.
        static $counters = [];
        $counters[static::class] ??= 0;
        return ++$counters[static::class];
    }
}
class B extends A {
}
 
var_dump((new A)->counter()); // int(1)
var_dump((new A)->counter()); // int(2)
var_dump((new B)->counter()); // int(1)
var_dump((new B)->counter()); // int(2)

This code will work the same way both before and after the proposed behavior change.

Vote

Voting started 2021-04-14 and ended 2021-04-28.

Vote: Change static variable inheritance as proposed? — Yes: 38 · No: 0 (closed)