dborsatto/smart-enums

4.1.0 2023-09-02 13:23 UTC

This package is auto-updated.

Last update: 2024-08-31 00:37:03 UTC


README

Packagist PHP version Packagist

dborsatto/smart-enums是一个PHP库,允许您在没有等待PHP 8.1版本的情况下使用枚举。它提供了与Doctrine的集成,因此您可以在实体中使用枚举对象,并使用Symfony表单。

安装

与任何Composer包一样,运行CLI命令将库要求添加到您的应用程序中

composer require dborsatto/smart-enums

入门

这个库是围绕有一个定义的值集以及每个值都有某种描述的需求构建的。我们常常遇到一个属性值必须限制在给定选项集中的情况(这就是枚举),但这些选项实际上是用户在我们应用程序中看到的更信息化的、描述性的消息的内部表示。

让我们用一个例子来说明:订单的状态可以是已打开、已发货或已交付。在这种情况下,枚举有这三个可能值。问题是,当向用户显示当前状态的消息时,你需要某种从字符串'open'到某种可用于特定上下文的文本的转换。此外,如果你真的这么想,最终'open'只是一个枚举的内部表示,你只关心它是打开的,而不是真的关心使用字符串'open'来定义状态

这个库是围绕这样一个概念构建的:枚举的每个可能值都将有一些文本描述,这可以是一个用于国际化过程的符号,以及用于验证内部状态和执行事务(等等)的方法。

在这个库的核心中,有一个接口和一个实现了该接口的抽象类。你的工作是扩展这个抽象类并实现它所需你创建的唯一方法。让我们用一个例子来说明,使用前面提到的订单状态。

class OrderStatus extends \DBorsatto\SmartEnums\AbstractEnum
{
    // First is a list of all possible values, defined as constants
    private const STATUS_OPEN = 'open';
    private const STATUS_SHIPPED = 'shipped';
    private const STATUS_DELIVERED = 'delivered';

    // In this example, the text representation is implemented in a way
    // that can be easily fed into an internationalization system
    // for easy translation, but if you don't need that you can use actual text
    private const STATUSES = [
        self::STATUS_OPEN => 'order_status.open',
        self::STATUS_SHIPPED => 'order_status.shipped',
        self::STATUS_DELIVERED => 'order_status.delivered',
    ];

    // This is the only method you will *need* to implement
    // You need to return an array with available options as keys,
    // and their text representation as values
    protected static function getValues(): array
    {
        return self::STATUSES;
    }
    
    // Even though you could have public constants and create enums using
    // OrderStatus::fromValue(OrderStatus::STATUS_OPEN), we think that's not the right way.
    // We see the constant as an internal representation of the possible value,
    // but the user does not need to be aware of this.
    // Also, from a purely formal point of value, `::fromValue()` can throw an exception
    // if the given value is not available, but calling the method using the constant
    // you are sure that the status is available, yet you still need to handle the exception.
    // By implementing named constructors, you can keep the visibility to private,
    // and there is no need to handle meaningless exceptions.
    public static function open(): self
    {
        return self::newInstance(self::STATUS_OPEN);
    }
    
    public static function shipped(): self
    {
        return self::newInstance(self::STATUS_SHIPPED);
    }
    
    public static function delivered(): self
    {
        return self::newInstance(self::STATUS_DELIVERED);
    }
    
    public function isDelivered(): bool
    {
        return $this->value === self::STATUS_DELIVERED;
    }
    
    public function canBeShipped(): bool
    {
        return $this->value === self::STATUS_OPEN;
    }
    
    public function canBeDelivered(): bool
    {
        return $this->value === self::STATUS_SHIPPED;
    }
    
    /**
     * @throws OrderStatusException
     */
    public function ship(): self
    {
        if (!$this->canBeShipped()) {
            // We recommend creating your own exceptions
            throw OrderStatusException::orderCannotBeShipped();
        }
        
        return self::shipped();
    }
    
    /**
     * @throws OrderStatusException
     */
    public function deliver(): self
    {
        if (!$this->canBeDelivered()) {
            throw OrderStatusException::orderCannotBeDelivered();
        }
        
        return self::delivered();
    }
}

// Elsewhere
$status = OrderStatus::open();
// Will return order_status.open, as defined in the STATUSES constant
echo $status->getDescription();
// ...
try {
    $shippedStatus = $status->ship();
} catch (OrderStatusException $exception) {
    // ...
}

这相当多的样板代码,尤其是考虑到其他库(如myclabs/php-enum)提供的魔法,你不需要写一半的代码。但我们故意这样做,因为我们不喜欢魔法,更愿意有更长但更明确的东西。这就是为什么这个库被称为智能枚举:它很智能,因为一切都旨在简单,尽可能少做假设。

你可以添加你需要的任何方法。这个库的伟大之处在于,你的枚举将完全自我感知,并包含它们需要的逻辑。在这个例子中,状态只有三个,它们之间的关系很清晰,但我们有具有十几个可能选项和复杂转换的情况。你可以在枚举中编写任何东西,逻辑将被完全封装。

在实体中使用

使用包含逻辑的枚举的主要好处是当它成为你实体的一部分时

