Позднее статическое связывание (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 typestatic. Это критически важно для 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всегда возвращает класс определения метода.