[Перевод] PHP дженерики уже сегодня (ну, почти)
Если спросить PHP-разработчиков, какую возможность они хотят увидеть в PHP, большинство назовет дженерики.
Но, реализовать их сложно. Поддержка дженериков на уровне языка была бы наилучшим решением. Мы надеемся, что однажды нативная поддержка станет частью языка, но, вероятно, этого придется ждать несколько лет.
Данная статья покажет, как, используя существующие инструменты, в некоторых случаях с минимальными модификациями, мы можем получить мощь дженериков в PHP уже сейчас.
ни разу в общении не слышал, чтобы кто-то называл это "обобщенным программированием". От переводчика: Я умышленно использую кальку с английского "дженерики", т.к.
Содержание:
- Что такое дженерики
- Как внедрить дженерики без поддержки языка
- Стандартизация
- Поддержка инструментами
- Поддержка стороннего кода
- Дальнейшие шаги
- Ограничения
- Почему бы вам просто не добавить дженерики в язык?
- Что, если мне не нужны дженерики?
Что такое дженерики
Данный раздел покрывает краткое введение в дженерики.
Ссылки для чтения:
- RFC на добавление PHP дженериков
- Поддержка дженериков в Phan
- Дженерики и шаблоны в Psalm
Простейший пример
Так как на данный момент невозможно определить дженерики на уровне языка, нам придется воспользоваться другой прекрасной возможностью — определить их в докблоках.
Взгляните на этот пример: Мы уже используем этот вариант во множестве проектов.
/** * @param string[] $names * @return User[] */
function createUsers(iterable $names): array
Мы определили параметр $names
как нечто, что может быть перечислено. В коде выше мы делаем то, что возможно на уровне языка. PHP выбросит TypeError
, если типы параметров и возвращаемое значение не соответствуют. Также мы указали, что функция вернет массив.
$names
должны быть строками, а функция обязана вернуть массив объектов User
. Докблок улучшает понимание кода. А вот IDE, такие как PhpStorm, понимают эту нотацию и предупреждают разработчика о том, что дополнительный контракт не соблюден. Сам по себе PHP не делает таких проверок. В добавок к этому, инструменты статического анализа, такие как 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 этого не умеет.
Более сложные дженерики
Рассмотрим объект, представляющий собой стек : Продолжим углубляться в тему дженериков.
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 не поддерживает данную аннотацию.
На самом деле, мы покрыли только часть информации о дженериках, но на данный момент этого достаточно.
Как внедрить дженерики без поддержки языка
Необходимо выполнить следующие действия:
- На уровне сообщества определите стандарты дженериков в докблоках (например, новый PSR, либо возврат назад, к PSR-5)
- Добавьте докблок-аннотации в код
- Используйте IDE, понимающие эти обозначения, чтобы проводить статический анализ в режиме реального времени, с целью поиска несоответствий.
- Используйте инструменты статического анализа (такие как Psalm) как один из шагов CI, чтобы отловить ошибки.
- Определите метод для передачи информации о типах в сторонних библиотеках.
Стандартизация
На данный момент, сообщество 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
. Максимум, который они понимают в данном коде такой же, как в PhpStorm: the type of $user
Phan и PHPStan не понимают специфичные аннотации psalm.
Я с вами не соглашусь, т.к. Вы можете утверждать, что PhpStorm'у просто стоит принять соглашение array<keyType, valueType>
. считаю, что это диктование стандартов — задача языка и сообщества, а инструменты лишь должны им следовать.
Той, которую интересуют дженерики. Я предполагаю, что описанное выше соглашение будет тепло встречено большей частью PHP-сообщества. В настоящее время ни PHPStan, ни PhpStorm не поддерживают шаблоны. Тем не менее, все становится гораздо сложнее, когда речь идет о шаблонах. Их назначение схоже, но если вы копнете глубже, то поймете, что реализации немного отличаются. В отличие от Psalm и Phan.
Каждый из представленных вариантов является своего рода компромиссом.
Проще говоря, есть потребность в соглашении о формате записи дженериков:
- Они улучшают жизнь разработчиков. Разработчики могут добавить дженерики в свой код и получить от этого пользу.
- Разработчики могут использовать инструменты, которые им больше нравятся и переключаться между ними (инструментами) по мере необходимости.
- Создатели инструментов могут создавать эти самые инструменты, понимая пользу для сообщества и не опасаясь того, что что-то изменится, или что их обвинят в "неправильном подходе".
Поддержка инструментами
Phan вроде как, тоже. Psalm имеет всю необходимую функциональность для проверки дженериков.
Я уверен, что PhpStorm внедрит дженерики как только в сообществе появится соглашении о едином формате.
Поддержка стороннего кода
Завершающая часть головоломки дженериков — это добавление поддержки сторонних библиотек.
Тем не менее, это произойдет не сразу. Надеюсь, как только стандарт определения дженериков появится, большинство библиотек внедрят его. При использовании статических анализаторов для валидации типов в дженериках важно, чтобы были определены все функции, которые принимают или возвращают эти дженерики. Часть библиотек используются, но не имеют активной поддержки.
Что произойдет, если ваш проект будет опираться на работу сторонних библиотек, не имеющих поддержку дженериков?
Psalm, Phan и PhpStorm поддерживают заглушки. К счастью, данная проблема уже решена, и решением этим являются функции-заглушки.
Добавляя докблоки в заглушки, инструменты статического анализа получают необходимую им дополнительную информацию. Заглушки — это обычные файлы, содержащие сигнатуры функций и методов, но не реализующие их. Например, если у вас имеется класс стека без тайпхинтов и дженериков, вроде такого.
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'ы.
Мы можем использовать инструменты статического анализа как часть нашего CI в качестве гарантии безопасности. Когда все будет сделано, мы сможем пользоваться инструментами вроде PhpStorm для проверки дженериков в режиме реального времени, пока пишем код.
Кроме того, дженерики могут быть реализованы и в PHP (ну, почти).
Ограничения
PHP — это динамичный язык, который позволяет делать много "магических" вещей, например таких. Есть ряд ограничений. Если какие-либо типы неизвестны, то инструменты не смогут во всех случаях корректно использовать дженерики. Если вы используете слишком много магии PHP, может случиться так, что статические анализаторы не смогут точно извлечь все типы в системе.
Если вы пишете чистый код, то не стоит использовать слишком много магии. Тем не менее, основное применение подобного анализа — проверка вашей бизнес-логики.
Почему бы вам просто не добавить дженерики в язык?
У PHP открытый исходный код, и никто не мешает вам склонировать исходники и реализовать дженерики! Это было бы наилучшим вариантом.
Что, если мне не нужны дженерики?
Одно из главных преимуществ PHP в том, что он гибок в выборе подходящего уровня сложности реализации в зависимости от того, что вы создаете. Просто игнорируйте все вышесказанное. А вот в больших проектах стоит использовать такие возможности. С одноразовым кодом не нужно думать о таких вещах, как тайпхинтинг.
Буду рад вашим замечаниям в ЛС. Спасибо всем дочитавшим до этого места.