class Order
{
    // ...

    /**
     * @var OrderStatus
     */
    private $status;
    
    // ...

    public function __construct()
    {
        $this->status = OrderStatus::open();
    }
    
    // ...
    
    /**
     * @throws OrderStatusException 
     */
    public function ship(): void
    {
        $this->status = $this->status->ship();
    }
    
    /**
     * @throws OrderStatusException 
     */
    public function delivered(DateTimeImmutable $deliveryDate): void
    {
        $this->status = $this->status->deliver();
        $this->deliveryDate = $deliveryDate;
    }
}

在这个例子中,所有关于订单状态的逻辑都将封装在枚举中,实体将能够访问它并相应地执行。

为了使实体中的集成更容易,我们创建了一个与Doctrine的桥梁,它允许你轻松地添加自定义类型,这样你就可以创建枚举对象而不是字符串。

使用Doctrine的枚举需要两个步骤:创建一个自定义类型,并告诉Doctrine关于它。第一步是这个库帮助你完成的。

class OrderStatusType extends \DBorsatto\SmartEnums\Bridge\Doctrine\Type\AbstractEnumType
{
    public const NAME = 'order_status_type';

    protected function getEnumClass(): string
    {
       return OrderStatus::class;
    }

    public function getName(): string
    {
        return self::NAME;
    }
}

通过扩展 AbstractEnumType,所有转换过程将为您处理。如果您在配置中声明该类型为可空的,null 值将得到适当的处理。

第二步是让 Doctrine 知道您的自定义类型。我们使用 Symfony,所以我们向 doctrine.dbal.types 部分添加了正确的配置(有关更多详细信息,请参阅参考配置)。如果您使用纯 Doctrine,必须按照官方文档中解释的方式调用 Doctrine\DBAL\Types\Type::addType()

设置类型后,您可以为实体配置使用它。如果您使用注解,则代码将类似于以下内容

class Order
{
    // ...

    /**
     * @var OrderStatus
     *
     * @ORM\Column(type="enum_order_status")
     */
    private $status;
}

Symfony 表单集成

正如我们之前提到的,我们使用 Symfony。这意味着我们必须找到一种让枚举与表单一起工作的方式,因此我们还包含了一个可直接使用的表单类型

use DBorsatto\SmartEnums\Bridge\Symfony\Form\Type\EnumType;

class OrderType extends \Symfony\Component\Form\AbstractType
{
    public function buildForm(FormBuilderInterface $builder,array $options)
    {
        // This example is not the best because ideally you would transition
        // an order status manually by calling an entity method,
        // but sometimes you just have to let users pick an option 
        $builder->add('orderStatus', EnumType::class, [
            'enum_class' => OrderStatus::class,
            'label' => 'Status',
        ]);
    }
}

这将为您提供一个包含所有可用选项的 ChoiceType 输入。如果您需要限制可能的选项选择,可以将 choices 值传递给配置数组,其中包含用户可以选择的可用对象列表。

Symfony 验证器集成

此库包含一个可用于与 Symfony 验证器一起使用的约束。它需要 enumClass 参数,并可选择一个错误 message

/** @var \Symfony\Component\Validator\Validator\ValidatorInterface $validator */
$violations = $validator->validate($value, [
    new \DBorsatto\SmartEnums\Bridge\Symfony\Validator\EnumConstraint(['enumClass' => Enum::class]),
]);

该约束的工作方式与任何其他 Symfony 约束相同,这意味着您也可以将其用作注解。

实用工具

此库包含一些实用类,您在日常使用中可能不需要它们,但它们仍然可供您使用。

// EnumFactory acts as a wrapper for when you only have the enum class available,
// but you need guarantees about it being a valid enum
$factory = new \DBorsatto\SmartEnums\EnumFactory(OrderStatus::class);
// At this point, all methods just forward to the actual enum methods
$factory->fromValue('...');
$factory->fromValues([...]);
$factory->all();

// Sometimes you just need to get the enum value and description as an key => value array
// Because this is usually a formatting problem, instead of breaking encapsulation
// and making the enum constant public, use this formatter
$formatter = new \DBorsatto\SmartEnums\EnumFormatter(OrderStatus::class);

// These methods both return array<string, string> values
$formatter->toValueDescriptionList();
$formatter->toDescriptionValueList();

关于枚举标识的一个重要说明

由于具有相同值的两个枚举在概念上是相同的,因此我们构建了 AbstractEnum 以确保实例被重复使用。这意味着 OrderStatus::open() === OrderStatus::open() 将评估为 true。

为此,您需要记住两件事

  • 由于技术原因,这不能在接口级别强制执行。这就是我们建议您始终扩展 AbstractEnum 而不是直接实现 EnumInterface 的原因。
  • 在枚举内部,您**绝对**不能修改 $this->value。状态转换必须始终返回一个新的枚举,并且它们绝不能更新当前枚举。遗憾的是,PHP 8.1 之前的版本不支持只读属性,正如我们所说,我们不希望使用任何让我们绕过这一限制的魔术解决方案,所以我们相信用户会聪明地使用它,不会搞砸。

许可协议

此存储库在 MIT 许可协议下发布。