Learn (Almost) Anything
КаталогСкачать курс

Современный PHP

Определяет scalar types, `null`, `mixed`, `never`, union/intersection/DNF types, nullable-синтаксис, coercion и реальную область действия `declare(strict_types=1)`. Ссылается на [Массивы как ordered map](course://article/Массивы как ordered map) и [PHPDoc, generics и статический анализ](course://article/PHPDoc, generics и статический анализ).

ruЭнциклопедия52 урока8 модулей10
Справочник PHPPHPСовременный PHPЯдро языкаДанные и стандартная библиотекаОбъектная модельВеб-слой и HTTPHTTPБазы данных и безопасностьЭкосистема и качество кода

Типы и strict_types

Определяет scalar types, `null`, `mixed`, `never`, union/intersection/DNF types, nullable-синтаксис, coercion и реальную область действия `declare(strict_types=1)`. Ссылается на [Массивы как ordered map](course://article/Массивы как ordered map) и [PHPDoc, generics и статический анализ](course://article/PHPDoc, generics и статический анализ).

Что PHP называет типом

Тип в PHP — это не только подсказка для IDE. Это часть контракта: какие значения можно передать в функцию, что она возвращает, какие значения может хранить свойство класса или константа класса. Проверка типов в PHP динамическая: многое выясняется во время выполнения, но сами объявления типов парсятся и частично проверяются раньше.

Базовые скалярные типы: bool, int, float, string. Писать нужно именно так: boolean, integer и похожие псевдонимы в type declaration не работают как ожидаемый скалярный тип. Кроме них есть array, object, callable, iterable, null, false, true, mixed, void, never, классы, интерфейсы и enum. Про устройство массивов как отдельного типа важно читать вместе с Массивы как ordered map, потому что PHP-массив — не просто «список».

<?php

declare(strict_types=1);

function formatPrice(int $cents, string $currency): string
{
    return number_format($cents / 100, 2) . ' ' . $currency;
}

echo formatPrice(1299, 'EUR'); // 12.99 EUR

Здесь объявлены типы двух параметров и тип результата. Если вызов нарушит контракт, PHP выбросит TypeError. Это уже пересекается с Ошибки, исключения и Throwable: ошибка типа — не доменное «товар не найден», а ошибка программы или границы ввода.

Быстрое повторение #rc-1

Что произойдёт, если функция объявлена как formatPrice(int $cents, string $currency): string, а вызов нарушит типы параметров?

Показать ответ

PHP выбросит TypeError. Это ошибка контракта функции, а не доменная ситуация вроде «товар не найден».

Где можно объявлять типы

Типы ставят у параметров, return value, свойств класса и, начиная с PHP 8.3, у констант класса. В обычном коде чаще всего видны первые два случая:

<?php

function userDisplayName(array $user): string
{
    return trim($user['first_name'] . ' ' . $user['last_name']);
}

Но array $user говорит только «сюда нужен массив». Он не говорит, что внутри есть ключи first_name и last_name. Для такой формы нужны PHPDoc и статический анализ: например, @param array{first_name: string, last_name: string} $user. Это не рантайм-проверка PHP, но практический способ описывать более точные структуры. См. PHPDoc, generics и статический анализ.

С объектами контракт обычно читается лучше:

<?php

final class UserName
{
    public function __construct(
        public readonly string $first,
        public readonly string $last,
    ) {}
}

function userDisplayName(UserName $name): string
{
    return trim($name->first . ' ' . $name->last);
}

Объектная модель, видимость и readonly свойства подробнее раскрываются в Классы, объекты и видимость.

Быстрое повторение #rc-2

Почему array $user в параметре не заменяет PHPDoc shape вроде array{first_name: string, last_name: string}?

Показать ответ

array $user проверяет только сам факт массива. Он не гарантирует наличие ключей first_name и last_name и типы значений внутри.

null, nullable и union types

null — отдельный тип и отдельное значение: «значения нет». Если функция может вернуть строку или отсутствие значения, это лучше писать явно:

<?php

function findEmailById(int $id): ?string
{
    if ($id === 42) {
        return 'ada@example.com';
    }

    return null;
}

?string — короткая форма string|null. Она работает только для одного базового типа. Если вариантов больше, используют union type через |:

<?php

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

Union type полезен на границах системы: данные из формы, JSON, query string, старого API. Но если union расползается по бизнес-логике, это часто сигнал, что модель ещё не оформлена. Условия, сравнения и match, которые обычно идут рядом с такими ветками, см. в Операторы и управляющие конструкции.

Быстрое повторение #rc-3

В PHP ?string — это короткая запись для чего?

Показать ответ

?string означает string|null. Такая nullable-запись работает только для одного базового типа.

mixed, void и never

mixed означает «может быть что угодно»: object|resource|array|string|float|int|bool|null. Это честнее, чем отсутствие типа, но не точнее. Хорошее место для mixed — низкоуровневый декодер, логгер, универсальный контейнер, адаптер к легаси-коду. Плохое место — основная доменная функция, где автор просто не решил, что она принимает.

<?php

function logValue(mixed $value): void
{
    error_log(var_export($value, true));
}

void у return type означает: функция ничего полезного не возвращает. Она может делать сайд-эффект: записать лог, отправить письмо, изменить состояние объекта.

never строже: функция вообще не возвращает управление. Она либо бросает исключение, либо вызывает exit, либо уходит в бесконечный цикл.

<?php

function fail(string $message): never
{
    throw new RuntimeException($message);
}

never бывает полезен для маленьких guard-функций: после вызова такой функции анализатор и человек понимают, что нормального продолжения нет.

Intersection и DNF types

Union type говорит «одно из»: A|B. Intersection type говорит «одновременно всё»: A&B. В PHP intersection работает для class-types: классов и интерфейсов.

<?php

function dumpCountableIterator(Iterator&Countable $items): void
{
    echo count($items) . PHP_EOL;

    foreach ($items as $item) {
        var_dump($item);
    }
}

Здесь объект должен быть и Iterator, и Countable. Это не «массив или итератор», а именно объект, удовлетворяющий обоим контрактам. Связанные темы — SPL, итераторы и коллекции, Наследование, интерфейсы и трейты и Функции, замыкания и callable.

DNF types появились для случаев, где нужно сочетать union и intersection. Запись выглядит так:

<?php

function handle((Iterator&Countable)|array $items): void
{
    foreach ($items as $item) {
        var_dump($item);
    }
}

Это читается как «либо объект, который одновременно Iterator и Countable, либо массив». DNF не стоит применять ради демонстрации силы типовой системы. Если сигнатура становится тяжёлой, иногда лучше ввести интерфейс или адаптер.

flowchart TD A[Вызов функции] --> B{В файле вызова есть declare(strict_types=1)?} B -->|Да| C[Скалярные аргументы проверяются строго] B -->|Нет| D[PHP может выполнить coercion скаляров] C --> E{Тип совпал?} D --> F{Можно привести значение?} E -->|Да| G[Функция выполняется] E -->|Нет| H[TypeError] F -->|Да| G F -->|Нет| H G --> I[Return type проверяется при возврате] I -->|Нарушен| H
flowchart TD
    A[Вызов функции] --> B{В файле вызова есть declare(strict_types=1)?}
    B -->|Да| C[Скалярные аргументы проверяются строго]
    B -->|Нет| D[PHP может выполнить coercion скаляров]
    C --> E{Тип совпал?}
    D --> F{Можно привести значение?}
    E -->|Да| G[Функция выполняется]
    E -->|Нет| H[TypeError]
    F -->|Да| G
    F -->|Нет| H
    G --> I[Return type проверяется при возврате]
    I -->|Нарушен| H
`strict_types` влияет на скалярные вызовы из конкретного файла, а не на весь проект сразу.

Что реально делает strict_types

По умолчанию PHP старается приводить скалярные значения к ожидаемому типу, если может. Например, функция ждёт int, а ей передали "10"; в coercive-режиме PHP может превратить строку в число. declare(strict_types=1) меняет это поведение, но только для вызовов, написанных в этом файле.

<?php

// lib.php
function double(int $value): int
{
    return $value * 2;
}
<?php

// strict-caller.php
declare(strict_types=1);

require __DIR__ . '/lib.php';

echo double('10'); // TypeError
<?php

// weak-caller.php
require __DIR__ . '/lib.php';

echo double('10'); // int(20)-подобное поведение: строка приводится к int

Ключевой факт: решает файл вызова, а не файл объявления функции. Поэтому strict_types=1 не «включает строгий режим во всём проекте». Его обычно ставят в начале каждого PHP-файла проекта, сразу после <?php, как было показано в Синтаксис, теги и подключение файлов.

Есть ещё одна тонкость: strict_types относится к скалярным объявлениям. Класс User не станет «более строгим» от этой директивы: объект либо является экземпляром нужного класса/интерфейса, либо нет. Также int допускается там, где ожидается float: это осознанное исключение, потому что целое число безопасно представимо как число с плавающей точкой.

Типичные ловушки

Первая ловушка — думать, что типы PHP заменяют валидацию. Они не проверят, что строка является email, что число положительное, что массив содержит нужные ключи. Для входных данных всё равно нужны фильтрация и валидация; см. GET, POST и фильтрация ввода.

Вторая — писать array там, где нужен объект или хотя бы PHPDoc shape. array быстро становится контейнером для всего подряд. Третья — использовать mixed как способ не принимать решение. Иногда это честно, но в прикладной логике точный тип почти всегда дешевле, чем будущая отладка.

Четвёртая — путать void и never. void возвращает управление без значения. never не возвращает управление вообще.

См. также

Источники

  1. PHP Manual: Type declarations
  2. PHP Manual: Type System
  3. PHP Manual: declare
  4. PHP Manual: Never
  5. PHP Manual: Mixed