Что такое дженерики php
PHP дженерики уже сегодня (ну, почти)
Если спросить PHP-разработчиков, какую возможность они хотят увидеть в PHP, большинство назовет дженерики.
Поддержка дженериков на уровне языка была бы наилучшим решением. Но, реализовать их сложно. Мы надеемся, что однажды нативная поддержка станет частью языка, но, вероятно, этого придется ждать несколько лет.
Данная статья покажет, как, используя существующие инструменты, в некоторых случаях с минимальными модификациями, мы можем получить мощь дженериков в PHP уже сейчас.
От переводчика: Я умышленно использую кальку с английского «дженерики», т.к. ни разу в общении не слышал, чтобы кто-то называл это «обобщенным программированием».
Содержание:
Что такое дженерики
Данный раздел покрывает краткое введение в дженерики.
Простейший пример
Так как на данный момент невозможно определить дженерики на уровне языка, нам придется воспользоваться другой прекрасной возможностью — определить их в докблоках.
Мы уже используем этот вариант во множестве проектов. Взгляните на этот пример:
Дженерики для определения ключей и значений перечисляемых типов
Выше приведен самый простой пример дженерика. Более сложные способы включают возможность указания типа его ключей, наравне с типом значений. Ниже один из способов такого описания:
Статические анализаторы, такие как Psalm, PHPStan и Phan понимают данную аннотацию и учтут ее при проверке.
Рассмотрим следующий код:
К сожалению, на момент написания статьи PhpStorm этого не умеет.
Более сложные дженерики
Продолжим углубляться в тему дженериков. Рассмотрим объект, представляющий собой стек :
Psalm и Phan поддерживают следующие аннотации:
Докблок используется для передачи дополнительной информации о типах, например:
Psalm, при анализе следующего кода:
Будет жаловаться на 2 строку с ошибкой Argument 1 of Stack::push expects User, string(hello) provided.
На данный момент PhpStorm не поддерживает данную аннотацию.
На самом деле, мы покрыли только часть информации о дженериках, но на данный момент этого достаточно.
Как внедрить дженерики без поддержки языка
Необходимо выполнить следующие действия:
Стандартизация
На данный момент, сообщество PHP уже неофициально приняло данный формат дженериков (они поддерживаются большинством инструментов и их значение понятно большинству):
Тем не менее, у нас есть проблемы с простыми примерами, вроде такого:
Psalm его понимает, и знает, какой тип у ключа и значения возвращаемого массива.
На момент написания статьи, PhpStorm этого не понимает. Используя данную запись я упускаю мощь статического анализа в реальном времени, предлагаемую PhpStorm-ом.
Если бы я выбрал Psalm как инструмент статического анализа, я бы мог написать следующее:
Psalm все это понимает.
Я предполагаю, что описанное выше соглашение будет тепло встречено большей частью PHP-сообщества. Той, которую интересуют дженерики. Тем не менее, все становится гораздо сложнее, когда речь идет о шаблонах. В настоящее время ни PHPStan, ни PhpStorm не поддерживают шаблоны. В отличие от Psalm и Phan. Их назначение схоже, но если вы копнете глубже, то поймете, что реализации немного отличаются.
Каждый из представленных вариантов является своего рода компромиссом.
Проще говоря, есть потребность в соглашении о формате записи дженериков:
Поддержка инструментами
Psalm имеет всю необходимую функциональность для проверки дженериков. Phan вроде как, тоже.
Я уверен, что PhpStorm внедрит дженерики как только в сообществе появится соглашении о едином формате.
Поддержка стороннего кода
Завершающая часть головоломки дженериков — это добавление поддержки сторонних библиотек.
Надеюсь, как только стандарт определения дженериков появится, большинство библиотек внедрят его. Тем не менее, это произойдет не сразу. Часть библиотек используются, но не имеют активной поддержки. При использовании статических анализаторов для валидации типов в дженериках важно, чтобы были определены все функции, которые принимают или возвращают эти дженерики.
Что произойдет, если ваш проект будет опираться на работу сторонних библиотек, не имеющих поддержку дженериков?
К счастью, данная проблема уже решена, и решением этим являются функции-заглушки. Psalm, Phan и PhpStorm поддерживают заглушки.
Заглушки — это обычные файлы, содержащие сигнатуры функций и методов, но не реализующие их. Добавляя докблоки в заглушки, инструменты статического анализа получают необходимую им дополнительную информацию. Например, если у вас имеется класс стека без тайпхинтов и дженериков, вроде такого.
Вы можете создать файл-заглушку, имеющую идентичные методы, но с добавлением докблоков и без реализации функций.
Когда статический анализатор видит класс стека, он берет информацию о типах из заглушки, а не из реального кода.
Возможность просто делиться кодом заглушек (например, через composer) была бы крайне полезна, т.к. позволяла бы делиться проделанной работой.
Дальнейшие шаги
Сообществу нужно отойти от соглашений и определить стандарты.
Может быть, лучшим вариантом будет PSR про дженерики?
Или, может быть, создатели основных статических анализаторов, PhpStorm, других IDE и кто-либо из людей, причастных к разработке PHP (для контроля) могли бы разработать стандарт, которым бы пользовались все.
Как только стандарт появится, все смогут помочь с добавлением дженериков в существующие библиотеки и проекты, создавая Pull Request’ы. А там, где это невозможно, разработчики могут писать и обмениваться заглушками.
Когда все будет сделано, мы сможем пользоваться инструментами вроде PhpStorm для проверки дженериков в режиме реального времени, пока пишем код. Мы можем использовать инструменты статического анализа как часть нашего CI в качестве гарантии безопасности.
Кроме того, дженерики могут быть реализованы и в PHP (ну, почти).
Ограничения
Есть ряд ограничений. PHP — это динамичный язык, который позволяет делать много «магических» вещей, например таких. Если вы используете слишком много магии PHP, может случиться так, что статические анализаторы не смогут точно извлечь все типы в системе. Если какие-либо типы неизвестны, то инструменты не смогут во всех случаях корректно использовать дженерики.
Тем не менее, основное применение подобного анализа — проверка вашей бизнес-логики. Если вы пишете чистый код, то не стоит использовать слишком много магии.
Почему бы вам просто не добавить дженерики в язык?
Это было бы наилучшим вариантом. У PHP открытый исходный код, и никто не мешает вам склонировать исходники и реализовать дженерики!
Что, если мне не нужны дженерики?
Просто игнорируйте все вышесказанное. Одно из главных преимуществ PHP в том, что он гибок в выборе подходящего уровня сложности реализации в зависимости от того, что вы создаете. С одноразовым кодом не нужно думать о таких вещах, как тайпхинтинг. А вот в больших проектах стоит использовать такие возможности.
Спасибо всем дочитавшим до этого места. Буду рад вашим замечаниям в ЛС.
UPD: ghost404 в комментариях отметил, что PHPStan с версии 0.12.x понимает psalm аннотации и поддерживает дженерики
О дженериках в PHP и о том, зачем они нам нужны
В этой статье мы рассмотрим некоторые распространённые проблемы, связанные с массивами в PHP. Все проблемы могут быть решены с помощью RFC, добавляющего в PHP дженерики. Мы не будем сильно углубляться в то, что такое дженерики, но к концу статьи вы должны понять, чем они полезны и почему многие так ждут их появления в PHP.
Допустим, у вас есть набор блог-постов, скачанных из какого-то источника данных.
Это распространённый сценарий, на примере которого мы обсудим роль дженериков и то, почему сообщество так нуждается в них. Рассмотрим проблемы, возникающие при таком сценарии.
Целостность структуры данных (Data integrity)
Если циклически пройти по нашему набору постов, то в результате получим критическую ошибку.
С целостностью структуры данных есть ещё одна сложность. Допустим, у вас есть метод, которому нужен массив блог-постов:
Но у этого подхода есть обратная сторона: вам придётся вызывать функцию применительно к распакованному массиву.
Производительность
Можно предположить, что лучше заранее знать, содержит ли массив только элементы определённого типа, чем потом в каждом цикле каждый раз вручную проверять типы.
Мы не можем прогнать на дженериках бенчмарки, потому что их пока нет, так что остаётся лишь гадать, как они повлияют на производительность. Но не будет безумием предположить, что оптимизированное поведение PHP, написанное на С, — это лучший способ решения проблемы по сравнению с созданием кучи кода для пользовательского пространства.
Автозавершение (Code completion)
Начиная с PHP 7.0 появились типы возвращаемых значений, а в PHP 7.1 они были улучшены с помощью void и типов, допускающих значение null. Но мы никак не можем сообщить IDE, что содержится в массиве. Поэтому мы возвращаемся к PHPDoc.
Неуверенность в содержимом массива и влиянии разбросанности кода на производительность и удобство сопровождения, а также неудобство написания дополнительных проверок заставили меня долго искать решение получше.
На мой взгляд, такое решение — дженерики. Не буду подробно расписывать, что они делают, об этом вы можете почитать в RFC. Но я приведу пример того, как дженерики могут помочь в решении вышеописанных проблем, всегда обеспечивая наличие в коллекции корректных данных.
Важное замечание: дженериков пока что нет в PHP. RFC предназначен для PHP 7.1, о его будущем нет никакой дополнительной информации. Нижеприведённый код основан на интерфейсах Iterator и ArrayAccess, которые существуют с PHP 5.0. В конце мы разберём пример с дженериками, представляющий собой фиктивный код.
Теперь можем воспользоваться подобным классом:
Работает! Даже без дженериков! Есть только одна проблема: решение немасштабируемое. Вам нужны отдельные реализации для каждого типа коллекции, даже если классы будут различаться только типом.
Вероятно, создавать подклассы можно с бо́льшим удобством, «злоупотребив» поздним статическим связыванием и рефлексивным API PHP. Но вам в любом случае понадобится создавать классы для каждого доступного типа.
Великолепные дженерики
И всё! Мы используем в качестве динамического типа, который можно проверять перед runtime. И опять же, класс GenericCollection можно было бы брать для любых типов.
PHP RFC: Generic Types and Functions
NOTE: a newer version of this RFC may be under development on GitHub.
Introduction
Also note that, while the syntax proposed by this specification may be similar to that of Hack/HHVM, compatibility with Hack is not a stated objective.
Proposal
The proposed syntax and feature set references various generic features of gradually-typed languages such as Dart and TypeScript, as well as statically-typed languages like C# and Java, in an attempt to create something that looks and feels familiar to those with generics experience from other languages.
Generic Types
A type (class/trait/interface) declaration is considered generic when the declaration includes one or more type parameter aliases, enclosed in angle brackets, immediately following the type-name.
A type parameter alias may optionally include an upper bound, e.g. a supertype of permitted type arguments for a given type parameter, which may be indicated by the use of the word is and a type-hint following the alias.
A type parameter may include a default type-hint, which will be applied in the absence of a given type-hint, indicated by an = sign folled by a type-hint, at the end of the type parameter.
The following demonstrates the proposed syntax for a generic class:
Note the use of type parameters in the class declaration Entry where two type aliases are defined.
An instance of a generic class can be constructed using explicit type arguments:
The type arguments, in this example, may also be inferred from the given arguments, rather than explicitly given:
In either case, note that neither the type parameters, not the type argument names, are part of the class name:
Extending a Generic Type
A class that extends a generic class can choose to complete the type arguments:
This class is not itself generic (e.g. doesn’t have any type parameters) although it extends a generic class.
Type-hinting and Type-checking Generic Types
Nested Type Arguments
Generic classes may be instantiated and generic functions/methods may be called with nested type arguments.
Upper Bounds
Bounds Checking
Traits
Type arguments must be supplied via the use clause.
Generic Functions and Methods
Generic Methods
Generic methods are subject to the same rules and behavior as generic functions, see above.
The same applies when overriding constructors and static methods.
Generic Constructors
Constructors may accept arbitrary type-arguments, just like any other method, e.g.:
In other words, the constructor may accept more type-arguments than those affecting the type.
Generic Closures
TODO describe callable type-hints and/or generic Closure and/or Function types
Type Checking
Bounded Polymorphism
TODO: decide whether or not bounded polymorphism should be supported.
Multiple Constraints
TODO: decide whether or not multiple constraints should be supported, e.g. with a Java-like syntax:
Autoloading
When autoloading is triggered e.g. by a new statement with a generic type, autoloading is triggered as normal, with only the class-name (without type parameters) being supplied.
Reflection
Type parameters, as well as type arguments given for type parameters, are made available via reflection.
TODO (some notes with ideas are available.)
Reification
This differs from generics in Hack, where type-hints are not reified and are unavailable at run-time. Type erasure would be inconsistent with the existing PHP type system, where any available type-information and declared type-hints are always available at run-time via reflection.
Related Enhancements
Static Type-checking With »instanceof»
These enhancements would seem natural and consistent with the addition of scalar type-hints in PHP 7, and implementation of the actual type-checks, are necessary under any circumstances, in order for instanceof to work properly in conjunction with scalar type arguments, and also are required for upper bound checks.
New Pseudo-types
Backward Incompatible Changes
No BC breaks are expected from this proposal.
Proposed PHP Version(s)
This proposal aims for PHP 7.1.
Proposed Voting Choices
For this proposal to be accepted, a 2/3 majority is required.
Patches and Tests
No patch has been written for this yet. As I’m not a C-coder myself, I encourage others to write a patch based on this proposal.
Some preliminary tests have been written for most key concepts and behaviors. Most notably, at this time, tests for reflection API enhancements are still missing.
The same fork also contains some experimental parser enhancements written by Dominic Grostate.
Related RFCs
Generics
Содержание
Generics [ править ]
Начиная с JDK 1.5, в Java появляются новые возможности для программирования. Одним из таких нововведений являются Generics. Generics являются аналогией с конструкцией «Шаблонов»(template) в С++, но имеет свои нюансы. Generics позволяют абстрагировать множество типов. Наиболее распространенными примерами являются Коллекции.
Вот типичное использование такого рода (без Generics):
Как правило, программист знает, какие данные должны быть в List’e. Тем не менее, стоит обратить особое внимание на Приведение типа («Cast») в строчке 3. Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется Cast. Cast не только создает беспорядки, но дает возможность появление ошибки «Runtime Error» из-за невнимательности программиста.
И появляется такой вопрос: «Как с этим бороться? » В частности: «Как же зарезервировать List для определенного типа данных?»
Как раз такую проблему решают Generics.
Некоторые могут задуматься, что беспорядок в коде увеличился, но это не так. Вместо приведения к Integer в строчке 3, у нас теперь есть Integer в качестве параметра в строчке 1. Здесь существенное отличие. Теперь компилятор может проверить этот тип на корректность во время компиляции.
Эффект от Generics особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
Свойства [ править ]
Пример реализации Generic-класса [ править ]
Теперь рассмотрим чем старая реализация кода отличается от новой:
List ─ список элементов E
Как видите, больше не нужно приводить Integer, так как метод get() возвращает ссылку на объект конкретного типа (в данном случае – Integer).
Несовместимость generic-типов [ править ]
Это одна из самых важных вещей, которую вы должны узнать о Generics
Как говорится: «В бочке мёда есть ложка дегтя». Для того чтобы сохранить целостности и независимости друг от друга Коллекции, у Generics существует так называемая «Несовместимость generic-типов».
Проблемы реализации Generics [ править ]
Пусть мы захотели написать метод, который берет Collection и выводит на экран. И мы захотели вызвать dump для Integer.
Проблема в том что эта реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекции, грубо говоря Collection имеет ограничения.
Для решения этой проблемы используется Wildcard («?»). Он не имеет ограничения в использовании(то есть имеет соответствие с любым типом) и в этом его плюсы. И теперь, мы можем вызвать dump с любым типом коллекции.
Пусть вы захотели сделать метод, который берет массив Object и переносить их в коллекцию.
Напомним, что вы не можете просто засунуть Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «Generic-Метод» Для этого перед методом нужно объявить и использовать его.
Но все равно после выполнение останется ошибка в третьей строчке :
Реализуем метод копирование из одной коллекции в другую
Реализуем метод нахождение максимума в коллекции.
Реализуем метод Swap в List
Ограничения Generic [ править ]
Также нужно запомнить простые правила для работы с Generics.
Преобразование типов [ править ]
В Generics также можно манипулировать с информацией, хранящийся в переменных.
PHP Generics. Right here. Right now
Многие PHP разработчики хотели бы видеть в PHP поддержку дженериков, и я в том числе.
RFC по их добавлению был создан ещё в 2016 году, но до сих пор не принял окончательный вид.
Я рассмотрел несколько вариантов решений поддержки дженериков в синтаксисе PHP, но не нашёл рабочей версии, которой мог бы воспользоваться обычный разработчик.
В итоге я решил, что могу сам попробовать реализовать такое решение на PHP.
Скриншот выше — реальный пример того, что у меня получилось.
Если хочется сразу попробовать, то вот библиотека mrsuh/php-generics и репо, в котором можно поиграться.
В качестве способа реализации дженериков я выбрал мономорфизацию.
Цитата отсюда. Оригинал тут.
Как работает?
Нужно подключить библиотеку как зависимость composer (минимальная версия PHP 7.4).
Добавить ещё одну директорию («cache/») в composer autoload PSR-4 для сгенерированных классов.
Она обязательно должна идти перед основной директорией.
composer.json
Для примера нужно добавить несколько PHP файлов:
Что делает скрипт composer dump-generics :
В данном случае должны быть сгенерированы:
Composer autoload сначала будет проверять, есть ли класс в директории «cache», а уже потом в директории «src».
Пример с кодом выше можно посмотреть тут.
Больше примеров можно посмотреть тут.
Особенности реализации
Какой синтаксис используется?
В RFC не определён конкретный синтаксис, поэтому я взял тот, который реализовывал Никита Попов.
Проблемы с синтаксисом
Для парсинга кода пришлось допилить nikic/php-parser.
Вот тут можно посмотреть изменения грамматики, которые пришлось внести для поддержки дженериков.
Внутри парсера используется PHP реализация YACC.
Реализация алгоритма YACC (LALR) и существующий синтаксис PHP не дают возможности использовать некоторые вещи, потому что они могут вызывать коллизии при генерации синтаксического анализатора.
Варианты решения можно почитать тут.
Поэтому на данный момент вложенные дженерики не поддерживаются.
Имена параметров не имеют каких-то специальных ограничений
Можно использовать несколько параметров в дженериках
Можно использовать значения по умолчанию
В каком месте класса можно использовать дженерики?
Пример класса, который использует дженерики:
В каком месте класса дженерика можно использовать параметры дженериков?
Пример класса дженерика:
Насколько быстро работает?
Все конкретные классы генерируются заранее, и их можно кешировать (не должно влиять на производительность).
Генерация множества конкретных классов должна негативно сказываться на производительности при:
Думаю, всё индивидуально, и нужно проверять на конкретном проекте.
Нельзя использовать без composer autoload
Магия с автозагрузкой сгенерированных конкретных классов будет работать только с composer autoload.
Если вы напрямую подключите класс с дженериком через require, то у вас ничего не будет работать из-за ошибки синтаксиса.
PhpUnit по своим соображениям подключает файлы тестов только через require.
Поэтому использовать классы дженериков внутри тестов PhpUnit не получится.
PhpStorm
Не поддерживает синтаксис дженериков, потому что даже RFC ещё не до конца сформирован.
Также PhpStorm не имеет работающего плагина для подключения LSP, чтобы иметь возможность поддерживаеть синтаксисы сторонних языков.
От поддержки Hack (который уже поддерживает дженерики) отказались.
VSCode
Поддерживает синтаксис дженериков после установки плагина для Hack.
Нет автодополнения.
Reflection
PHP выполняет проверки типов в runtime.
Значит, все аргументы дженериков должны быть доступны через reflection в runtime.
А этого не может быть, потому что информация о аргументах дженериков после генерации конкретных классов стирается.
Что не реализовано по RFC
Дженерики функций, анонимных функций и методов
Проверка типов параметров дженериков
T должен быть подклассом или имплементировать интерфейс TInterface.
Вариантность параметров
Существующие решения на PHP
Psalm Template Annotations
spatie/typed
TimeToogo/PHP-Generics
ircmaxell/PhpGenerics
Отличие от mrsuh/php-generics:
Заключение
Думаю, у меня получилось то, чего я хотел: библиотека легко устанавливается и может использоваться на реальных проектах.
Расстраивает то, что по понятным причинам популярные IDE не поддерживают в полной мере новый синтаксис дженериков, поэтому сейчас пользоваться им сложно.
Если у вас будут предложения или вопросы, можете оставлять их тут или в комментариях.