Зачем нужна типизация массивов в PHP
В принципе, в любой версии 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 или статического анализа, а также поможет другим разработчикам в команде понять ваш код. Также это поможет избежать ошибок и сделает код чище и читабельнее.
Типизация массивов помогает задокументировать замысел функции и то, как её следует использовать. А ещё лучше, когда современные инструменты показывают, если вы делаете что-то не так.