MidТеория5 min

PHP 8.1

Enums, fibers, readonly properties, intersection types, never, first-class callable syntax

PHP 8.1 — Enums и Fibers

PHP 8.1 (ноябрь 2021) принёс два фундаментальных нововведения: перечисления (Enums) и Fibers для асинхронного программирования.

Enums (Перечисления)

Pure Enums

<?php

// Pure Enum — без значений
enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

// Использование
$suit = Suit::Hearts;

function playCard(Suit $suit): void
{
    echo $suit->name; // "Hearts"
}

playCard(Suit::Hearts); // OK
// playCard('Hearts');  // TypeError!

// Сравнение
var_dump($suit === Suit::Hearts); // true
var_dump($suit === Suit::Clubs);  // false

Backed Enums (с базовым типом)

<?php

// Backed Enum — string или int значения
enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';
    case Banned = 'banned';
}

// Получение значения
echo Status::Active->value; // "active"
echo Status::Active->name;  // "Active"

// Создание из значения
$status = Status::from('active');     // Status::Active
$status = Status::tryFrom('unknown'); // null (без исключения)
$status = Status::from('unknown');    // ValueError!

// Все cases
$all = Status::cases(); // [Status::Active, Status::Inactive, Status::Banned]

// Int-backed enum
enum Priority: int
{
    case Low = 1;
    case Medium = 2;
    case High = 3;
    case Critical = 4;
}

Методы и интерфейсы в Enums

<?php

interface HasLabel
{
    public function label(): string;
}

enum Color: string implements HasLabel
{
    case Red = '#FF0000';
    case Green = '#00FF00';
    case Blue = '#0000FF';

    // Enum может содержать методы
    public function label(): string
    {
        return match ($this) {
            self::Red   => 'Red Color',
            self::Green => 'Green Color',
            self::Blue  => 'Blue Color',
        };
    }

    // Статические методы тоже
    public static function fromHex(string $hex): self
    {
        return self::from($hex);
    }

    // Константы в Enum
    public const DEFAULT = self::Blue;
}

echo Color::Red->label(); // "Red Color"
echo Color::DEFAULT->value; // "#0000FF"

Ограничения Enums

<?php

enum Status: string
{
    case Active = 'active';

    // ❌ НЕЛЬЗЯ: свойства (properties)
    // public string $description; // Fatal error

    // ❌ НЕЛЬЗЯ: конструктор
    // public function __construct() {} // Fatal error

    // ❌ НЕЛЬЗЯ: наследование
    // Enums не могут наследовать друг друга
    // и не могут быть extended

    // ❌ НЕЛЬЗЯ: клонирование
    // clone $status; // Error

    // ✅ МОЖНО: константы, методы, implements, use (traits без свойств)
}

Подвох для экзамена: Enum НЕ может иметь свойства (properties). У Enum есть только cases, константы и методы. Это частый вопрос.

Enums в практике

<?php

// Отлично подходят для: статусы, роли, типы
enum OrderStatus: string
{
    case Pending = 'pending';
    case Processing = 'processing';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';

    public function canTransitionTo(self $new): bool
    {
        return match ($this) {
            self::Pending    => in_array($new, [self::Processing, self::Cancelled]),
            self::Processing => in_array($new, [self::Shipped, self::Cancelled]),
            self::Shipped    => $new === self::Delivered,
            self::Delivered, self::Cancelled => false,
        };
    }

    public function isFinished(): bool
    {
        return in_array($this, [self::Delivered, self::Cancelled]);
    }
}

// В type hints
function processOrder(Order $order, OrderStatus $status): void
{
    if (!$order->status->canTransitionTo($status)) {
        throw new DomainException("Invalid transition");
    }
}

Fibers

<?php

// Fiber — лёгкий поток выполнения с ручным переключением
// Основа для асинхронных фреймворков (ReactPHP, Amp, Swoole)

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('first');  // Приостановка, возврат 'first'
    echo "Got: $value\n";             // Получили 'second' при resume

    Fiber::suspend('third');           // Ещё одна приостановка
});

$result = $fiber->start();         // Запуск, получаем 'first'
echo "Fiber said: $result\n";     // "Fiber said: first"

$result = $fiber->resume('second'); // Возобновляем, передаём 'second'
// Вывод: "Got: second"
echo "Fiber said: $result\n";     // "Fiber said: third"

$fiber->resume(); // Fiber завершается

// Проверка состояния
var_dump($fiber->isRunning());     // false
var_dump($fiber->isTerminated());  // true
var_dump($fiber->isSuspended());   // false

Практический пример: неблокирующее чтение

<?php

// Fibers позволяют писать асинхронный код синхронным стилем
// Вместо callback hell или Promise chains

// Без fibers (callback style):
$client->get('/users', function ($response) use ($client) {
    $users = json_decode($response);
    $client->get("/users/{$users[0]->id}/orders", function ($orders) {
        // Вложенные callbacks...
    });
});

// С fibers (через async-фреймворк, использующий Fiber внутри):
$response = $client->get('/users');           // "Синхронный" вызов
$users = json_decode($response);
$orders = $client->get("/users/{$users[0]->id}/orders");
// Код выглядит синхронным, но внутри Fiber приостанавливается на I/O

