HardТеория8 min

Позднее статическое связывание

self:: vs static::, LSB, new static(), forwarding calls, практические паттерны

Позднее статическое связывание (Late Static Binding)

Проблема self::

self:: всегда ссылается на класс, в котором метод определен, а не на класс, который его вызывает. Это создает проблему при наследовании.

<?php
declare(strict_types=1);

class ParentModel
{
    protected static string $table = 'parents';

    public static function getTable(): string
    {
        return self::$table;  // Always refers to ParentModel::$table
    }

    public static function find(int $id): string
    {
        return sprintf('SELECT * FROM %s WHERE id = %d', self::getTable(), $id);
    }
}

class ChildModel extends ParentModel
{
    protected static string $table = 'children';
}

echo ParentModel::find(1);
// SELECT * FROM parents WHERE id = 1 ✓

echo ChildModel::find(1);
// SELECT * FROM parents WHERE id = 1 ✗ — WRONG! Expected 'children'

Проблема: self::$table в ParentModel::getTable() всегда ссылается на ParentModel, даже при вызове через ChildModel.

Решение: static::

static:: разрешается на основе класса, который фактически вызывает метод во время выполнения (runtime). Это и есть Late Static Binding (LSB).

<?php
declare(strict_types=1);

class ParentModel
{
    protected static string $table = 'parents';

    public static function getTable(): string
    {
        return static::$table;  // Resolved at runtime — LSB
    }

    public static function find(int $id): string
    {
        return sprintf('SELECT * FROM %s WHERE id = %d', static::getTable(), $id);
    }
}

class ChildModel extends ParentModel
{
    protected static string $table = 'children';
}

echo ParentModel::find(1);
// SELECT * FROM parents WHERE id = 1 ✓

echo ChildModel::find(1);
// SELECT * FROM children WHERE id = 1 ✓ — CORRECT!

Как работает LSB

<?php
declare(strict_types=1);

class A
{
    public static function create(): static
    {
        echo 'Called class: ' . static::class . "\n";
        return new static();  // Creates instance of the CALLING class
    }

    public static function identify(): string
    {
        return static::class;  // Returns name of the CALLING class
    }
}

class B extends A
{
    // Inherits create() and identify()
}

class C extends B
{
    // Inherits from B which inherits from A
}

$a = A::create();     // Called class: A → instance of A
$b = B::create();     // Called class: B → instance of B
$c = C::create();     // Called class: C → instance of C

echo A::identify();   // A
echo B::identify();   // B
echo C::identify();   // C

// Type checking
var_dump($a instanceof A);  // true
var_dump($b instanceof B);  // true
var_dump($b instanceof A);  // true — B extends A
var_dump($c instanceof C);  // true
var_dump($c instanceof A);  // true — C extends B extends A

new static() vs new self()

<?php
declare(strict_types=1);

class Animal
{
    public function __construct(
        protected readonly string $name,
    ) {
    }

    // new self() — ALWAYS creates Animal
    public function cloneWithSelf(): self
    {
        return new self($this->name . ' (self-clone)');
    }

    // new static() — creates instance of the ACTUAL class
    public function cloneWithStatic(): static
    {
        return new static($this->name . ' (static-clone)');
    }
}

class Dog extends Animal
{
    public function bark(): string
    {
        return "{$this->name} says Woof!";
    }
}

$dog = new Dog('Rex');

// new self() → creates Animal (NOT Dog!)
$selfClone = $dog->cloneWithSelf();
var_dump(get_class($selfClone));        // 'Animal'
// $selfClone->bark();                  // Error! Animal has no bark()

// new static() → creates Dog
$staticClone = $dog->cloneWithStatic();
var_dump(get_class($staticClone));      // 'Dog'
echo $staticClone->bark();             // 'Rex (static-clone) says Woof!'

Правило: Если метод должен возвращать экземпляр того же класса, что и вызывающий -- используйте new static() и return type static. Это критически важно для Fluent Interface, Builder, Active Record паттернов.

Static свойства и LSB

<?php
declare(strict_types=1);

class Counter
{
    protected static int $count = 0;

    public static function increment(): void
    {
        static::$count++;  // LSB — increments in the CALLING class
    }

    public static function getCount(): int
    {
        return static::$count;
    }

    public static function reset(): void
    {
        static::$count = 0;
    }
}

class UserCounter extends Counter
{
    protected static int $count = 0;  // Separate counter
}

