OOP: Late static binding. static:: vs self::
Glossary overview

OOP: Late static binding. static:: vs self::

Late Static Binding (позднее статическое связывание) в PHP — это механизм, позволяющий в статических методах ссылаться на класс, который был вызван, а не на тот, где метод был определен. Он решает проблему наследования, сохраняя имя класса из последнего «неперенаправленного вызова» и используя ключевое слово static:: вместо self::.

Также из этой темы:

Основные аспекты:

  • self:: используют, когда поведение должно быть жёстко привязано к классу, где метод объявлен. Когда ты пишешь self::, PHP подставляет класс в момент компиляции. То есть буквально тот класс, внутри которого написан код.
  • static:: используют, когда код должен корректно работать в наследниках. static:: определяется в момент выполнения, а не в момент компиляции. PHP смотрит: «Кто реально вызвал метод?»
  • Использование: Применяется для статических методов, свойств и констант, обеспечивая правильный полиморфизм при наследовании.

Поэтому:

  • self:: = раннее связывание. Раннее – решение принимается заранее.
  • static:: = позднее связывание. Позднее – решение принимается в момент вызова.

Late static binding работает не только для методов, но и для статических свойств:

class A {
    protected static $name = 'A';

    public static function getName() {
        return static::$name;
    }
}

class B extends A {
    protected static $name = 'B';
}

echo B::getName(); // B

Если бы тут был self::$name, всегда возвращалось бы A.

То есть:

  • self:: говорит: «Мне плевать, кто меня вызвал, я работаю строго как класс A».
  • static:: говорит: «Я уважаю иерархию. Если меня вызвал B, значит работаю как B».

технический пример именно для методов:

class Animal {
    public static function speak() {
        echo "Animal sound";
    }

    public static function makeSound() {
        static::speak();
    }
}

class Dog extends Animal {
    public static function speak() {
        echo "Woof";
    }
}

// Call
Dog::makeSound();

// Output
Woof

Почему?

Потому что внутри makeSound() используется static::speak().
PHP во время выполнения смотрит: метод вызван через Dog, значит static = Dog.

Вот это и есть late static binding для методов.

static как возвращаемый тип

class A {
    public static function create(): static {
        return new static();
    }
}

Тут new static() создаёт экземпляр того класса, который реально вызвал метод.

Если вызвать B::create(), создастся объект B, а не A.

Почему это вообще понадобилось?

Потому что наследование без этого ведёт себя странно.

Если бы существовал только self::, ты бы не мог корректно переопределять статические методы в потомках.

Late static binding позволяет родительскому классу “уважать” потомков.

Использование

Late static binding особенно важен, когда родительский класс реализует общий алгоритм, а детали должны переопределяться в потомках.

class Report {
    public static function generate() {
        static::header();
        static::body();
        static::footer();
    }

    protected static function header() {
        echo "Base header\n";
    }

    protected static function body() {
        echo "Base body\n";
    }

    protected static function footer() {
        echo "Base footer\n";
    }
}

class PdfReport extends Report {
    protected static function body() {
        echo "PDF body\n";
    }
}

PdfReport::generate();

Выведет:

Base header
PDF body
Base footer

Если заменить static:: на self::, переопределение работать не будет.

Примеры с плагина CPM

Класс Plugin общий

// Plugin.php
final public static function getInstance(): static
{
    $class = static::class; // ← НЕ 'Plugin', а тот класс на котором вызвали

    if (!isset(self::$instances[$class])) {
        self::$instances[$class] = new static(); // ← создаёт экземпляр наследника
    }

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

// Use, create plugin CpmExtraFeatures
class CpmExtraFeatures extends Plugin

CpmExtraFeatures::getInstance()
  → static::class = 'CpmExtraFeatures'   (не 'Plugin')
  → new static()  = new CpmExtraFeatures() (не new Plugin())

Без static:: — singleton всегда создавал бы Plugin, а не наследника.

Класс Block общий

// Block.php (базовый класс)
public static function registerHooks(): void
{
add_action('acf/init', [static::class, 'registerBlock']);
// ↑
// static::class — конкретный блок
}

// Use
class CardsBlock extends Block

CardsBlock::registerHooks()
  → static::class = 'CardsBlock'
  → add_action('acf/init', [CardsBlock::class, 'registerBlock'])

class ProsConsBlock extends Block

ProsConsBlock::registerHooks()
  → static::class = 'ProsConsBlock'
  → add_action('acf/init', [ProsConsBlock::class, 'registerBlock'])

Код написан один раз в Block.php, но каждый наследник регистрирует свой callback.

Или в том же классе , блоки — static::name() и static::$blockName.

// Block.php
public static function name(): string
{
    return static::$blockName; // ← берёт свойство из наследника
}

public static function enqueueScripts(): void
{
    $block = static::name(); // 'cpm-cards' или 'cpm-pros-cons'
    apply_filters("cpm_extra_features/blocks/$block/style", []);
}
// CardsBlock.php
class CardsBlock extends Block {
    protected static string $blockName = 'cpm-cards';
}

// ProsConsBlock.php
class ProsConsBlock extends Block {
    protected static string $blockName = 'cpm-pros-cons';
}

CardsBlock::enqueueScripts()
  → static::name() → static::$blockName → 'cpm-cards'

ProsConsBlock::enqueueScripts()
  → static::name() → static::$blockName → 'cpm-pros-cons'

Или pluginPath():

// Plugin.php
public static function pluginPath(string $path = ''): string
{
    return plugin_dir_path(static::getInstance()->pluginFile());
    //                      ↑
    //                 static:: → CpmExtraFeatures::getInstance()
    //                           → $this->plugin_file (из наследника)
}

Наглядно: что было бы без static::

С self:: (неправильно):              С static:: (правильно):
─────────────────────────            ─────────────────────────
CardsBlock::registerHooks()          CardsBlock::registerHooks()
  self::class = 'Block'                static::class = 'CardsBlock'
  self::$blockName = ошибка            static::$blockName = 'cpm-cards'
  new self() = new Block()             new static() = new CardsBlock()

Все блоки были бы одинаковые ❌       Каждый блок — свой ✓

Итого

Late static binding – это механизм, который позволяет static:: определяться динамически во время выполнения, а не жёстко ссылаться на текущий класс как self::.

Это тонкая вещь. Но без неё наследование в PHP было бы гораздо менее гибким.