定制 mrsuh/php-generics 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

mrsuh/php-generics

Composer 安装命令:

composer require mrsuh/php-generics

包简介

PHP Generics library

README 文档

README

Table of contents

How it works

In a nutshell:

  • parse generics classes;
  • generate concrete classes based on them (you can choose monomorphization or type-erasure);
  • autoload concrete classes instead of generics classes.

For example, you need to add several PHP files:

  • generic class Box;
  • class Usage for use generic class;
  • script with composer autoload and Usage class.

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 App\Usage;

$usage = new Usage();
$usage->run();

Generate concrete classes from generic classes with composer dump-generics command

composer dump-generics -vv

What the composer dump-generics command does?

  • finds all generic uses in classes (src/Usage.php for example).
  • generates concrete classes from generic classes with unique names based on name and arguments of generic class.
  • replaces generic class names to concrete class names in places of use.

In this case should be generated:

  • 2 concrete classes of generics BoxForInt and BoxForString;
  • 1 concrete class Usage with replaced generics class names to concrete class names.

Generate vendor/autoload.php with composer dump-autoload command

composer dump-autoload

Run bin/test.php script

php bin/test.php

Composer autoload first checks the "cache" directory and then the "src" directory to load the classes.

📘 You can find repository with this example here.

Installation

Require

  • PHP >= 7.4
  • Composer (PSR-4 Autoload)

Install library

composer require mrsuh/php-generics

Add directory("cache/") to composer autoload PSR-4 for generated classes. It should be placed before the main directory.

composer.json

{
    "autoload": { 
        "psr-4": {
            "App\\": ["cache/","src/"]
        }
    }
}

Monomorphization

A new class is generated for each generic argument combination.

Before monomorphization:

<?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;
    }
}

After monomorphization:

<?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;
    }
}

Command

composer dump-generics

Where in class can generics be used?

<?php

namespace App;

use App\Entity\Cat;
use App\Entity\Bird;
use App\Entity\Dog;

class Test extends GenericClass<Cat> implements GenericInterface<Bird> { // <-- extends/implements
 
  use GenericTrait<Dog>; // <-- trait use
 
  private GenericClass<int>|GenericClass<Dog> $var; // <-- property type
 
  public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> { // <-- method argument/return type
      
       var_dump($var instanceof GenericInterface<int>); // <-- instanceof
      
       var_dump(GenericClass<int>::class); // <-- class constants      
       var_dump(GenericClass<array>::CONSTANT); // <-- class constants
      
       return new GenericClass<float>(); // <-- new
  }
}

Where in generic class can parameters be used?

<?php

namespace App;

class Test<T,V> extends GenericClass<T> implements GenericInterface<V> { // <-- extends/implements
 
  use GenericTrait<T>; // <-- trait use
  use T; // <-- trait use
 
  private T|GenericClass<V> $var; // <-- property type
 
  public function test(T|GenericInterface<V> $var): T|GenericClass<V> { // <-- method argument/return type
      
       var_dump($var instanceof GenericInterface<V>); // <-- instanceof      
       var_dump($var instanceof T); // <-- instanceof
      
       var_dump(GenericClass<T>::class); // <-- class constants   
       var_dump(T::class); // <-- class constants
       var_dump(GenericClass<T>::CONSTANT); // <-- class constants
       var_dump(T::CONSTANT); // <-- class constants
      
       $obj1 = new T(); // <-- new
       $obj2 = new GenericClass<V>(); // <-- new
      
       return $obj2;
  }
}

📘 You can read more about monomorphization here.

Type erasure

A new class is generated without generics arguments.

Before type erasure:

<?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;
    }
}

After type erasure:

<?php

namespace App;

class Box {

    private $data = null;
    
    public function set($data) : void
    {
        $this->data = $data;
    }
    
    public function get()
    {
        return $this->data;
    }
}

Command

composer dump-generics --type=type-erasure

Where in class can generics be used?

<?php

namespace App;

use App\Entity\Cat;
use App\Entity\Bird;
use App\Entity\Dog;

class Test extends GenericClass<Cat> implements GenericInterface<Bird> { // <-- extends/implements
 
