Metaprogramming

Late static binding: why self gave you the wrong class

The mistake

self and static look like two spellings of the same idea. Inside a class, both seem to mean “this class”, so people pick whichever one they saw last. And for a long time nothing goes wrong, because a class with nothing extending it has only one answer to give. The two keywords disagree the moment someone writes class User extends Model, and that person is usually not the person who wrote self.

The machine

Simulator · Late static binding

The class each keyword binds to, and what a wrapping call does to it, run on the tested reducer. Every case is checked against a real PHP 8.4 on each push, by scripts/verify-against-php.ts.

Model::make() returns new self(). Resolve it, then let someone extend the class.

Drive it

The panel opens on one class. Model::make() returns new self(), and there is nothing else for either keyword to resolve to.

  • Resolve it, then add the subclass. The factory does not change. The call site becomes User::make(). You still get a Model.
  • Change self to static. The call site does not move. One word in the factory does. Now you get a User.
  • Wrap the call in Model::make(). The factory still says new static() and you get a Model again. Then switch the wrapper to parent::make() and the User comes back.

The mechanism

PHP keeps two classes in mind while it runs a method.

The first is the class the body is written in. self means that one, and it is fixed when the file is written. Model::make() is defined in Model, so new self() inside it builds a Model, and no call site can change that. This is early binding: resolved from the source, before anything runs.

The second is the class that was called. static means that one. When you write User::make(), PHP does not find make() on User, so it runs the copy on Model, but it remembers that you named User. new static() reads that memory and builds a User. PHP calls that late binding, because the class is not settled until the call happens.

The rule covers more than new. self::class and static::class split the same way, and so do constants: self::LABEL reads the constant on Model while static::LABEL reads the one on User. Whatever the keyword binds to, every use of it in that method sees the same class.

One more rule sits on top of those two. PHP does not work the called class out again at each hop. It carries it from one static call to the next, and only some calls carry it. static::, self:: and parent:: are forwarding calls: they pass the remembered class along untouched. Naming a class does not. Writing Model::make() sets the remembered class to Model, and everything downstream sees Model.

So these two wrappers are not interchangeable, even though they run the same method:

public static function build()
{
    return parent::make();  // User::build() gives you a User
}

public static function build()
{
    return Model::make();   // User::build() gives you a Model
}

The factory is correct in both. The second wrapper throws the answer away one line before it is used, and nothing in the file that contains new static() shows it.

Eloquent is built on this. Model::newInstance() in laravel/framework 13.7 is $model = new static;, which is why hydrating a query result gives you back a User and not a bare Model. Change that one word and every hydrated model would come back as a Model.

In your code

A factory or a fluent constructor on a class other people extend wants static:

class Model
{
    public static function make(): static
    {
        return new static();
    }
}

class User extends Model {}

User::make();   // a User

The : static return type is worth adding on its own. It does not change what the method returns, it checks it. With return new self() in the body, Model::make() still works and User::make() raises a TypeError instead of handing back the wrong class. A failure at the call is easier to read than a wrong object found three layers later.

The fine print

  • static is allowed as a return type and inside the body of a method. It is not allowed as a parameter type or a property type.
  • A method used through a trait binds to the class that uses the trait, not to the trait. Traits have their own rules and this page does not model them.
  • A call through a variable class name, $c::make(), does not forward the remembered class. forward_static_call() and forward_static_call_array() are how you keep it in that case.
  • new parent() is legal and behaves like new self(): a fixed class from the source, one step up.
  • __callStatic is a different mechanism and lives on the magic methods page.
  • self is sometimes right. A singleton, or a class nobody is meant to extend, means self on purpose. The bug is self by accident.

Further reading

Spotted a problem, or have a way to make this clearer? Suggest an improvement.