class OrderCounter extends Counter
{
    protected static int $count = 0;  // Separate counter
}

Counter::increment();
Counter::increment();
UserCounter::increment();
OrderCounter::increment();
OrderCounter::increment();
OrderCounter::increment();

echo Counter::getCount();       // 2
echo UserCounter::getCount();   // 1
echo OrderCounter::getCount();  // 3
// Each class has its own counter thanks to LSB + overridden property

Без переопределения свойства

<?php
declare(strict_types=1);

class SharedCounter
{
    protected static int $count = 0;

    public static function increment(): void
    {
        static::$count++;
    }

    public static function getCount(): int
    {
        return static::$count;
    }
}

class ChildA extends SharedCounter
{
    // Does NOT override $count — shares with parent!
}

class ChildB extends SharedCounter
{
    // Does NOT override $count — shares with parent!
}

ChildA::increment();
ChildB::increment();
ChildB::increment();

echo SharedCounter::getCount();  // 3 — all share the SAME property
echo ChildA::getCount();         // 3 — same!
echo ChildB::getCount();         // 3 — same!

Ловушка экзамена: Если дочерний класс НЕ переопределяет static свойство, он использует свойство родителя. static::$prop в таком случае обращается к тому же $prop что и parent::$prop.

Forwarding и Non-Forwarding вызовы

PHP различает два типа вызовов:

  • Forwarding calls -- self::, parent::, static::, forward_static_call(). Они НЕ изменяют привязку.
  • Non-forwarding calls -- ClassName::method(), $object->method(). Они УСТАНАВЛИВАЮТ новую привязку.
<?php
declare(strict_types=1);

class Base
{
    public static function who(): string
    {
        return static::class;
    }

    public static function test(): string
    {
        return self::who();      // Forwarding call — preserves LSB context
    }

    public static function testParent(): string
    {
        return parent::who();    // Would be forwarding, but Base has no parent
    }
}

class Child extends Base
{
    public static function who(): string
    {
        return static::class;
    }

    public static function testSelf(): string
    {
        return self::who();      // Forwarding — preserves LSB context
    }
}

class GrandChild extends Child
{
    // Inherits everything
}

// Non-forwarding call: GrandChild::test()
// Sets LSB context to GrandChild
echo GrandChild::test();
// test() calls self::who() — forwarding call, preserves GrandChild context
// who() in Child returns static::class → 'GrandChild'

echo GrandChild::testSelf();
// testSelf() is defined in Child, calls self::who()
// self::who() is forwarding — preserves GrandChild context
// Result: 'GrandChild'
<?php
declare(strict_types=1);

class A
{
    public static function entry(): string
    {
        // static:: resolves to whatever class called entry()
        return static::helper();
    }

    protected static function helper(): string
    {
        return 'A::helper (static = ' . static::class . ')';
    }
}

class B extends A
{
    protected static function helper(): string
    {
        // parent:: is a forwarding call — preserves LSB context
        return 'B::helper, parent says: ' . parent::helper();
    }
}

class C extends B
{
    // Inherits B::helper
}

echo C::entry();
// 1. C::entry() — non-forwarding, sets LSB to C
// 2. entry() calls static::helper() — resolves to C
// 3. C inherits B::helper(), which calls parent::helper() (forwarding, LSB stays C)
// 4. A::helper() sees static::class = 'C'
// Result: 'B::helper, parent says: A::helper (static = C)'

Практические примеры

Active Record Pattern

<?php
declare(strict_types=1);

abstract class Model
{
    protected static string $table;
    protected static string $primaryKey = 'id';

    /** @var array<string, mixed> */
    protected array $attributes = [];

    public static function find(int $id): ?static
    {
        $table = static::$table;
        $pk = static::$primaryKey;

        // Simulated database query
        echo "SELECT * FROM {$table} WHERE {$pk} = {$id}\n";

        $instance = new static();
        $instance->attributes = ['id' => $id];
        return $instance;
    }

    /**
     * @return list<static>
     */
    public static function all(): array
    {
        $table = static::$table;
        echo "SELECT * FROM {$table}\n";
        return [];
    }

    public static function create(array $data): static
    {
        $instance = new static();
        $instance->attributes = $data;
        echo sprintf(
            "INSERT INTO %s (%s) VALUES (%s)\n",
            static::$table,
            implode(', ', array_keys($data)),
            implode(', ', array_map(fn($v) => "'{$v}'", $data)),
        );
        return $instance;
    }

