Урок 03

strict_types, union types, nullable

Три ключевые темы

  • strict_types — как PHP проверяет типы.
  • Union types — один из нескольких типов.
  • Nullable — значение или null.

strict_types

Директива на уровне файла:

<?php

declare(strict_types=1);

Должна быть первой инструкцией (после <?php и комментариев).

Что меняется

Без strict:

function sum(int $a, int $b): int
{
    return $a + $b;
}

sum('5', '10');     // 15 — строки стали int
sum(true, false);   // 1
sum(3.5, 2.5);      // 6 (float → int с округлением?)

Со strict:

<?php

declare(strict_types=1);

function sum(int $a, int $b): int
{
    return $a + $b;
}

sum('5', '10');   // TypeError
sum(3.5, 2.5);    // TypeError (float → int неявно)
sum(5, 10);       // 15

Что не покрывает strict_types

Только вызовы функций в текущем файле. Не влияет на:

  • Арифметические операции.
  • Сравнение.
  • Присваивание.
  • Внутренние функции PHP (некоторые).

Пример:

<?php

declare(strict_types=1);

$result = '5' + 3;   // 8 — без ошибки

Арифметика не проверяется.

Правило

Всегда включай declare(strict_types=1) в начале каждого PHP-файла.

Исключение — шаблоны (Blade), конфиги с return [].

Union types

Один из нескольких типов (PHP 8.0+):

function formatId(int|string $id): string
{
    return (string) $id;
}

formatId(123);      // '123'
formatId('abc');    // 'abc'

Порядок типов

Не важен для PHP, но соглашение:

int|string           // хорошо
string|int           // тоже работает

Принято — примитивы сначала, null в конце.

С null

function f(int|string|null $x): void { }

Эквивалент ?:

function f(int|string|null $x): void { }

?type — только для одного типа:

?string           // string|null — ok
?string|int       // ошибка — ? работает только с одним типом
string|int|null   // правильно

Возврат

function parse(string $input): int|false
{
    return is_numeric($input) ? (int)$input : false;
}

Типичный паттерн — вернуть значение или false.

Ещё пример:

function findUser(int $id): User|null
{
    return User::find($id);
}

Массивы и union

function sum(array|int $values): int
{
    if (is_int($values)) {
        return $values;
    }

    return array_sum($values);
}

Сужение типов

PHP проверяет типы в блоке if:

function format(int|string $value): string
{
    if (is_int($value)) {
        return number_format($value);
    }

    // PHP знает, что здесь $value — string
    return strtoupper($value);
}

Анализаторы (PHPStan) тоже понимают.

Union с классами

function save(User|Order $entity): void
{
    if ($entity instanceof User) {
        // User
    } else {
        // Order
    }
}

Nullable

?type — короткая запись type|null:

function greet(?string $name): string
{
    return $name ?? 'Гость';
}

Со значением по умолчанию

function greet(?string $name = null): string
{
    return $name ?? 'Гость';
}

greet();          // Гость
greet('Иван');    // Иван
greet(null);      // Гость

Иначе:

function greet(string $name = null): string
// устаревший синтаксис, не используй

Возврат nullable

function findUser(int $id): ?User
{
    return User::query()->find($id);
}

Правило: если может вернуть null — указывай явно.

Проверка null

?? — coalescing:

$name = $user->name ?? 'Гость';

?-> — nullsafe (PHP 8.0+):

$city = $user?->address?->city;

Явная проверка:

if ($user !== null) {
    echo $user->name;
}

if (is_null($user)) { }
if ($user === null) { }

Массивы и null

function first(array $items): ?string
{
    return $items[0] ?? null;
}

?? vs isset:

$data = ['a' => null];

$data['a'] ?? 'default';         // 'default' (null → default)
isset($data['a']);                // false
array_key_exists('a', $data);     // true

?? срабатывает при null и отсутствии ключа.

Nullsafe в цепочке

$city = $order?->user?->address?->city;

Если любой в цепочке null — результат null, без ошибок.

Без ?->:

$city = $order && $order->user && $order->user->address
    ? $order->user->address->city
    : null;

Дефолты через ??=

$count ??= 0;
$config['host'] ??= 'localhost';

Присваивает, только если null.

Практика

<?php

declare(strict_types=1);

final class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly ?string $email = null,
    ) {}
}

function findUser(int $id): ?User
{
    // имитация
    return $id === 1 ? new User(1, 'Иван', 'ivan@example.com') : null;
}

function formatEmail(?string $email): string
{
    return $email ?? 'email не указан';
}

$user = findUser(1);

if ($user !== null) {
    echo $user->name . ': ' . formatEmail($user->email) . "\n";
}

$missing = findUser(99);
echo $missing?->name ?? 'Пользователь не найден';   // Пользователь не найден

Частые ошибки

1. ? с union:

?string|int   // ошибка
string|int|null   // правильно

2. Возврат null без указания:

function f(): string { return null; }   // TypeError
function f(): ?string { return null; }  // ok

3. Забыть strict_types:

<?php
// без declare
function f(int $x): void {}
f('5');   // сработает без ошибки

4. ?? не работает с пустой строкой:

$value = '';
$result = $value ?? 'default';   // '' (не 'default')
$result = $value ?: 'default';   // 'default'

Итог

  • declare(strict_types=1) — всегда.
  • Union types — int|string, User|Order.
  • Nullable — ?string для одного типа, |null для union.
  • ?? — для null-значений.
  • ?-> — nullsafe.
  • Анализаторы — PHPStan, Psalm — ловят ошибки типов.

Дальше — match и switch: новый синтаксис.