- PVSM.RU - https://www.pvsm.ru -
Если спросить PHP-разработчиков, какую возможность они хотят увидеть в PHP, большинство назовет дженерики.
Поддержка дженериков на уровне языка была бы наилучшим решением. Но, реализовать их сложно [1]. Мы надеемся, что однажды нативная поддержка станет частью языка, но, вероятно, этого придется ждать несколько лет.
Данная статья покажет, как, используя существующие инструменты, в некоторых случаях с минимальными модификациями, мы можем получить мощь дженериков в PHP уже сейчас.
От переводчика: Я умышленно использую кальку с английского "дженерики", т.к. ни разу в общении не слышал, чтобы кто-то называл это "обобщенным программированием".
Данный раздел покрывает краткое введение в дженерики [2].
Ссылки для чтения:
Так как на данный момент невозможно определить дженерики на уровне языка, нам придется воспользоваться другой прекрасной возможностью — определить их в докблоках.
Мы уже используем этот вариант во множестве проектов. Взгляните на этот пример:
/**
* @param string[] $names
* @return User[]
*/
function createUsers(iterable $names): array { ... }
В коде выше мы делаем то, что возможно на уровне языка. Мы определили параметр $names
как нечто, что может быть перечислено. Также мы указали, что функция вернет массив. PHP выбросит TypeError
, если типы параметров и возвращаемое значение не соответствуют.
Докблок улучшает понимание кода. $names
должны быть строками, а функция обязана вернуть массив объектов User
. Сам по себе PHP не делает таких проверок. А вот IDE, такие как PhpStorm, понимают эту нотацию и предупреждают разработчика о том, что дополнительный контракт не соблюден. В добавок к этому, инструменты статического анализа, такие как Psalm, PHPStan и Phan могут валидировать корректность переданных данных в функцию и из неё.
Выше приведен самый простой пример дженерика. Более сложные способы включают возможность указания типа его ключей, наравне с типом значений. Ниже один из способов такого описания:
/**
* @return array<string, User>
*/
function getUsers(): array { ... }
Здесь сказано, что массив возвращаемых функцией getUsers
имеет строковые ключи и значения типа User
.
Статические анализаторы, такие как Psalm, PHPStan и Phan понимают данную аннотацию и учтут ее при проверке.
Рассмотрим следующий код:
/**
* @return array<string, User>
*/
function getUsers(): array { ... }
function showAge(int $age): void { ... }
foreach(getUsers() as $name => $user) {
showAge($name);
}
Статические анализаторы выбросят предупреждение на вызове showAge
с ошибкой, наподобие такой: Argument 1 of showAge expects int, string provided
.
К сожалению, на момент написания статьи PhpStorm этого не умеет.
Продолжим углубляться в тему дженериков. Рассмотрим объект, представляющий собой стек [6] :
class Stack
{
public function push($item): void { ... }
public function pop() { ... }
}
Стек может принимать любой тип объекта. Но что, если мы хотим ограничить стек только объектами типа User
?
Psalm и Phan поддерживают следующие аннотации:
/**
* @template T
*/
class Stack
{
/**
* @param T $item
*/
public function push($item): void;
/**
* @return T
*/
public function pop();
}
Докблок используется для передачи дополнительной информации о типах, например:
/** @var Stack<User> $userStack */
$stack = new Stack();
Means that $userStack must only contain Users.
Psalm, при анализе следующего кода:
$userStack->push(new User());
$userStack->push("hello");
Будет жаловаться на 2 строку с ошибкой Argument 1 of Stack::push expects User, string(hello) provided.
На данный момент PhpStorm не поддерживает данную аннотацию.
На самом деле, мы покрыли только часть информации о дженериках, но на данный момент этого достаточно.
Необходимо выполнить следующие действия:
На данный момент, сообщество PHP уже неофициально приняло данный формат дженериков (они поддерживаются большинством инструментов и их значение понятно большинству):
/**
* @return User[]
*/
function getUsers(): array { ... }
Тем не менее, у нас есть проблемы с простыми примерами, вроде такого:
/**
* @return array<string, User>
*/
function getUsers(): array { ... }
Psalm его понимает, и знает, какой тип у ключа и значения возвращаемого массива.
На момент написания статьи, PhpStorm этого не понимает. Используя данную запись я упускаю мощь статического анализа в реальном времени, предлагаемую PhpStorm-ом.
Рассмотрим код ниже. PhpStorm не понимает, что $user
имеет тип User
, а $name
— строковой:
foreach(getUsers() as $name => $user) {
...
}
Если бы я выбрал Psalm как инструмент статического анализа, я бы мог написать следующее:
/**
* @return User[]
* @psalm-return array<string, User>
*/
function getUsers(): array { ... }
Psalm все это понимает.
PhpStorm знает, что переменная $user
относится к типу User
. Но, он все еще не понимает, что ключ массива относится к строке. Phan и PHPStan не понимают специфичные аннотации psalm. Максимум, который они понимают в данном коде такой же, как в PhpStorm: the type of $user
Вы можете утверждать, что PhpStorm'у просто стоит принять соглашение array<keyType, valueType>
. Я с вами не соглашусь, т.к. считаю, что это диктование стандартов — задача языка и сообщества, а инструменты лишь должны им следовать.
Я предполагаю, что описанное выше соглашение будет тепло встречено большей частью PHP-сообщества. Той, которую интересуют дженерики. Тем не менее, все становится гораздо сложнее, когда речь идет о шаблонах. В настоящее время ни PHPStan, ни PhpStorm не поддерживают шаблоны. В отличие от Psalm и Phan. Их назначение схоже, но если вы копнете глубже, то поймете, что реализации немного отличаются.
Каждый из представленных вариантов является своего рода компромиссом.
Проще говоря, есть потребность в соглашении о формате записи дженериков:
Psalm имеет всю необходимую функциональность для проверки дженериков. Phan вроде как, тоже.
Я уверен, что PhpStorm внедрит дженерики как только в сообществе появится соглашении о едином формате.
Завершающая часть головоломки дженериков — это добавление поддержки сторонних библиотек.
Надеюсь, как только стандарт определения дженериков появится, большинство библиотек внедрят его. Тем не менее, это произойдет не сразу. Часть библиотек используются, но не имеют активной поддержки. При использовании статических анализаторов для валидации типов в дженериках важно, чтобы были определены все функции, которые принимают или возвращают эти дженерики.
Что произойдет, если ваш проект будет опираться на работу сторонних библиотек, не имеющих поддержку дженериков?
К счастью, данная проблема уже решена, и решением этим являются функции-заглушки. Psalm, Phan [7] и PhpStorm [8] поддерживают заглушки.
Заглушки — это обычные файлы, содержащие сигнатуры функций и методов, но не реализующие их. Добавляя докблоки в заглушки, инструменты статического анализа получают необходимую им дополнительную информацию. Например, если у вас имеется класс стека без тайпхинтов и дженериков, вроде такого.
class Stack
{
public function push($item)
{
/* some implementation */
}
public function pop()
{
/* some implementation */
}
}
Вы можете создать файл-заглушку, имеющую идентичные методы, но с добавлением докблоков и без реализации функций.
/**
* @template T
*/
class Stack
{
/**
* @param T $item
* @return void
*/
public function push($item);
/**
* @return T
*/
public function pop();
}
Когда статический анализатор видит класс стека, он берет информацию о типах из заглушки, а не из реального кода.
Возможность просто делиться кодом заглушек (например, через composer) была бы крайне полезна, т.к. позволяла бы делиться проделанной работой.
Сообществу нужно отойти от соглашений и определить стандарты.
Может быть, лучшим вариантом будет PSR про дженерики?
Или, может быть, создатели основных статических анализаторов, PhpStorm, других IDE и кто-либо из людей, причастных к разработке PHP (для контроля) могли бы разработать стандарт, которым бы пользовались все.
Как только стандарт появится, все смогут помочь с добавлением дженериков в существующие библиотеки и проекты, создавая Pull Request'ы. А там, где это невозможно, разработчики могут писать и обмениваться заглушками.
Когда все будет сделано, мы сможем пользоваться инструментами вроде PhpStorm для проверки дженериков в режиме реального времени, пока пишем код. Мы можем использовать инструменты статического анализа как часть нашего CI в качестве гарантии безопасности.
Кроме того, дженерики могут быть реализованы и в PHP (ну, почти).
Есть ряд ограничений. PHP — это динамичный язык, который позволяет делать много "магических" вещей, например таких [9]. Если вы используете слишком много магии PHP, может случиться так, что статические анализаторы не смогут точно извлечь все типы в системе. Если какие-либо типы неизвестны, то инструменты не смогут во всех случаях корректно использовать дженерики.
Тем не менее, основное применение подобного анализа — проверка вашей бизнес-логики. Если вы пишете чистый код, то не стоит использовать слишком много магии.
Это было бы наилучшим вариантом. У PHP открытый исходный код, и никто не мешает вам склонировать исходники и реализовать дженерики!
Просто игнорируйте все вышесказанное. Одно из главных преимуществ PHP в том, что он гибок в выборе подходящего уровня сложности реализации в зависимости от того, что вы создаете. С одноразовым кодом не нужно думать о таких вещах, как тайпхинтинг. А вот в больших проектах стоит использовать такие возможности.
Спасибо всем дочитавшим до этого места. Буду рад вашим замечаниям в ЛС.
Автор: berezuev
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/321119
Ссылки в тексте:
[1] сложно: https://www.youtube.com/watch?v=teKnckg5x7I&feature=youtu.be&t=1121
[2] дженерики: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D0%BE%D0%B1%D1%89%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[3] RFC: https://wiki.php.net/rfc/generics
[4] Phan: https://github.com/phan/phan/wiki/Generic-Types
[5] шаблоны: https://psalm.dev/docs/templated_annotations/
[6] стек: https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA
[7] Phan: https://github.com/phan/phan/wiki/How-To-Use-Stubs
[8] PhpStorm: https://github.com/JetBrains/phpstorm-stubs/tree/master/standard
[9] таких: https://www.youtube.com/watch?v=RfXO5Y-QqPo
[10] Источник: https://habr.com/ru/post/456466/?utm_campaign=456466&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.