    public function getAttribute(string $key): mixed
    {
        return $this->attributes[$key] ?? null;
    }
}

class User extends Model
{
    protected static string $table = 'users';
}

class Post extends Model
{
    protected static string $table = 'posts';
}

$user = User::find(1);    // SELECT * FROM users WHERE id = 1
$post = Post::find(42);   // SELECT * FROM posts WHERE id = 42

User::create(['name' => 'Alice', 'email' => '[email protected]']);
// INSERT INTO users (name, email) VALUES ('Alice', '[email protected]')

var_dump($user instanceof User);  // true — new static() created User
var_dump($post instanceof Post);  // true — new static() created Post

Singleton Pattern

<?php
declare(strict_types=1);

abstract class Singleton
{
    /** @var array<class-string, static> */
    private static array $instances = [];

    final protected function __construct() {}

    public static function getInstance(): static
    {
        $class = static::class;

        if (!isset(self::$instances[$class])) {
            self::$instances[$class] = new static();
        }

        return self::$instances[$class];
    }

    // Prevent cloning
    private function __clone(): void {}
}

class Database extends Singleton
{
    private string $dsn = '';

    public function connect(string $dsn): void
    {
        $this->dsn = $dsn;
    }

    public function getDsn(): string
    {
        return $this->dsn;
    }
}

class Cache extends Singleton
{
    /** @var array<string, mixed> */
    private array $store = [];

    public function set(string $key, mixed $value): void
    {
        $this->store[$key] = $value;
    }

    public function get(string $key): mixed
    {
        return $this->store[$key] ?? null;
    }
}

$db1 = Database::getInstance();
$db2 = Database::getInstance();
var_dump($db1 === $db2);  // true — same instance

$cache = Cache::getInstance();
var_dump($cache === $db1);  // false — different singletons

Factory Method Pattern

<?php
declare(strict_types=1);

abstract class Notification
{
    protected string $message;

    abstract public function send(): void;

    // Factory method using LSB
    public static function create(string $message): static
    {
        $instance = new static();
        $instance->message = $message;
        return $instance;
    }

    // Named constructors
    public static function urgent(string $message): static
    {
        $instance = static::create("[URGENT] {$message}");
        return $instance;
    }
}

class EmailNotification extends Notification
{
    public function send(): void
    {
        echo "Email: {$this->message}\n";
    }
}

class SmsNotification extends Notification
{
    public function send(): void
    {
        echo "SMS: {$this->message}\n";
    }
}

// Factory method creates correct type
$email = EmailNotification::create('Hello');
$sms = SmsNotification::urgent('Server down!');

$email->send();  // Email: Hello
$sms->send();    // SMS: [URGENT] Server down!

var_dump($email instanceof EmailNotification);  // true
var_dump($sms instanceof SmsNotification);      // true

get_called_class() и ::class

<?php
declare(strict_types=1);

class Base
{
    public static function info(): array
    {
        return [
            'self::class'          => self::class,          // Always 'Base'
            'static::class'        => static::class,        // Depends on caller
            'get_called_class()'   => get_called_class(),   // Same as static::class
        ];
    }

    public function instanceInfo(): array
    {
        return [
            'self::class'          => self::class,          // Always 'Base'
            'static::class'        => static::class,        // Depends on $this
            'get_class($this)'     => get_class($this),     // Same as static::class
            '$this::class'         => $this::class,         // PHP 8.0+ — same
        ];
    }
}

class Child extends Base {}

// Static context
var_dump(Base::info());
// self::class         → 'Base'
// static::class       → 'Base'
// get_called_class()  → 'Base'

var_dump(Child::info());
// self::class         → 'Base'   ← always the defining class
// static::class       → 'Child'  ← the calling class (LSB)
// get_called_class()  → 'Child'  ← same as static::class

// Instance context
$child = new Child();
var_dump($child->instanceInfo());
// self::class         → 'Base'
// static::class       → 'Child'
// get_class($this)    → 'Child'
// $this::class        → 'Child'

Запомни: static::class и get_called_class() эквивалентны. Для объектов $this::class, get_class($this) и static::class все возвращают фактический класс. self::class всегда возвращает класс определения метода.


Проверь себя

5 из 10

В каком паттерне LSB особенно важен?

Если дочерний класс НЕ переопределяет static свойство, что происходит?

Что вернет `self::class` при вызове метода из дочернего класса?

Эквивалентны ли `static::class` и `get_called_class()`?

Что такое Late Static Binding?