Что 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\: ошибка типа — не доменное «товар не найден», а ошибка программы или границы ввода.
Где можно объявлять типы
Типы ставят у параметров, 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 свойства подробнее раскрываются в \Классы, объекты и видимость\.
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, которые обычно идут рядом с такими ветками, см. в \Операторы и управляющие конструкции\.
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 пробует привести скалярные аргументы"]
C --> E{"Тип аргумента совпал?"}
D --> F{"Значение можно привести к нужному типу?"}
E -->|"Да"| G["Функция выполняется"]
E -->|"Нет"| H["TypeError"]
F -->|"Да"| G
F -->|"Нет"| H
G --> I["Проверяется объявленный return type"]
I -->|"Значение подходит"| J["Результат возвращается"]
I -->|"Нарушен контракт"| HЧто реально делает 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 не возвращает управление вообще.
См. также
- \Синтаксис, теги и подключение файлов\ — где правильно ставить
declare(strict_types=1). - \Функции, замыкания и callable\ — параметры, return types, variadic и callable-сигнатуры.
- \Массивы как ordered map\ — почему
arrayв PHP шире, чем обычный список. - \PHPDoc, generics и статический анализ\ — как описывать shape-массивы, generic-коллекции и более точные контракты.
- \Ошибки, исключения и Throwable\ — что происходит при
TypeErrorи как он связан сThrowable. - \Классы, объекты и видимость\ — как классы делают типы выразительнее, чем массивы с договорённостями.