artisansdk/generic

PHP 中的泛型用户实现。

dev-master 2023-07-10 18:41 UTC

This package is auto-updated.

Last update: 2024-09-10 21:03:49 UTC


README

基于 PHP 的泛型(模板类)实现,有助于实现更严格的类型一致性。

目录

安装

该包像任何其他 PHP 包一样安装到 PHP 应用程序中

composer require artisansdk/generic

使用指南

例如,请参阅 tests/Example.php。运行以下命令进行性能测试

php tests/Example.php

使用泛型只需用所需的类型参数实例化它,然后从那时起将用于类型检查

<?php

use ArtisanSDK\Generic\Types\HashMap;

// Generate a generic hash map consisting of integer keys and string values
// @example [0 => 'foo', 1 => 'bar', ... ]
$map = HashMap::generic(HashMap::TYPE_INT, HashMap::TYPE_STRING);
$map->set(0, 'foo');
$map->set('1', 'bar'); // throws InvalidArgumentException because string is first parameter

// Generate a generic hash map consisting of string keys and stdClass values
// @example ['foo' => stdClass, 'bar' => stdClass, ... ]
$map = HashMap::generic(HashMap::TYPE_STRING, 'stdClass');
$map->set('foo', new stdClass());
$map->set('bar', 123); // throws InvalidArgumentException

每个泛型(例如 ArtisanSDK\Generic\Types\Collection)都实现了 ArtisanSDK\Generic\Contract,该合约包含所有预定义的类型常量。导入泛型提供了使用泛型所需的所有依赖。您可以直接在泛型本身上引用它们,而不是引用 Contract::TYPE_* 常量,如 Collection::TYPE_*。有关所有预定义类型常量,请参阅 ArtisanSDK\Generic\Contract

库背后的动机

泛型(或模板类)在具有大量表示特定类型的类的系统中很常见。例如,游戏有很多坏蛋和武器,它们通常具有相似的行为。为了在 PHP 中满足类型检查,这意味着需要创建数百个类,或者创建复杂的继承层次结构或者保持扁平但重复大量逻辑。特质的使用可以帮助,但会引入其他复杂性。泛型是答案。有一个泛型坏蛋或武器类,可以从它定义数百个其他类,并确保它们的行为一致,并且不能将错误的弹药装入错误的武器。大多数语言会使用自定义语法来实现这一点,但在 PHP 中,我们必须求助于用户代码。然而,通过一点反射魔法和一些巧妙的组合,我们可以实现大体相同的结果。

进一步阅读:曾有一个关于 PHP 泛型的 RFC,但因为它缺乏支持而被跳过了。您可以在这里阅读它 →

创建自定义泛型

泛型由一个代理模板类和扩展基本抽象泛型的类型化泛型组成。代理模板类可以根据其行为进行基本类型假设,但其参数是无类型的。这允许代理类将行为逻辑封装在模板的形式中,同时对该类周围包装的代理一无所知。模板类实现了 ArtisanSDK\Generic\Contract,该合约需要实现一个 generic() 静态工厂。应用程序代码永远不应直接依赖于这个模板。

类型化泛型扩展了基本抽象泛型 ArtisanSDK\Generic\Generic,该泛型也实现了 ArtisanSDK\Generic\Contract。这个类的作用是允许应用程序代码在具有模板定义的行为的同时,提供泛型的模板功能。这保持了类型一致性。抽象泛型的父逻辑在启用类型检查(默认)时使用反射,通过比较类型化泛型在 generic() 方法上实现的文档块中定义的模板参数与在调用代理模板类的某个方法时传递的参数进行比较。如果参数的类型要模板化,则类型化泛型必须在使用模板的所有模板方法中使用相同的参数名称。

堆栈示例

以下是一个自定义的 App\Types\Stack 泛型示例,它代理到未类型化的 App\Types\Templates\Stack 类。请注意,类型化泛型和代理模板上的 generic() 方法都有相同的文档块(参数名称很重要),并且代理模板类的所有公共方法在文档块中都使用相同的参数名称,如果类型化参数需要检查的话。自定义方法的类型化参数顺序无关紧要。

这是类型化泛型,这是您的应用程序应该进行类型提示的。

<?php

namespace App\Types;

use ArtisanSDK\Generic\Generic;
use ArtisanSDK\Generic\Contract;

class Stack extends Generic
{
    public function __construct($item)
    {
        parent::__construct(Templates\Stack::class, $item);
    }

    /**
     * @param mixed $item for generic stack type
     */
    public static function generic() : Contract
    {
        $args = func_get_args();

        return new static(...$args);
    }
}

这是代理模板类,它定义了泛型的行为。注意,push() 方法使用文档块类型提示了 $item 参数,因此会进行类型检查,而 all()pop() 接受没有参数,因此不会进行类型检查。此外,slice() 接受参数但不会对它们进行类型提示,因此不会通过代理进行类型检查(尽管它们通过 PHP 内部进行类型检查)。技术上,您可以省略 push() 方法的文档块,因为参数名称与类型化参数匹配:只有 generic() 严格要求文档块。

<?php

namespace App\Types\Templates;

use ArtisanSDK\Generic\Contract;
use App\Types\Stack as Type;

class Stack implements Contract
{
    private $items = [];

    /**
     * @param mixed $item for generic stack type
     */
    public static function generic() : Contract
    {
        $args = func_get_args();

        return new Type(...$args);
    }

    /**
     * @param mixed $item to push on stack
     */
    public function push($item)
    {
        array_push($this->items, $item);

        return $this;
    }

    public function pop()
    {
        return array_pop($this->items);
    }

    public function all() : array
    {
        return $this->items;
    }

    public function slice(int $offset, int $length = null) : array
    {
        return array_slice($this->items, $offset, $length);
    }
}

然后要使用您自定义的泛型,您需要定义堆栈的类型化参数。在这种情况下,我们创建了一个只接受 User 类的堆栈。传递任何其他类将抛出异常。以下是一个示例

<?php

use App\Models\User;
use App\Types\Stack;

$users = Stack::generic(User::class);
$users->push(new User());
$users->push(new stdClass()); // throws InvalidArgumentException

$stack = Stack::generic('stdClass');
$stack->push(new stdClass());
$stack->push(new User()); // throws InvalidArgumentException

运行不带类型检查

设置环境变量 PHP_GENERIC_DISABLE=1 以禁用泛型类型检查。在生产环境中,如果您在 CI/CD 管道中运行检查,那么您可能想这样做以提高速度。

PHP_GENERIC_DISABLE=1 php tests/Example.php

生成 100K 类型对象的性能差异可以忽略不计,平均只有 0.0014ms,而调用代理模板类的 400K 次调用成本在禁用类型检查时为 25756ms,启用时为 26184ms。这实际上是 363ms 的差异,或者每次调用 0.001ms(可以忽略不计)。内存消耗更多,禁用时为 25MB,启用时为 61MB。

许可

版权所有 (c) 2023 Artisan Made

此软件包采用 MIT 许可证发布。有关商业许可条款,请参阅随代码副本一起分发的 LICENSE 文件。