Avatar ·

В чём разница между new self и new static в PHP? Позднее статическое связывание

Всем привет! Третий день бьюсь над одной странностью в PHP. У меня есть базовый класс, который создаёт свои экземпляры через статический метод. Когда я наследую этот класс, я ожидаю, что дочерний класс будет создавать объекты своего типа, но почему-то создаётся объект родительского класса. Вот упрощённый пример: ```php class ParentClass { public static function create() { return new self(); } } class ChildClass extends ParentClass { // никакого переопределения } $obj = ChildClass::create(); // $obj — это ParentClass, а не ChildClass! echo get_class($obj); // ParentClass ``` Я читал про позднее статическое связывание (Late Static Binding) и про ключевое слово `static`, но до конца не понимаю, как это работает. Почему `new self()` всегда ссылается на тот класс, где этот код написан, а не на класс, из которого был вызван метод? И как мне сделать так, чтобы `ChildClass::create()` возвращал именно объект `ChildClass`, а не `ParentClass`? Объясните, пожалуйста, на пальцах, как различаются `new self()` и `new static()`, и в каких ситуациях что использовать. Заранее спасибо! <<>>

Ключевая идея: время разрешения класса

Разница между new self() и new static() — это разница между ранним (early) и поздним (late) статическим связыванием. Грубо говоря, self «замораживает» класс на момент компиляции, а static определяет его во время выполнения.

Как работает new self()

self всегда ссылается на тот класс, в котором физически написан код с этим ключевым словом. Даже если метод вызывается из дочернего класса, self не изменится. Это решается на этапе парсинга скрипта.

class A {
    public static function who() {
        echo __CLASS__; // всегда 'A'
    }
    public static function test() {
        self::who();    // всегда вызовет A::who()
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__; // 'B'
    }
}

B::test(); // выведет 'A', а не 'B'

Как работает new static() (позднее статическое связывание)

static — это «тот класс, который был вызван в цепочке наследования». PHP вычисляет его в момент выполнения, используя класс, который фигурировал в вызове (так называемый «вызывающий класс»).

class A {
    public static function who() {
        echo __CLASS__;
    }
    public static function test() {
        static::who();  // вызовет метод того класса, откуда вызвали test()
    }
}

class B extends A {
    public static function who() {
        echo __CLASS__;
    }
}

B::test(); // выведет 'B' — позднее связывание сработало!

Решение вашей проблемы: замените self на static

Чтобы ChildClass::create() возвращал объект ChildClass, измените код родительского класса:

class ParentClass {
    public static function create() {
        // Было: return new self();
        return new static(); // Теперь создаётся экземпляр вызванного класса
    }
}

class ChildClass extends ParentClass {
    // Дополнительные свойства или методы
}

$obj = ChildClass::create();
echo get_class($obj); // Теперь выведет 'ChildClass'

Важные нюансы и подводные камни

  • Не все операции с static поддерживают позднее связывание. Например, static::$property и static::CONST работают, а static::method() — да. Но parent:: всегда жёстко привязан к родителю.
  • Используйте static в фабричных методах. Это классический паттерн: статический метод, который должен возвращать экземпляр того класса, из которого его вызвали.
  • Будьте осторожны с переопределением. Если дочерний класс переопределит метод create(), то поведение new static() может измениться. Всегда проверяйте, не нарушает ли наследование вашу логику.
  • Производительность. static работает чуть медленнее self из-за дополнительных проверок во время выполнения, но в 99% случаев это незаметно.

Когда использовать self, а когда static?

  • new self() — когда вам точно нужен объект текущего класса, независимо от того, кто вызвал метод. Например, в неизменяемых (final) классах.
  • new static() — когда вы пишете библиотеку, трейт или абстрактный класс, который должен корректно работать с наследниками. Это основа для реализации паттернов «Строитель» и «Фабричный метод».

Запомните простое правило: self — это «где я написан», static — это «откуда меня вызвали». Надеюсь, теперь стало понятнее!

<<>>

Дополнение: практический пример с трейтами

Часто путаница возникает при использовании трейтов. Трейт — это не класс, а набор методов. Если в трейте написать new self(), то self будет ссылаться на класс, который использует трейт (фактически на тот класс, куда трейт «вставлен»). Но если трейт используется в нескольких классах, self будет разным для каждого из них — это может сбивать с толку.

trait FactoryTrait {
    public static function create() {
        return new self(); // self — это класс, который использует трейт
    }
}

class User {
    use FactoryTrait;
}

class Admin extends User {
    // create() унаследован
}

$admin = Admin::create();
echo get_class($admin); // User, а не Admin — снова та же проблема!

Решение то же самое — используйте new static() в трейте:

trait FactoryTrait {
    public static function create() {
        return new static(); // теперь всё работает как ожидается
    }
}

$admin = Admin::create();
echo get_class($admin); // Admin

Вывод: В трейтах и базовых классах, которые предполагают наследование, всегда отдавайте предпочтение static вместо self, если вам нужно создавать экземпляры дочерних классов. Это сделает ваш код гибким и предсказуемым для всех наследников.