MidТеория5 min

PHP 8.0

JIT, named arguments, union types, match, nullsafe operator, attributes, constructor promotion

PHP 8.0 — Новая эра

PHP 8.0 (ноябрь 2020) — мажорный релиз с JIT-компилятором, новой системой атрибутов и множеством синтаксических улучшений.

JIT Compiler (Just-In-Time)

<?php

// JIT компилирует "горячий" PHP-код в машинный код
// Настройка в php.ini:
//
// opcache.jit=1255
// opcache.jit_buffer_size=256M
//
// Формат opcache.jit = CRTO:
// C: CPU-specific optimization (0=off, 1=on)
// R: Register allocation (0=off, 1=local, 2=global)
// T: Trigger (0=script load, 1=first exec, 2=profiling, 3=hot code, 4=@jit, 5=tracing)
// O: Optimization level (0=off, 1=minimal, 2=inline, 3=optimized, 4=full, 5=full+CFG)
//
// Рекомендуемые значения:
// tracing JIT (production): opcache.jit=tracing
// function JIT: opcache.jit=function
// Отключить: opcache.jit=disable

// JIT наиболее эффективен для:
// - Математических вычислений
// - Обработки данных в циклах
// - CPU-bound задач

// JIT менее эффективен для:
// - I/O-bound приложений (web)
// - Коротких скриптов
// - Кода с частыми вызовами встроенных функций

Named Arguments

<?php

// Позиционные аргументы (старый стиль)
array_slice([1, 2, 3, 4, 5], 1, 3, true);
// Что значит true? Непонятно без документации.

// Named arguments — читаемость
array_slice([1, 2, 3, 4, 5], offset: 1, length: 3, preserve_keys: true);

// Можно пропускать необязательные параметры
function createUser(
    string $name,
    string $email,
    string $role = 'user',
    bool $active = true,
    ?string $avatar = null,
): User {
    // ...
}

// Указываем только нужные
$user = createUser(
    name: 'Alice',
    email: '[email protected]',
    avatar: '/img/alice.jpg',  // Пропустили role и active
);

// Комбинирование позиционных и именованных
$user = createUser('Alice', '[email protected]', avatar: '/img/alice.jpg');
// Позиционные ДОЛЖНЫ идти ПЕРЕД именованными

Подвохи named arguments

<?php

// ПОДВОХ 1: Имена параметров стали частью API!
// Переименование параметра — breaking change
function divide(int $dividend, int $divisor): float
{
    return $dividend / $divisor;
}

divide(dividend: 10, divisor: 3); // OK
// Если переименовать $divisor в $by — сломается вызов

// ПОДВОХ 2: Нельзя передать один аргумент и позиционно, и именованно
// divide(10, dividend: 20); // Error: Named parameter $dividend overwrites previous argument

// ПОДВОХ 3: С variadic параметрами
function test(string ...$args) {}
test(a: 'first', b: 'second');
// $args = ['a' => 'first', 'b' => 'second'] — ассоциативный массив!

// ПОДВОХ 4: Порядок именованных аргументов не важен
divide(divisor: 3, dividend: 10); // OK, результат 3.33...

Union Types

<?php

// Тип может быть ОДНИМ ИЗ перечисленных
function processInput(int|string $input): string
{
    if (is_int($input)) {
        return "Number: $input";
    }
    return "String: $input";
}

processInput(42);       // OK
processInput('hello');  // OK
// processInput([]);    // TypeError

// В свойствах классов
class Setting
{
    public int|float $value;
    public string|bool|null $option;
}

// Return type
function findUser(int $id): User|null
{
    // Эквивалентно ?User, но union type более явный
    return UserRepository::find($id);
}

// false как отдельный тип в union
function search(string $needle, string $haystack): string|false
{
    return strpos($haystack, $needle); // Может вернуть int или false
}

Правило: void нельзя использовать в union types. null можно: int|null === ?int.


Match Expression

<?php

// match — замена switch с важными отличиями
$status = 'active';

// match использует СТРОГОЕ сравнение (===)
$label = match ($status) {
    'active'   => 'Active User',
    'inactive' => 'Inactive User',
    'banned'   => 'Banned User',
    default    => 'Unknown',
};

echo $label; // "Active User"

// ПОДВОХ: match использует ===, а switch использует ==
$value = 0;

// switch (опасно!)
switch ($value) {
    case 'hello': echo 'match!'; break;  // Сработает! 0 == 'hello' в PHP 7
    default: echo 'no match';
}

// match (безопасно)
$result = match ($value) {
    'hello' => 'match!',     // Не сработает: 0 !== 'hello'
    0       => 'zero!',      // Сработает
    default => 'no match',
};

// Множественные значения
$category = match ($statusCode) {
    200, 201, 204 => 'Success',
    301, 302      => 'Redirect',
    400, 422      => 'Client Error',
    500, 503      => 'Server Error',
    default       => 'Unknown',
};

// match без аргумента (аналог if-elseif)
$message = match (true) {
    $age < 13  => 'Child',
    $age < 18  => 'Teenager',
    $age < 65  => 'Adult',
    default    => 'Senior',
};

Отличия match от switch

Аспект switch match
Сравнение Нестрогое (==) Строгое (===)
Возвращает значение Нет Да — это выражение
break нужен Да Нет (нет fall-through)
default обязателен Нет Нет, но без default бросает UnhandledMatchError
Множественные значения Fall-through Через запятую

Nullsafe Operator ?->

<?php

// До PHP 8.0
$country = null;
if ($user !== null) {
    $address = $user->getAddress();
    if ($address !== null) {
        $country = $address->getCountry();
    }
}

