<? phpukraine СПІВБЕСІДИ
Пошук по платформі
CORE PHP · MIDDLE ЧАСТО ПИТАЮТЬ

Чим self відрізняється від static і що таке late static binding?

`self::` і `parent::` - посилання на клас, зафіксовані на етапі компіляції за місцем написання коду, а `static::` резолвиться в рантаймі в клас, на якому виклик почався. Це і є late static binding (PHP 5.3+): завдяки йому `new static()` у батьківській фабриці створює нащадка, а `static::CONST` бере константу нащадка.

Чому `new self()` у батьківському класі повертає базовий клас, хоча я викликав метод на нащадку?
Що таке late static binding і з якої версії PHP воно працює?
У базовому класі синглтон на `self::$instance`. Що зламається, коли зʼявиться другий нащадок?
Коли `static::` і `self::` дадуть однаковий результат, а коли різний?
OOP static self late-static-binding inheritance factory

Різниця між self і static зводиться до моменту резолву. self і parent PHP розвʼязує на етапі компіляції за тим, у якому класі фізично написано рядок коду: self вказує на той самий клас, parent на його батька. static розвʼязується в рантаймі й дає «викликаний клас» (called class), тобто той, з якого виклик стартував. Це значення PHP тягне в кадрі виклику разом із $this і скоупом, тому CsvReport::make() виконує тіло методу, написане в Report, але static усередині означає CsvReport. Механізм зʼявився в PHP 5.3 і називається late static binding: «late» тут про те, що привʼязка відкладена з компіляції на виконання. Прочитати викликаний клас можна через static::class (з PHP 5.5) або get_called_class(), а $obj::class доступний з PHP 8.0.

LSB охоплює чотири речі: new static(), static::method(), static::CONSTANT і static::$property. Приклад у коді показує обидва боки одразу. make() написаний один раз у базовому класі, але повертає той клас, на якому його викликали, і тип повернення static (PHP 8.0+) чесно про це говорить аналізаторам і IDE. А fileName() з self::FORMAT для будь-якого нащадка вперто віддає report.txt, бо звернення до константи вже скомпільовано в Report::FORMAT. Та сама історія з методами: self::render() в базовому класі викличе саме базову реалізацію, навіть якщо нащадок її перевизначив і навіть якщо ви в нестатичному контексті з живим $this. Якщо потрібен поліморфізм, пишіть $this->render() або static::render().

Окремо стоїть питання форвардингу. Виклики через self::, parent::, static:: і forward_static_call() передають викликаний клас далі: якщо нащадок робить parent::make(), то всередині Report::make() static усе ще вказує на нащадка. Явне звернення на кшталт Report::make() форвардингом не є і скидає викликаний клас на Report, тому в бібліотечному коді такий виклик ламає всю ідею фабрики. Друга межа проходить по private-методах: вони не перевизначаються, і якщо нащадок оголосить private-метод з тим самим імʼям, static::foo() з коду батька впаде фатальною помилкою про виклик private-методу з чужого скоупу. Тож static:: доречний лише для того, що нащадок реально може перевизначити.

Найдорожча помилка з LSB стосується статичних властивостей. static::$generated вибирає клас у рантаймі, але не створює нащадку окремої комірки: властивість існує в одному екземплярі там, де її оголошено, а нащадки лише дивляться на той самий слот. У прикладі Report::$generated після виклику CsvReport::make() дорівнює одиниці, і другий нащадок додаватиме туди ж. Через це саморобні синглтони виду static::$instance ??= new static() тихо повертають один обʼєкт на всю ієрархію: перший, хто встиг. Якщо лічильник чи кеш справді потрібен окремий, ключуйте його класом (static::$perClass[static::class]) або переоголошуйте властивість у кожному нащадку. Здоровіше рішення - віддати час життя обʼєкта DI-контейнеру й не тягнути глобальний стан у статику взагалі.

static доречний у named constructors і флюентних API, де базовий клас має повертати конкретний підтип: Collection::make(), withX()-методи, білдери запитів. Шкодить він там, де ви не готові підтримувати контракт конструктора: new static() мовчки обіцяє, що всі нащадки створюються тими самими аргументами, і перший нащадок з іншою сигнатурою ламає фабрику в рантаймі. Саме на це лається правило про небезпечне new static() у phpstan-strict-rules, і воно замовкає, коли клас або конструктор оголошено final. Робоче правило: static беріть для того, що ви свідомо віддаєте нащадкам, self - для внутрішньої механіки, яку перевизначати ніхто не має. А в final-класі різниці немає взагалі, тож вигадувати там гнучкість, якої ніхто не використає, немає сенсу.

