- PVSM.RU - https://www.pvsm.ru -
Многие PHP разработчики хотели бы видеть в PHP поддержку дженериков [1], и я в том числе. RFC [2] по их добавлению был создан ещё в 2016 году, но до сих пор не принял окончательный вид. Я рассмотрел несколько вариантов решений поддержки дженериков в синтаксисе PHP, но не нашёл рабочей версии, которой мог бы воспользоваться обычный разработчик.
В итоге я решил, что могу сам попробовать реализовать такое решение на PHP. Скриншот выше — реальный пример того, что у меня получилось.
Если хочется сразу попробовать, то вот библиотека mrsuh/php-generics [3] и репо [4], в котором можно поиграться.
В качестве способа реализации дженериков я выбрал мономорфизацию.
Цитата отсюда [5]. Оригинал тут [6].
Для тех, кто не слишком знаком, есть три основных способа реализации дженериков:
+ Type-erasure (стираемые): Дженерики просто удаляются и Foo<T> становится Foo. Во время выполнения дженерики ни на что не влияют, и предполагается, что проверки типов осуществляются на каком-то предварительном этапе компиляции/анализа (прим. Python, TypeScript).
+ Reification (реификация): Дженерики остаются в рантайме и могут быть на этом этапе использованы (и в случае PHP, могут быть проверены в рантайме).
+ Monomorphization (мономорфизация): С точки зрения пользователя, это очень похоже на реификацию, но подразумевает, что для каждой комбинации аргументов дженериков генерируется новый класс. То есть, Foo<T> не будет хранить информацию что, класс Foo инстанциирован с параметром T, а вместо этого будут созданы классы Foo_T1, Foo_T2, …, Foo_Tn специализированный для данного типа параметра.
Кратко:
Подробный алгоритм.
Нужно подключить библиотеку как зависимость composer (минимальная версия PHP 7.4).
composer require mrsuh/php-generics
Добавить ещё одну директорию ("cache/") в composer autoload PSR-4 для сгенерированных классов.
Она обязательно должна идти перед основной директорией.
composer.json
{
"autoload": {
"psr-4": {
"App\": ["cache/","src/"]
}
}
}
Для примера нужно добавить несколько PHP файлов:
Box
;Usage
, который его использует;autoload
и использует класс Usage
.src/Box.php
<?php
namespace App;
class Box<T> {
private ?T $data = null;
public function set(T $data): void {
$this->data = $data;
}
public function get(): ?T {
return $this->data;
}
}
src/Usage.php
<?php
namespace App;
class Usage {
public function run(): void
{
$stringBox = new Box<string>();
$stringBox->set('cat');
var_dump($stringBox->get()); // string "cat"
$intBox = new Box<int>();
$intBox->set(1);
var_dump($intBox->get()); // integer 1
}
}
bin/test.php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use AppUsage;
$usage = new Usage();
$usage->run();
Сгенерировать конкретные классы из классов дженериков командой composer dump-generics
.
composer dump-generics -v
Generating concrete classes
- AppBoxForString
- AppBoxForInt
- AppUsage
Generated 3 concrete classes in 0.062 seconds, 16.000 MB memory used
Что делает скрипт composer dump-generics
:
src/Usage.php
);В данном случае должны быть сгенерированы:
BoxForInt
и BoxForString
;Usage
, в котором все классы дженериков заменены на конкретные.cache/BoxForInt.php
<?php
namespace App;
class BoxForInt
{
private ?int $data = null;
public function set(int $data) : void
{
$this->data = $data;
}
public function get() : ?int
{
return $this->data;
}
}
cache/BoxForString.php
<?php
namespace App;
class BoxForString
{
private ?string $data = null;
public function set(string $data) : void
{
$this->data = $data;
}
public function get() : ?string
{
return $this->data;
}
}
cache/Usage.php
<?php
namespace App;
class Usage
{
public function run() : void
{
$stringBox = new AppBoxForString();
$stringBox->set('cat');
var_dump($stringBox->get());// string "cat"
$intBox = new AppBoxForInt();
$intBox->set(1);
var_dump($intBox->get());// integer 1
}
}
Сгенерировать актуальный vendor/autoload.php файл командой composer dump-autoload
.
composer dump-autoload
Generating autoload files
Generated autoload files
Запустить скрипт.
php bin/test.php
Composer autoload сначала будет проверять, есть ли класс в директории "cache", а уже потом в директории "src".
Пример с кодом выше можно посмотреть тут [4].
Больше примеров можно посмотреть тут [7].
В RFC [2] не определён конкретный синтаксис, поэтому я взял тот [8], который реализовывал Никита Попов.
Пример синтаксиса:
<?php
namespace App;
class Generic<in T: Iface = int, out V: Iface = string> {
public function test(T $var): V {
}
}
Для парсинга кода пришлось допилить nikic/php-parser [9]. Вот тут [10] можно посмотреть изменения грамматики, которые пришлось внести для поддержки дженериков. Внутри парсера используется PHP реализация [11] YACC [12]. Реализация алгоритма YACC (LALR) и существующий синтаксис PHP не дают возможности использовать некоторые вещи, потому что они могут вызывать коллизии при генерации синтаксического анализатора.
Пример коллизии:
<?php
const FOO = 'FOO';
const BAR = 'BAR';
var_dump(new DateTime<FOO, BAR>('now')); // кажется, что здесь есть дженерик
var_dump( (new DateTime < FOO) , ( BAR > 'now') ); // на самом деле нет
Варианты решения можно почитать тут [13].
Поэтому на данный момент вложенные дженерики не поддерживаются.
<?php
namespace App;
class Usage {
public function run() {
$map = new Map<Key<int>, Value<string>>();//не поддерживается
}
}
<?php
namespace App;
class GenericClass<T, varType, myCoolLongParaterName> {
private T $var1;
private varType $var2;
private myCoolLongParaterName $var3;
}
<?php
namespace App;
class Map<keyType, valueType> {
private array $map;
public function set(keyType $key, valueType $value): void {
$this->map[$key] = $value;
}
public function get(keyType $key): ?valueType {
return $this->map[$key] ?? null;
}
}
<?php
namespace App;
class Map<keyType = string, valueType = int> {
private array $map = [];
public function set(keyType $key, valueType $value): void {
$this->map[$key] = $value;
}
public function get(keyType $key): ?valueType {
return $this->map[$key] ?? null;
}
}
<?php
namespace App;
class Usage {
public function run() {
$map = new Map<>();//обязательно нужно добавить знаки "<>"
$map->set('key', 1);
var_dump($map->get('key'));
}
}
Пример класса, который использует дженерики:
<?php
namespace App;
use AppEntityCat;
use AppEntityBird;
use AppEntityDog;
class Test extends GenericClass<Cat> implements GenericInterface<Bird> {
use GenericTrait<Dog>;
private GenericClass<int>|GenericClass<Dog> $var;
public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> {
var_dump($var instanceof GenericInterface<int>);
var_dump(new GenericClass<int>::class);
var_dump(new GenericClass<array>::CONSTANT);
return new GenericClass<float>();
}
}
Пример класса дженерика:
<?php
namespace App;
class Test<T,V> extends GenericClass<T> implements GenericInterface<V> {
use GenericTrait<T>;
use T;
private T|GenericClass<V> $var;
public function test(T|GenericInterface<V> $var): T|GenericClass<V> {
var_dump($var instanceof GenericInterface<V>);
var_dump($var instanceof T);
var_dump(new GenericClass<T>::class);
var_dump(T::class);
var_dump(new GenericClass<T>::CONSTANT);
var_dump(T::CONSTANT);
$obj1 = new T();
$obj2 = new GenericClass<V>();
return $obj2;
}
}
Все конкретные классы генерируются заранее, и их можно кешировать (не должно влиять на производительность).
Генерация множества конкретных классов должна негативно сказываться на производительности при:
Думаю, всё индивидуально, и нужно проверять на конкретном проекте.
Магия с автозагрузкой сгенерированных конкретных классов будет работать только с composer autoload.
Если вы напрямую подключите класс с дженериком через require, то у вас ничего не будет работать из-за ошибки синтаксиса.
PhpUnit по своим соображениям [14] подключает файлы тестов только через require.
Поэтому использовать классы дженериков внутри тестов PhpUnit не получится.
PhpStorm
Не поддерживает синтаксис дженериков, потому что даже RFC [2] ещё не до конца сформирован.
Также PhpStorm не имеет работающего плагина [15] для подключения LSP [16], чтобы иметь возможность поддерживаеть синтаксисы сторонних языков.
От поддержки Hack [17] (который уже поддерживает дженерики) отказались [18].
VSCode
Поддерживает синтаксис дженериков после установки плагина для Hack [19].
Нет автодополнения.
PHP выполняет проверки типов в runtime [20]. Значит, все аргументы дженериков должны быть доступны [21] через reflection в runtime. А этого не может быть, потому что информация о аргументах дженериков после генерации конкретных классов стирается.
<?php
namespace App;
function foo<T,V>(T $arg): V {
}
T должен быть подклассом или имплементировать интерфейс TInterface.
<?php
namespace App;
class Generic<T: TInterface> {
}
<?php
namespace App;
class Generic<in T, out V> {
}
Особенности:
<?php
/**
* @template T
*/
class MyContainer {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/** @return T */
public function getValue() {
return $this->value;
}
}
Особенности:
<?php
$list = new Collection(T::bool());
$list[] = new Post(); // TypeError
<?php
$point = new Tuple(T::float(), T::float());
$point[0] = 1.5;
$point[1] = 3;
$point[0] = 'a'; // TypeError
$point['a'] = 1; // TypeError
$point[10] = 1; // TypeError
Особенности:
<?php
class Maybe {
private $MaybeValue;
public function __construct(__TYPE__ $Value = null) {
$this->MaybeValue = $Value;
}
public function HasValue() {
return $this->MaybeValue !== null;
}
public function GetValue() {
return $this->MaybeValue;
}
public function SetValue(__TYPE__ $Value = null) {
$this->MaybeValue = $Value;
}
}
<?php
$Maybe = new MaybestdClass();
$Maybe->HasValue(); //false
$Maybe->SetValue(new stdClass());
$Maybe->HasValue(); //true
$Maybe->SetValue(new DateTime()); //ERROR
<?php
$Configuration = new GenericsConfiguration();
$Configuration->SetIsDevelopmentMode(true);
$Configuration->SetRootPath(__DIR__);
$Configuration->SetCachePath(__DIR__ . '/Cache');
//Register the generic auto loader
GenericsLoader::Register($Configuration);
Особенности:
Test/Item.php
<?php
namespace test;
class Item<T> {
protected $item;
public function __construct(T $item = null)
{
$this->item = $item;
}
public function getItem()
{
return $item;
}
public function setItem(T $item)
{
$this->item = $item;
}
}
Test/Test.php
<?php
namespace Test;
class Test {
public function runTest()
{
$item = new Item<StdClass>;
var_dump($item instanceof Item); // true
$item->setItem(new StdClass); // works fine
// $item->setItem([]); // E_RECOVERABLE_ERROR
}
}
test.php
<?php
require "vendor/autoload.php";
$test = new TestTest;
$test->runTest();
Отличие от mrsuh/php-generics [3]:
Думаю, у меня получилось то, чего я хотел: библиотека легко устанавливается и может использоваться на реальных проектах. Расстраивает то, что по понятным причинам популярные IDE не поддерживают в полной мере новый синтаксис дженериков, поэтому сейчас пользоваться им сложно.
Если у вас будут предложения или вопросы, можете оставлять их тут [27] или в комментариях.
Автор: Anton Sukhachev
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/367833
Ссылки в тексте:
[1] дженериков: https://en.wikipedia.org/wiki/Generic_programming
[2] RFC: https://github.com/PHPGenerics/php-generics-rfc
[3] mrsuh/php-generics: https://github.com/mrsuh/php-generics
[4] репо: https://github.com/mrsuh/php-generics-example
[5] отсюда: https://habr.com/ru/company/skyeng/blog/543794
[6] тут: https://www.reddit.com/r/PHP/comments/j65968/ama_with_the_phpstorm_team_from_jetbrains_on/g83skiz/?context=3
[7] тут: https://github.com/mrsuh/php-generics/tests
[8] тот: https://github.com/PHPGenerics/php-generics-rfc/issues/45
[9] nikic/php-parser: https://github.com/nikic/PHP-Parser
[10] тут: https://github.com/mrsuh/PHP-Parser/pull/1/files#diff-14ec37995c001c0c9808ab73668d64db5d1acc1ab0f60a360dcb9c611ecd57ea
[11] PHP реализация: https://github.com/ircmaxell/PHP-Yacc
[12] YACC: https://ru.wikipedia.org/wiki/Yacc
[13] тут: https://github.com/PHPGenerics/php-generics-rfc/issues/35#issuecomment-571546650
[14] своим соображениям: https://github.com/sebastianbergmann/phpunit/issues/4039
[15] плагина: https://plugins.jetbrains.com/plugin/10209-lsp-support
[16] LSP: https://en.wikipedia.org/wiki/Language_Server_Protocol
[17] Hack: https://hacklang.org
[18] отказались: https://blog.jetbrains.com/phpstorm/2015/06/hack-language-support-in-phpstorm-postponed/
[19] плагина для Hack: https://marketplace.visualstudio.com/items?itemName=pranayagarwal.vscode-hack
[20] runtime: https://github.com/PHPGenerics/php-generics-rfc/issues/43
[21] должны быть доступны: https://github.com/PHPGenerics/php-generics-rfc/blob/cc7219792a5b35226129d09536789afe20eac029/generics.txt#L426-L430
[22] Psalm Template Annotations: https://psalm.dev/docs/annotating_code/templated_annotations/
[23] Psalm: https://psalm.dev
[24] spatie/typed: https://github.com/spatie/typed
[25] TimeToogo/PHP-Generics: https://github.com/TimeToogo/PHP-Generics
[26] ircmaxell/PhpGenerics: https://github.com/ircmaxell/PhpGenerics
[27] тут: https://github.com/mrsuh/php-generics/issues
[28] Источник: https://habr.com/ru/post/577750/?utm_source=habrahabr&utm_medium=rss&utm_campaign=577750
Нажмите здесь для печати.