// PHP 8.0+
$country = $user?->getAddress()?->getCountry();
// Если $user === null → null (не вызывает getAddress)
// Если getAddress() === null → null (не вызывает getCountry)

// Работает с методами, свойствами, массивами
$name = $user?->profile?->name;
$first = $user?->getOrders()?->first();

// ПОДВОХ: nullsafe operator short-circuits ВСЕЙ цепочку
// Но побочные эффекты до ?-> уже выполнены
$result = getUser()?->process(); // getUser() ВЫЗЫВАЕТСЯ всегда

// Нельзя использовать для записи:
// $user?->name = 'Alice'; // Syntax error

Constructor Property Promotion

<?php

// До PHP 8.0
class Point
{
    public float $x;
    public float $y;
    public float $z;

    public function __construct(float $x, float $y, float $z)
    {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
    }
}

// PHP 8.0+ — promoted properties
class Point
{
    public function __construct(
        public float $x,
        public float $y,
        public float $z,
    ) {
        // Свойства создаются и присваиваются автоматически
    }
}

// Можно смешивать promoted и обычные параметры
class User
{
    public string $fullName;

    public function __construct(
        public readonly int $id,  // promoted
        public string $firstName, // promoted
        public string $lastName,  // promoted
        string $middleName = '',  // обычный параметр (не создаёт свойство!)
    ) {
        $this->fullName = trim("$firstName $middleName $lastName");
    }
}

Attributes (атрибуты)

<?php

// Атрибуты — замена doc-block аннотаций
// Структурированные метаданные, доступные через Reflection API

#[Attribute]
class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Middleware
{
    public function __construct(public string $name) {}
}

class UserController
{
    #[Route('/users', method: 'GET')]
    #[Middleware('auth')]
    #[Middleware('throttle')]
    public function index(): array
    {
        return User::all();
    }

    #[Route('/users/{id}', method: 'GET')]
    public function show(int $id): User
    {
        return User::find($id);
    }
}

// Чтение атрибутов через Reflection
$ref = new ReflectionMethod(UserController::class, 'index');
$attributes = $ref->getAttributes(Route::class);

foreach ($attributes as $attr) {
    $route = $attr->newInstance();
    echo "Path: {$route->path}, Method: {$route->method}\n";
}

Новые строковые функции

<?php

$email = '[email protected]';

// str_contains() — замена strpos() !== false
if (str_contains($email, '@')) {
    echo 'Valid format';
}

// str_starts_with() — замена strncmp() === 0
if (str_starts_with($email, 'alice')) {
    echo 'Starts with alice';
}

// str_ends_with() — замена substr() === '...'
if (str_ends_with($email, '.com')) {
    echo 'Commercial domain';
}

// До PHP 8.0 приходилось делать:
if (strpos($email, '@') !== false) {} // Опасно! 0 == false в PHP 7
if (substr($email, 0, 5) === 'alice') {}
if (substr($email, -4) === '.com') {}

Throw Expression

<?php

// throw теперь выражение, а не statement
// Можно использовать в тернарном операторе, ??, fn()

$value = $input ?? throw new InvalidArgumentException('Input required');

$name = $user->getName() ?: throw new LogicException('Name cannot be empty');

// В arrow function
$validator = fn($value) => $value >= 0
    ? $value
    : throw new DomainException('Negative value');

// В short ternary
$config = $options['timeout'] ?? throw new RuntimeException('Timeout not set');

WeakMap

<?php

// WeakMap — как SplObjectStorage, но не предотвращает GC
$cache = new WeakMap();

$obj1 = new stdClass();
$obj2 = new stdClass();

$cache[$obj1] = ['computed' => 'data1'];
$cache[$obj2] = ['computed' => 'data2'];

echo count($cache); // 2

unset($obj1);
echo count($cache); // 1 — данные для $obj1 автоматически удалены

// Идеально для:
// - Кэширование вычислений, привязанных к объектам
// - ORM: маппинг Entity -> метаданные
// - Декораторы: привязка дополнительных данных к объектам

Другие изменения

mixed type

<?php

// mixed = int|float|string|bool|array|object|null|callable|resource
// Эквивалент "любой тип" — но ЯВНО документирует намерение
function handleRequest(mixed $input): mixed
{
    // Разработчик осознанно принимает любой тип
    return $input;
}

Stringable interface

<?php

// Все классы с __toString() автоматически реализуют Stringable
class Money
{
    public function __construct(
        private int $amount,
        private string $currency,
    ) {}

    public function __toString(): string
    {
        return "{$this->amount} {$this->currency}";
    }
}

function render(string|Stringable $value): string
{
    return (string) $value;
}

render(new Money(100, 'USD')); // "100 USD"
render('plain text');           // "plain text"

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

Возможность Синтаксис
JIT opcache.jit=tracing
Named arguments func(name: 'value')
Union types int|string
Match expression match ($x) { 1 => 'one' }
Nullsafe operator $obj?->method()
Constructor promotion public function __construct(public int $id)
Attributes #[Route('/path')]
str_contains/starts_with/ends_with Да
Throw expression $x ?? throw new E()
mixed type Да
WeakMap Да
Stringable Автоматически

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

  1. Какое сравнение использует match? Строгое (===), в отличие от switch (==).
  2. Можно ли использовать ?-> для записи в свойство? Нет, только для чтения.
  3. Что произойдёт, если ни один case в match не подойдёт и нет default? UnhandledMatchError.
  4. Именованные аргументы можно располагать перед позиционными? Нет, позиционные всегда первые.
  5. Promoted property без модификатора видимости? Ошибка. Нужен public, protected или private.