Новые возможности классов в PHP 8

Раздел: PHP разработка -> Объектно-ориентированное программирование в PHP

Нововведения в классах 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 дают более точную проверку во время компиляции.

Классы PHP 8 (нововведения) - comments

En
Php 8 class (php)