New self vs new static в PHP: что выбрать в 2025 году?
Представьте: вы пишете на PHP уже несколько лет, чувствуете себя уверенно, и вдруг ваш коллега (или Stack Overflow) задаёт вопрос: «Почему у меня в дочернем классе создаётся объект родительского класса, хотя я вызываю конструктор через new self()?». Знакомо? Если да, то вы не одиноки. Тема new self vs new static — один из самых популярных вопросов на Stack Overflow с тысячами просмотров. И в 2025 году, с выходом новых версий PHP, понимание этой разницы стало ещё важнее.
В этой статье мы разберёмся, чем отличаются self и static при создании объектов, как работает позднее статическое связывание (Late Static Binding) и, главное, когда что использовать. Поехали!
Проблема: self всегда указывает на родителя
Давайте начнём с классического примера. Предположим, у нас есть родительский класс BaseController и дочерний ApiController.
<?php
class BaseController {
public static function create() {
return new self(); // Внимание! self — это BaseController
}
}
class ApiController extends BaseController {
// Наследует create()
}
$api = ApiController::create();
var_dump($api); // object(BaseController)#1 (0) { }
?>
Мы ожидали получить объект ApiController, а получили BaseController. Почему? Потому что self в контексте родительского класса всегда ссылается на тот класс, где он написан, а не на класс, через который его вызвали. Это раннее статическое связывание. PHP «запоминает» класс во время компиляции, а не во время выполнения.
Решение: используем static
Чтобы исправить ситуацию, нужно заменить self на static. Это включает механизм позднего статического связывания (LSP).
<?php
class BaseController {
public static function create() {
return new static(); // static — это класс, который вызвал метод
}
}
class ApiController extends BaseController {
// Наследует create()
}
$api = ApiController::create();
var_dump($api); // object(ApiController)#1 (0) { }
?>
Теперь $api — это объект ApiController. static разрешается во время выполнения и указывает на класс, через который был вызван метод. Это и есть позднее статическое связывание.
Когда использовать self, а когда static?
В 2025 году лучшие практики чётко разделяют эти два подхода. Вот основные правила.
Используйте self, когда:
- Вы работаете внутри одного класса и не ожидаете наследования для данного метода.
- Вам нужно создать объект именно того класса, в котором написан код, а не дочернего.
- Вы обращаетесь к константам или статическим свойствам, которые не должны быть переопределены.
Пример: фабричный метод, который гарантированно возвращает объект базового класса.
<?php
class Logger {
protected static $instance;
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self(); // Всегда Logger
}
return self::$instance;
}
}
?>
Используйте static, когда:
- Вы проектируете код для расширения — например, абстрактные классы или трейты.
- Вам нужно, чтобы дочерние классы могли создавать свои собственные экземпляры.
- Вы используете паттерн Factory Method или Builder.
Пример: абстрактный класс для сущностей.
<?php
abstract class Entity {
public static function create(array $data): static {
$entity = new static();
// ... валидация и заполнение
return $entity;
}
}
class User extends Entity {}
class Post extends Entity {}
$user = User::create(['name' => 'Alice']);
$post = Post::create(['title' => 'Hello']);
?>
Сравнение self и static в таблице
Чтобы закрепить, вот наглядное сравнение:
| Критерий | self | static |
|---|---|---|
| Тип связывания | Раннее (compile-time) | Позднее (run-time) |
| Наследование | Всегда ссылается на класс, где написан | Ссылается на класс-вызывающий |
| Применение | Константы, приватные статические свойства, синглтон | Фабрики, билдеры, абстрактные методы |
| Риск | Неожиданное создание родительского объекта | Требует осторожности при цепочках вызовов |
Продвинутые сценарии: цепочки и трейты
В реальных проектах 2025 года вы часто встретите комбинацию self и static. Рассмотрим два популярных случая.
1. Цепочки вызовов (Fluent interface)
В трейтах или абстрактных классах static обязателен для возврата правильного типа.
<?php
trait HasTimestamps {
protected $createdAt;
public function setCreatedAt(string $time): static {
$this->createdAt = $time;
return $this; // Возвращаем static
}
}
class Article {
use HasTimestamps;
}
$article = (new Article())->setCreatedAt('2025-01-01');
var_dump($article); // object(Article)
?>
Если бы мы написали return $this без static в трейте — всё равно работает, так как $this уже является объектом. Но для статических методов возврат static критичен.
2. Паттерн Builder с наследованием
Представьте, что вы строите SQL-запросы. Базовый класс QueryBuilder и дочерний MySqlBuilder.
<?php
class QueryBuilder {
protected $query = '';
public static function table(string $name): static {
$builder = new static();
$builder->query = "SELECT * FROM $name";
return $builder;
}
public function where(string $condition): static {
$this->query .= " WHERE $condition";
return $this;
}
public function getQuery(): string {
return $this->query;
}
}
class MySqlBuilder extends QueryBuilder {
public function limit(int $count): static {
$this-