declare(strict_types=1);

abstract class Report
{
    public const FORMAT = 'txt';

    /** Оголошено один раз - слот памʼяті один на всю ієрархію. */
    protected static int $generated = 0;

    /** @var array<class-string, int> лічильник окремо для кожного класу */
    protected static array $perClass = [];

    /** Тип повернення static дозволений з PHP 8.0. */
    public static function make(): static
    {
        static::$generated++;                       // спільний лічильник, не «свій» у нащадка
        $class = static::class;                     // викликаний клас, а не Report
        static::$perClass[$class] = (static::$perClass[$class] ?? 0) + 1;

        return new static();                        // Report::make() тут впаде: клас абстрактний
    }

    public function fileName(): string
    {
        return 'report.'.self::FORMAT;              // self зафіксовано на компіляції: завжди txt
    }

    public function realFileName(): string
    {
        return 'report.'.static::FORMAT;            // LSB: константа береться з викликаного класу
    }
}

final class CsvReport extends Report
{
    public const FORMAT = 'csv';
}

$report = CsvReport::make();

echo $report::class;                                // CsvReport - результат new static()
echo $report->fileName();                           // report.txt - класична пастка self::
echo $report->realFileName();                       // report.csv
echo Report::$generated;                            // 1: інкремент пішов у слот базового класу
Що `self` резолвиться в клас, де фізично написано код, а `static` - у «викликаний клас» (called class), який PHP тягне в кадрі виклику.
Що LSB поширюється на `new static()`, `static::method()`, `static::CONST` і `static::class`, а `get_called_class()` показує те саме значення.
Що `static` як тип повернення дозволили в PHP 8.0 і лише для return, у параметрах і властивостях його не буває.
Що статичні властивості не «розмножуються» по нащадках: якщо нащадок не переоголосив властивість, `static::$prop` і `self::$prop` пишуть в один і той самий слот.
Що виклики `self::`, `parent::`, `static::` і `forward_static_call()` форвардять викликаний клас далі, а явний `Base::method()` скидає його на `Base`.
Розуміння компромісу: `new static()` перетворює клас на точку розширення і накладає зобовʼязання на сигнатуру конструктора всіх нащадків.
Вважати, що `self::` в нестатичному методі викличе перевизначену версію, бо є `$this`: метод резолвиться в класі, де написано `self::`, і override просто пропускається.
Робити синглтон з `private static ?self $instance` у базовому класі й дивуватися, що `Csv::instance()` і `Xml::instance()` повертають один обʼєкт: властивість оголошена один раз, слот теж один.
Писати `new static()` у класі з конструктором на три аргументи, а потім у нащадку міняти сигнатуру конструктора: фабрика падає в рантаймі, і жоден інтерфейс від цього не рятує.
Викликати фабрику з `new static()` на абстрактному класі: `Error: Cannot instantiate abstract class`, бо викликаний клас саме абстрактний.
Розраховувати, що `static::privateMethod()` підхопить private-метод нащадка: private-методи не перевизначаються, і при збігу імен виклик валиться з «Call to private method ... from scope ...».
Ставити `static` як тип параметра або властивості: парсер це відкине, `static` допустимий тільки як тип повернення (PHP 8.0+).
ПОРАДА

Одна фраза, яка закриває питання: «`self` - це там, де код написано, `static` - це те, на чому виклик почався». Далі одразу дайте приклад різниці: `new self()` у фабриці базового класу назавжди прибиває результат до базового класу, а `new static()` віддає нащадка. І згадайте статичні властивості: LSB вибирає клас для читання, але не створює нащадку окремий слот памʼяті - на цьому валиться половина саморобних синглтонів.

оновлено 9 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`new static()` резолвиться в рантаймі в клас, на якому викликали метод, тому повернеться `CsvReport`. А `self::FORMAT` компілюється в звернення до `Report::FORMAT` і override нащадка не бачить. Тип повернення `static` цілком легальний з PHP 8.0.