Readonly Properties

<?php

class User
{
    // readonly — свойство можно установить только ОДИН раз
    public readonly int $id;
    public readonly string $email;

    public function __construct(int $id, string $email)
    {
        $this->id = $id;        // Инициализация — OK
        $this->email = $email;
    }
}

$user = new User(1, '[email protected]');
echo $user->id;     // 1
// $user->id = 2;   // Error: Cannot modify readonly property

// С constructor promotion — ещё компактнее
class Product
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly float $price,
    ) {}
}

// Ограничения readonly
class Example
{
    public readonly array $items;

    public function __construct()
    {
        $this->items = [1, 2, 3];
        // $this->items[] = 4; // Error! Нельзя модифицировать readonly
        // $this->items[0] = 0; // Error!
        // unset($this->items[0]); // Error!
    }
}

Важно: readonly защищает от переприсваивания, но объекты внутри readonly свойства можно мутировать (т.к. хранится ссылка).


Intersection Types

<?php

// Intersection type — объект должен реализовать ВСЕ типы
function processData(Countable&Iterator $data): void
{
    echo "Count: " . count($data) . "\n";
    foreach ($data as $item) {
        echo $item . "\n";
    }
}

// Только классы/интерфейсы (не скалярные типы!)
// int&string — НЕЛЬЗЯ

// Пример: фильтрация по нескольким интерфейсам
interface Cacheable
{
    public function getCacheKey(): string;
}

interface Serializable
{
    public function serialize(): string;
}

function store(Cacheable&Serializable $item): void
{
    $key = $item->getCacheKey();
    $data = $item->serialize();
    // ...
}

Never Return Type

<?php

// never — функция НИКОГДА не возвращает значение
// Она всегда бросает исключение или завершает скрипт

function abort(string $message): never
{
    throw new RuntimeException($message);
    // Код после throw недостижим
}

function redirect(string $url): never
{
    header("Location: $url");
    exit;
}

function infiniteLoop(): never
{
    while (true) {
        processQueue();
    }
}

// Отличие от void:
// void — возвращается, но без значения
// never — не возвращается НИКОГДА

First-class Callable Syntax

<?php

// Получение ссылки на функцию как Closure
$strlen = strlen(...);     // Closure
$result = $strlen('hello'); // 5

// Методы объекта
$user = new User();
$getName = $user->getName(...); // Closure

// Статические методы
$create = User::create(...); // Closure

// Полезно для callback
$names = array_map(
    strtolower(...),
    ['ALICE', 'BOB', 'CHARLIE']
);
// ['alice', 'bob', 'charlie']

// До PHP 8.1 использовали Closure::fromCallable()
$strlen = Closure::fromCallable('strlen');
// Или неудобные строки/массивы
$names = array_map('strtolower', ['ALICE', 'BOB']);

array_is_list()

<?php

// Проверяет, является ли массив "списком"
// Список = последовательные целые ключи от 0

var_dump(array_is_list([]));              // true
var_dump(array_is_list([1, 2, 3]));       // true
var_dump(array_is_list([0 => 'a', 1 => 'b'])); // true

var_dump(array_is_list([1 => 'a', 0 => 'b'])); // false (не по порядку)
var_dump(array_is_list(['a' => 1]));      // false (строковой ключ)

// Практическое применение: JSON
$list = [1, 2, 3];
echo json_encode($list); // [1,2,3] — JSON array

$assoc = ['a' => 1, 'b' => 2];
echo json_encode($assoc); // {"a":1,"b":2} — JSON object

// array_is_list() помогает предсказать формат JSON

Spread operator в именованных аргументах

<?php

function createUser(string $name, string $email, string $role = 'user'): void
{
    echo "$name ($email) - $role\n";
}

$args = ['name' => 'Alice', 'email' => '[email protected]', 'role' => 'admin'];
createUser(...$args); // Распаковка ассоциативного массива как named arguments
// PHP 8.1+: spread с ассоциативными массивами в литералах
$base = ['x' => 1, 'y' => 2];
$extended = [...$base, 'z' => 3]; // ['x' => 1, 'y' => 2, 'z' => 3]

Сводная таблица: PHP 8.1

Возможность Синтаксис
Pure Enums enum Suit { case Hearts; }
Backed Enums enum Status: string { case Active = 'active'; }
Fibers new Fiber(function() { Fiber::suspend(); })
Readonly properties public readonly int $id
Intersection types Countable&Iterator
never return type function abort(): never
First-class callables strlen(...)
array_is_list() Да
Spread в ассоциативных массивах [...$base, 'key' => 'val']

Типичные вопросы на экзамене

  1. Может ли Enum иметь свойства? Нет, только cases, константы и методы.
  2. Разница между from() и tryFrom()? from() бросает ValueError, tryFrom() возвращает null.
  3. Можно ли модифицировать содержимое readonly массива? Нет, $this->items[] = x вызовет ошибку.
  4. Могут ли intersection types содержать скалярные типы? Нет, только классы и интерфейсы.
  5. Разница между void и never? void возвращает (без значения), never не возвращает никогда.