Зачем нужна типизация массивов в PHP

Источник: «Why you should be typing your arrays in PHP»
В PHP уже давно существует возможность типизировать параметры методов и функций. Добавление типов в массивы (и просто добавление типов в целом) поможет при использовании IDE или статического анализа, а также поможет другим разработчикам в команде понять ваш код. Также это поможет избежать ошибок и сделает код чище и читабельнее.

В принципе, в любой версии PHP, можно сделать что-то вроде:

function getNames(array $input): array {
return array_map(function($item) {
return $item->getName();
}, $input)
}

Однако это мало говорит о типах, с которыми имеем дело.

Базовые типы массивов

Указывая тип array или iterable, или что-то подобное, вы лишь указываете, что это массив. Но на самом деле хотите указать, что это массив, содержащий определённый тип. Если переписать предыдущий пример и сделать его более конкретным, можно получить следующее:

/**
* @param array<User> $input
*
* @return array<string>
*/

function getNames(array $input): array {
return array_map(function($item) {
return $item->getName();
}, $input)
}

Если добавить типы, то станет понятно, передаётся массив, содержащий объекты User, а возвращаем массив string. В этом есть множество преимуществ.

Преимущества типизированных массивов

Подсказки IDE

Первое преимущество типизированных массивов заключается в том, что теперь IDE будет понимать, что это за массив, и, следовательно, сможет предоставить автодополнение внутри функции. Это также поможет при попытке найти использование getName() в классе User, поскольку теперь IDE знает, что вызывается именно этот метод.

Это также поможет в работе с инструментами рефакторинга, например, если переименовать метод getName, то такая IDE, как PHPStorm, сможет обновить и вызов этого метода. Кроме того, если передадите массив неправильного типа или попытаетесь сделать что-то, что невозможно с возвращаемым массивом строк, вы получите ошибку в IDE.

Инструменты статического анализа

Второе преимущество типизированных массивов связано с использованием инструментов статического анализа (SA) (например, PHPStan). SA-инструменты понимают, что именно вы делаете, и выдают ошибки, если вы делаете что-то не то. Чем больше типов используется в кодовой базе, тем лучше они понимают код, и тем лучше могут помочь обнаружить проблемы до того, как они попадут в продакшен.

В PHPStan добавление объявлений типов становится обязательным на уровне 6. Возможно, в устаревших кодовых базах будет сложно сделать это повсеместно, но настоятельно рекомендую перейти этот уровень и сгенерировать базовую линию. Это потребует добавления типов в любой новый код.

Понимание разработчиков

Скорее всего, вы не единственный, кто работает над вашей кодовой базой. Если другой разработчик увидит метод getNames, для начала ему придётся выяснить, что за параметры у функции, прежде чем понять, что нужно делать с этим методом.

Не сосчитать, сколько раз при взгляде на метод в коде думал: Понятия не имею, что здесь происходит, потому что в методе либо не было типов, либо в качестве типа был только array. Сначала приходилось рыться в кодовой базе, в поисках, где вызывается метод, прежде чем становилось понятно, какие параметры и какие значения возвращаются.

Продвинутые типы массивов

Благодаря современным средствам PHP можно продвинуться гораздо дальше, чем просто задать @param array<Type> $value. Можно создавать списки, указывать ключи массива или быть уверенным, что он не пуст.

Тип list

Например, можно указать, что что-то является списком — list.

Список — массив, в котором ключи начинаются с 0 и каждый раз увеличиваются на единицу. Тип list может быть удобен, например, при конвертации в JSON, так как в этом случае он становится массивом, а не объектом. Если создать массив без ключей, он всегда будет списком, но при необходимости можно указать ключи.

$list = [1, 2, 3];
$alsoList = [
0 => 1,
1 => 2,
2 => 3,
];
// Ключи начинаются с 1, а не с 0, поэтому это не список.
$notList = [
1 => 1,
2 => 2,
3 => 3,
];
// Имеет строковый ключ, поэтому это не список.
$alsoNotList = [
'foo' => 'bar',
1,
2
];

Тип list не существует, но его можно использовать как показано в примере ниже, а такой инструмент, как PHPStan, поможет определить, если передаётся не список.

/**
* @param list<User> $input
*
* @return list<string>
*/

function getNames(array $input): array {
return array_map(function($item) {
return $item->getName();
}, $input)
}

Определение ключей

Можно даже указать, что ожидается определённый набор ключей, или просто указать, какого типа должны быть ключи. С помощью следующей нотации указываются литеральные ключи: array{key: Type, key2: Type}. Это также включает автодополнение IDE для этих ключей и все другие ранее упомянутые преимущества. Если необходимо указать только общие типы, например, если ключи являются строками, то можно сделать следующим образом: array<string, Type>. В общем случае массивы обозначаются как array<KeyType, ValueType>. Если передаётся только один тип, то это тип значения.

/**
* @param array{begin: int, interval: int} $data
*
* @return int
*/

function transform(array $data): int {
return $data['begin'] + $data['interval'];
}

/**
* @param array<string, mixed> $data
*
* @return string
*/

function concatKeys(array $data): string {
$output = '';
foreach($data as $key => $v) {
$output .= $key;
}
return $output;
}

Непустые типы

Можно указать, что массив не должен быть пустым. Для этого вместо array (или list) можно использовать non-empty-array (или non-empty-list). Если взять в качестве примера функцию concatKeys, можно определить её так, чтобы убедиться, что получаемый массив никогда не будет пустой. Также указываем, что возвращаемая строка тоже никогда не будет пустой.

/**
* @param non-empty-array<string, mixed> $data
*
* @return non-empty-string
*/

function concatKeys(array $data): string {
$output = '';
foreach($data as $key => $v) {
$output .= $key;
}
return $output;
}

И опять, это обеспечивает лучшее понимание инструментария SA, IDE и других разработчиков.

Заключение

Добавление типов в массивы (и просто добавление типов в целом) поможет при использовании IDE или статического анализа, а также поможет другим разработчикам в команде понять ваш код. Также это поможет избежать ошибок и сделает код чище и читабельнее.

Типизация массивов помогает задокументировать замысел функции и то, как её следует использовать. А ещё лучше, когда современные инструменты показывают, если вы делаете что-то не так.

Дополнительные материалы

Предыдущая Статья

Что, если использовать контейнерные единицы измерения для... всего

Следующая Статья

MySQL 9.0 Community Edition: Ключевые возможности и улучшения