  use GenericTrait<Dog>; // <-- trait use
 
  private GenericClass<int>|GenericClass<Dog> $var; // <-- property type
 
  public function test(GenericInterface<int>|GenericInterface<Dog> $var): GenericClass<string>|GenericClass<Bird> { // <-- method argument/return type
      
       var_dump($var instanceof GenericInterface<int>); // <-- instanceof
      
       var_dump(GenericClass<int>::class); // <-- class constants      
       var_dump(GenericClass<array>::CONSTANT); // <-- class constants
      
       return new GenericClass<float>(); // <-- new
  }
}

Where in generic class can parameters be used?

<?php

namespace App;

class Test<T,V> extends GenericClass<T> implements GenericInterface<V> { // <-- extends/implements
 
  use GenericTrait<T>; // <-- trait use
 
  private GenericClass<V> $var; // <-- property type
 
  public function test(T|GenericInterface<V> $var): T|GenericClass<V> { // <-- method argument/return type
      
       var_dump($var instanceof GenericInterface<V>); // <-- instanceof          
      
       var_dump(GenericClass<T>::class); // <-- class constants           
       var_dump(GenericClass<T>::CONSTANT); // <-- class constants      
      
       return new GenericClass<V>(); // <-- new           
  }
}

📘 You can read more about type-erasure here.

Features

What syntax is used?

The RFC does not define a specific syntax so i took this one implemented by Nikita Popov

Syntax example:

<?php

namespace App;

class Generic<in T: Iface = int, out V: Iface = string> {
  
   public function test(T $var): V {
  
   }
}

Syntax problems

I had to upgrade nikic/php-parser for parse code with new syntax.
You can see here the grammar changes that had to be made for support generics.

Parser use PHP implementation of YACC.
The YACC(LALR) algorithm and current PHP syntax make it impossible to describe the full syntax of generics due to collisions.

Collision example:

<?php

const FOO = 'FOO';
const BAR = 'BAR';

var_dump(new \DateTime<FOO,BAR>('now')); // is it generic?
var_dump( (new \DateTime < FOO) , ( BAR > 'now') ); // no, it is not

Solution options

Therefore, nested generics are not currently supported.

<?php

namespace App;

class Usage {
   public function run() {
       $map = new Map<Key<int>, Value<string>>();//not supported
   }
}

Parameter names have not special restrictions

<?php

namespace App;

class GenericClass<T, varType, myCoolLongParaterName> {
   private T $var1;
   private varType $var2;
   private myCoolLongParaterName $var3;   
}

Several generic parameters support

<?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;
   }
}

Default generic parameter support

<?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<>();//be sure to add "<>"
       $map->set('key', 1);
       var_dump($map->get('key'));
   }
}

How fast is it?

All concrete classes are pre-generated and can be cached(should not affect performance).

Generating many concrete classes should negatively impact performance when:

  • resolves concrete classes;
  • storing concrete classes in memory;
  • type checking for each concrete class.

I think it's all individual for a specific case.

Doesn't work without composer autoload

Autoload magic of concrete classes works with composer autoload only.
Nothing will work because of syntax error if you include file by "require"

Reflection

PHP does type checks in runtime.
Therefore, all generics arguments must me available through reflection in runtime.
It can't be, because information about generics arguments is erased after concrete classes are generated.

Tests

How to run tests?

composer test

How to add test?

  • Add directory 00-your-dir-name to ./tests/{monomorphic/type-erased}
  • Generate output files and check it
php bin/generate.php monomorphic tests/monomorphic/000-your-dir-name
php bin/generate.php type-erased tests/type-erased/000-your-dir-name

mrsuh/php-generics 适用场景与选型建议

mrsuh/php-generics 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 968 次下载、GitHub Stars 达 220, 最近一次更新时间为 2021 年 09 月 13 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「generics」 「type erasure」 「monomorphization」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 mrsuh/php-generics 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 mrsuh/php-generics 我们能提供哪些服务?
定制开发 / 二次开发

基于 mrsuh/php-generics 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 968
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 220
  • 点击次数: 5
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 220
  • Watchers: 6
  • Forks: 7
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2021-09-13