Новые возможности классов в PHP 8
Нововведения в классах PHP 8
PHP 8 принёс несколько значительных улучшений в объектно-ориентированное программирование. Основные новшества касаются объявления свойств, типизации, метаданных и гибкости конструкторов. Рассмотрим каждое из них подробно.
Как эффективно объявить свойства класса прямо в конструкторе?
Constructor Property Promotion позволяет сократить объём шаблонного кода. Свойства определяются непосредственно в списке параметров конструктора с указанием модификатора доступа (public, protected, private). PHP автоматически создаст одноимённое свойство и присвоит ему значение из аргумента.
class User
{
public function __construct(
public string $name,
private int $age
) {}
}
Пример выше эквивалентен следующему коду без промоушена:
class User
{
public string $name;
private int $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
}
Цель: уменьшить дублирование, улучшить читаемость. Случаи использования: простые классы-значения (DTO), модели, где свойства напрямую инициализируются через конструктор.
Типичные ошибки:
- Повторное объявление того же свойства в теле класса — приведёт к фатальной ошибке.
- Использование модификатора var — не допускается, только явные модификаторы доступа.
- Нельзя комбинировать продвигаемое свойство с обычным параметром без модификатора в том же конструкторе (допускается, но не рекомендуется из-за путаницы).
Как добавить метаданные к классам, методам или свойствам?
Атрибуты (Attributes) заменили устаревшие аннотации в PHPDoc. Атрибут — это класс, помеченный встроенным атрибутом #[Atribute]. Его можно применять к классам, методам, свойствам, константам, параметрам.
#[Attribute(Attribute::TARGET_CLASS)]
class Route
{
public function __construct(public string $path) {}
}
#[Route('/api/users')]
class UserController
{
// ...
}
// Получение атрибута через Reflection
$ref = new ReflectionClass(UserController::class);
$attrs = $ref->getAttributes(Route::class);
$route = $attrs[0]->newInstance();
echo $route->path; // /api/users
Цель: структурированное хранение конфигурации, валидации, middleware. Случаи использования: маршрутизация, сериализация, внедрение зависимостей (DI), валидация.
Проблемы:
- Атрибут должен быть объявлен как класс, что увеличивает количество файлов.
- Ошибка — неправильная цель атрибута (например, применить к методу атрибут, допускающий только классы).
- Нельзя использовать атрибуты с устаревшим синтаксисом (<< >>) — только #[...].
Как сделать свойства только для чтения после инициализации?
Readonly свойства помечаются модификатором readonly. После присвоения в конструкторе (или при объявлении со значением) их нельзя изменить. Модификатор можно комбинировать с типизованными свойствами и с промоушеном.
class Point
{
public function __construct(
public readonly float $x,
public readonly float $y
) {}
}
$p = new Point(3.14, 2.71);
// $p->x = 0.0; // Ошибка: Cannot modify readonly property
Цель: гарантировать неизменяемость объекта после создания. Случаи использования: Value Objects, DTO, конфигурационные объекты.
Ошибки:
- Readonly свойство не может быть static.
- В трейте readonly свойство обязательно должно быть инициализировано либо в трейте, либо в классе, который использует трейт.
- Нельзя присвоить readonly свойству значение после конструктора, даже в другом методе.
Как указать, что параметр может принимать значения нескольких типов?
Union Types позволяют задать несколько допустимых типов через символ |. Это работает для параметров, возвращаемых значений и свойств.
class Converter
{
public function convert(int|string $input): float|int
{
if (is_string($input)) {
return (float) $input;
}
return $input * 1.0;
}
}
Цель: гибкая типизация без отката к mixed. Случаи использования: функции-обработчики, парсеры, API-ответы.
Возможные трудности:
- Нельзя объединять void с другими типами.
- Типы false и null могут использоваться, но false не должен быть единственным типом (бессмысленно).
- Наследование — объединение типов в дочернем классе должно быть сужением (contravariance) или таким же; расширение (добавление новых типов) запрещено для параметров.
Как вернуть экземпляр класса, который вызвал метод (позднее статическое связывание)?
В PHP 8 появился тип возврата static, который указывает, что метод возвращает объект того же класса, что и был вызван. Это улучшает поддержку Fluent интерфейсов и паттерна Builder.
class Builder
{
private array $data = [];
public function add(string $key, mixed $value): static
{
$this->data[$key] = $value;
return $this;
}
public function build(): array
{
return $this->data;
}
}
class SpecialBuilder extends Builder
{
public function special(): static
{
$this->add('special', true);
return $this;
}
}
$builder = (new SpecialBuilder())->add('name', 'test')->special();
var_dump($builder::class); // SpecialBuilder
Цель: правильная типизация цепочек вызовов. Случаи использования: Fluent API, Builder, Factory с возвратом текущего класса.
Предостережения:
- Тип static можно использовать только как возвращаемый тип, не как тип параметра.
- В абстрактных классах static работает как ожидается, но требует осторожности при наследовании.
Расширенные примеры и неочевидные комбинации
1. Constructor Property Promotion + Readonly + Union Types
class Response
{
public function __construct(
public readonly int|string $code,
public readonly string $message = ''
) {}
}
$r = new Response(200, 'OK');
echo $r->code; // 200
echo $r->message; // OK
// $r->code = '404'; // Fatal error: Cannot modify readonly property
200OK
Объяснение: свойство code может быть как целым числом, так и строкой, и не может быть изменено после создания.
2. Атрибуты с несколькими аргументами и повторное использование
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Validate
{
public function __construct(
public string $rule,
public ?string $message = null
) {}
}
class Form
{
#[Validate('required')]
#[Validate('email', 'Некорректный email')]
public string $email;
}
$ref = new ReflectionProperty(Form::class, 'email');
$attrs = $ref->getAttributes(Validate::class);
foreach ($attrs as $attr) {
$v = $attr->newInstance();
echo "{$v->rule}: {$v->message}\n";
}
required: email: Некорректный email
Пояснение: атрибут Validate можно применять несколько раз к одному элементу. Каждый атрибут создаёт отдельный экземпляр класса.
3. Static Return Type в трейтах
trait WithLogger
{
private array $log = [];
public function log(string $msg): static
{
$this->log[] = $msg;
return $this;
}
}
class Application
{
use WithLogger;
public function run(): void
{
echo 'Running...';
}
}
$app = (new Application())->log('start');
echo $app->log; // массив с одним элементом
Running...
Важно: трейт возвращает static, поэтому цепочка работает с любым классом, использующим трейт.
4. Именованные аргументы в конструкторе с промоушеном
class Point3D
{
public function __construct(
public float $x = 0.0,
public float $y = 0.0,
public float $z = 0.0
) {}
}
$p = new Point3D(z: 10.5, x: 1.0);
echo "({$p->x}, {$p->y}, {$p->z})"; // (1, 0, 10.5)
(1, 0, 10.5)
Применение: пропуск необязательных параметров и указание только нужных. Это улучшает читаемость при большом количестве параметров.
5. Union Types с nullable и mixed
class Container
{
public function store(int|string|null $value): void
{
echo gettype($value);
}
public function fetch(): mixed
{
return $this->data;
}
}
(new Container())->store(null); // NULL
(new Container())->store(42); // integer
(new Container())->store('str'); // string
NULLintegerstring
Примечание: mixed подразумевает любой тип, но если нужна определённая комбинация, union types дают более точную проверку во время компиляции.