Page MenuHomePhabricator

PHP Static Method Variable Scope
Open, WishlistPublic

Description

The behavior of "static" method variables in PHP is fairly subtle, and not entirely obvious to me from the documentation. Empirically, it appears:

  • Method variables declared static have (at least?) two possible scopes: per-subclass, or global.
  • Most of the time, static method variables are per-subclass.
  • static method variables in methods named __call() invoked through PHP magic dispatch are always global.
  • static method variables in private methods are global until PHP 7.4.0, and per-subclass after that.

I can't immediately find any PHP bugs describing these behaviors, or any description in the PHP 7.4.0 changelog about this.


Default Behavior

In PHP, the scope of a static method variable like this:

public function m() {
  static $x;

  // ...

}

...is normally per-subclass. For example, if class B extends class A, each class gets its own copy of $x:

<?php

class A {
    
    public function m() {
      static $x;
      if ($x === null) {
          $x = get_class($this);
      }
      return $x;
    }
    
}

class B extends A {
    
}

$a = new A();
$b = new B();

echo $a->m()."\n";
echo $b->m()."\n";

This emits:

A
B
https://3v4l.org/bRlJ4

This is consistent across all versions of PHP, and useful if you want to compute a map of something like the class's properties.


Behavior of __call()

What if, instead of naming this method m(), you name it __call() and invoke it by calling a method with no definition?

<?php

class A {
    
    public function __call($method, $arguments) {
      static $x;
      if ($x === null) {
          $x = get_class($this);
      }
      return $x;
    }
    
}

class B extends A {
    
}

$a = new A();
$b = new B();

echo $a->m()."\n";
echo $b->m()."\n";

Since there is no m() method, this calls to ...->m() invoke __call(...). This emits:

A
A
https://3v4l.org/k9PBI

...in all versions of PHP. That is, static variables in __call() do not get per-subclass scoping.

Note that you can "fix" this by moving the static variable to any other method (but: see below!) and calling that method from __call(). For example:

<?php

class A {
    
    public function __call($method, $arguments) {
      return $this->n();
    }
    
    public function n() {
      static $x;
      if ($x === null) {
          $x = get_class($this);
      }
      return $x;
    }
    
}

class B extends A {
    
}

$a = new A();
$b = new B();

echo $a->m()."\n";
echo $b->m()."\n";

This emits:

A
B

...in all versions of PHP.

Also note that if you call __call(...) directly, rather than using magic dispatch, you get per-subclass scoping behavior.


Behavior of private

What if, instead of defining public m(), we put the static variable in a private method?

<?php

class A {
    
    public function m() {
      return $this->n();
    }
    
    private function n() {
      static $x;
      if ($x === null) {
          $x = get_class($this);
      }
      return $x;
    }
    
}

class B extends A {
    
}

$a = new A();
$b = new B();

echo $a->m()."\n";
echo $b->m()."\n";

This emits:

A
B
PHP >= 7.4.0, https://3v4l.org/6bQWKA

...and:

A
A
PHP < 7.4.0, https://3v4l.org/6bQWKA

That is:

  • Until PHP 7.4.0, a static variable in a private method has no scoping behavior. A static variable in a protected or public method is scoped per-subclass.
  • In PHP 7.4.0 and later, method visibility no longer impacts the scoping of static variables.

Event Timeline

epriestley triaged this task as Wishlist priority.Mar 12 2021, 7:32 PM
epriestley created this task.

I think lint could reasonably emit two warnings about this:

  1. If a static variable is declared inside a __call() method, warn that static behaves differently in __call() than in other methods (and differently depending upon how __call() is invoked), and discourage use of static in __call().
  2. If a static variable is declared inside a private method, warn that static behaves differently in private methods before and after PHP 7.4, and discourage use of static in private methods.

If you really want global scoping, you can use a private static property on the class instead (versus a static variable in a method).

I don't see any clean approach when you want per-subclass scoping. The best you can do is put the static variable in a final protected method and leave a comment that it's actually supposed to be private.