elao/enum

扩展PHP枚举功能及框架集成

资助包维护!
ogizanagi

安装次数: 2,559,342

依赖项: 12

建议者: 0

安全: 0

星标: 324

关注者: 18

分支: 27

开放问题: 17

v2.5.0 2024-06-07 13:59 UTC

README

Latest Version Total Downloads Monthly Downloads Tests Coveralls Scrutinizer Code Quality php

为PHP 8.1+原生枚举以及特定框架和库的集成提供额外、有意见的功能。

#[ReadableEnum(prefix: 'suit.')]
enum Suit: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    case Hearts = '♥︎';
    case Diamonds = '♦︎';
    case Clubs = '♣︎';
    case Spades = '︎♠︎';
}

📢 此项目在PHP 8.1之前用于模拟枚举。
有关1.x文档,点击此处

您还可以参考此问题来跟踪此库V2版本的目标和进度。

安装

composer require "elao/enum:^2.0"

或者,为了帮助和测试最新的更改

composer require "elao/enum:^2.x-dev"

可读枚举

可读枚举提供了一种方法,通过为枚举添加新的ReadableEnumInterface接口来公开枚举案例的人类可读标签。

实现此接口的最简单方法是通过使用ReadableEnumTraitEnumCase属性

namespace App\Enum;

use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ReadableEnumTrait;
use Elao\Enum\Attribute\EnumCase;

enum Suit: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    #[EnumCase('suit.hearts')]
    case Hearts = '♥︎';

    #[EnumCase('suit.diamonds')]
    case Diamonds = '♦︎';

    #[EnumCase('suit.clubs')]
    case Clubs = '♣︎';

    #[EnumCase('suit.spades')]
    case Spades = '︎♠︎';
}

以下代码片段显示了如何获取枚举的人类可读值

Suit::Hearts->getReadable(); // returns 'suit.hearts'

它定义了一个适当的合同来公开枚举案例标签,而不是使用枚举案例的内部名称。这对于如果您要公开标签的本地化与您编写的代码的本地化不同非常有用,以及用于与需要公开此类标签的库集成。

它还特别适用于与Symfony的翻译组件一起使用,通过使用翻译键。

假设以下翻译文件

# translations/messages.fr.yaml
suit.hearts: 'Coeurs'
suit.diamonds: 'Carreaux'
suit.clubs: 'Piques'
suit.spades: 'Trèfles'
$enum = Suit::Hearts;
$translator->trans($enum->getReadable(), locale: 'fr'); // returns 'Coeurs'

配置后缀/前缀和默认值

作为一个快捷方式,您还可以使用ReadableEnum属性来定义要使用的公共suffixprefix,以及如果未显式提供,则默认使用枚举案例名称或值。

#[ReadableEnum(prefix: 'suit.')]
enum Suit: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    #[EnumCase('hearts︎')]
    case Hearts = '♥︎';
    case Diamonds = '♦︎';
    case Clubs = '♣︎';
    case Spades = '︎♠︎';
}

Suit::Hearts->getReadable(); // returns 'suit.hearts'
Suit::Clubs->getReadable(); // returns 'suit.Clubs'

使用案例值(仅适用于基于字符串的枚举)

#[ReadableEnum(prefix: 'suit.', useValueAsDefault: true)]
enum Suit: string implements ReadableEnumInterface
{
    use ReadableEnumTrait;

    case Hearts = 'hearts';
    case Diamonds = 'diamonds';
    case Clubs = 'clubs︎';
    case Spades = '︎spades';
}

Suit::Hearts->getReadable(); // returns 'suit.hearts'
Suit::Clubs->getReadable(); // returns 'suit.clubs'

额外值

EnumCase属性还为您提供了配置案例上的一些额外属性以及通过ExtrasTrait轻松访问这些属性的方式

namespace App\Enum;

use Elao\Enum\ReadableEnumInterface;
use Elao\Enum\ExtrasTrait;
use Elao\Enum\Attribute\EnumCase;

enum Suit implements ReadableEnumInterface
{
    use ExtrasTrait;

    #[EnumCase(extras: ['icon' => 'fa-heart', 'color' => 'red'])]
    case Hearts;

    #[EnumCase(extras: ['icon' => 'fa-diamond', 'color' => 'red'])]
    case Diamonds;

    #[EnumCase(extras: ['icon' => 'fa-club', 'color' => 'black'])]
    case Clubs;

    #[EnumCase(extras: ['icon' => 'fa-spade', 'color' => 'black'])]
    case Spades;
}

使用ExtrasTrait::getExtra(string $key, bool $throwOnMissingExtra = false): mixed访问这些信息

Suit::Hearts->getExtra('color'); // 'red'
Suit::Spades->getExtra('icon'); // 'fa-spade'
Suit::Spades->getExtra('missing-key'); // null
Suit::Spades->getExtra('missing-key', true); // throws

或创建自己的接口/特质

interface RenderableEnumInterface 
{
    public function getColor(): string;
    public function getIcon(): string;
}

use Elao\Enum\ExtrasTrait;

trait RenderableEnumTrait
{
    use ExtrasTrait;

    public function getColor(): string
    {
        $this->getExtra('color', true);
    }
    
    public function getIcon(): string
    {
        $this->getExtra('icon', true);
    }
}

use Elao\Enum\Attribute\EnumCase;

enum Suit implements RenderableEnumInterface
{
    use RenderableEnumTrait;

    #[EnumCase(extras: ['icon' => 'fa-heart', 'color' => 'red'])]
    case Hearts;
    
    // […]
}

Suit::Hearts->getColor(); // 'red'

标志枚举

标志枚举用于位操作。

namespace App\Enum;

enum Permissions: int
{
    case Execute = 1 << 0;
    case Write = 1 << 1;
    case Read = 1 << 2;
}

每个枚举案例都是一个位标志,可以与其他案例组合成一个位掩码,并使用FlagBag对象来处理

use App\Enum\Permissions;
use Elao\Enum\FlagBag;

$permissions = FlagBag::from(Permissions::Execute, Permissions::Write, Permissions::Read);
// same as:
$permissions = new FlagBag(Permissions::class, 7); 
// where 7 is the "encoded" bits value for:
Permissions::Execute->value | Permissions::Write->value | Permissions::Read->value // 7
// or initiate a bag with all its possible values using:
$permissions = FlagBag::fromAll(Permissions::class);

$permissions = $permissions->withoutFlags(Permissions::Execute); // Returns an instance without "execute" flag

$permissions->getValue(); // Returns 6, i.e: the encoded bits value
$permissions->getBits(); // Returns [2, 4], i.e: the decoded bits
$permissions->getFlags(); // Returns [Permissions::Write, Permissions::Read]

$permissions = $permissions->withoutFlags(Permissions::Read, Permissions::Write); // Returns an instance without "read" and "write" flags
$permissions->getBits(); // Returns []
$permissions->getFlags(); // Returns []

$permissions = new FlagBag(Permissions::class, FlagBag::NONE); // Returns an empty bag

$permissions = $permissions->withFlags(Permissions::Read, Permissions::Execute); // Returns an instance with "read" and "execute" flags

$permissions->hasFlags(Permissions::Read); // True
$permissions->hasFlags(Permissions::Read, Permissions::Execute); // True
$permissions->hasFlags(Permissions::Write); // False

因此,使用FlagBag::getValue()您可以为枚举中任何组合的标志获取一个编码值,并将其用于存储或您的过程之间的通信。

集成

Symfony表单

Symfony 已经提供了一个 EnumType,允许用户从 PHP 枚举中定义的选项中选择一个或多个。
它扩展了 ChoiceType 字段并定义了相同的选项。

然而,它使用枚举案例名称作为标签,这可能不方便。
由于这个库专门支持可读的枚举,它提供了一个自己的 EnumType,扩展了 Symfony 的一个,并使用每个案例的人类表示形式而不是它们的名称。

请使用它来代替 Symfony 的一个

namespace App\Form\Type;

use App\Enum\Suit;
use Symfony\Component\Form\AbstractType;
use Elao\Enum\Bridge\Symfony\Form\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class CardType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('suit', EnumType::class, [
                'class' => Suit::class, 
                'expanded' => true,
            ])
        ;
    }

    // ...
}

FlagBag 表单类型

如果您想在 Symfony 表单中使用 FlagBag,请使用 FlagBagType。此类型也扩展了 Symfony EnumType,但它将表单值转换为 FlagBag 实例,反之亦然。

namespace App\Form\Type;

use App\Enum\Permissions;
use Symfony\Component\Form\AbstractType;
use Elao\Enum\Bridge\Symfony\Form\Type\FlagBagType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;

class AuthenticationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('permission', FlagBagType::class, [
                'class' => Permissions::class, 
            ])
        ;
    }

    // ...
}

Symfony HttpKernel

从路由路径解析控制器参数

从 Symfony 6.1+ 开始,受支持的枚举案例将从路由路径参数解析

class CardController
{
    #[Route('/cards/{suit}')]
    public function list(Suit $suit): Response
    {
        // [...]
    }
}

➜ 调用 /cards/H 将解析 $suit 参数为 Suit::Hearts 枚举案例。

如果您尚未使用 Symfony HttpKernel 6.1+,此库将通过注册自己的解析器使此功能仍然可用。

从查询或体中解析控制器参数

您也可以从查询参数或请求体中解析

use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\Attributes\BackedEnumFromQuery;

class DefaultController
{
    #[Route('/cards')]
    public function list(
        #[BackedEnumFromQuery]
        ?Suit $suit = null,
    ): Response
    {
        // [...]
    }
}

➜ 调用 /cards?suit=H 将解析 $suit 参数为 Suit::Hearts 枚举案例。

使用 BackedEnumFromBody 从请求体($_POST)中解析。

它还支持可变参数

use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\Attributes\BackedEnumFromQuery;

class DefaultController
{
    #[Route('/cards')]
    public function list(
        #[BackedEnumFromQuery]
        ?Suit ...$suits = null,
    ): Response
    {
        // [...]
    }
}

➜ 调用 /cards?suits[]=H&suits[]=S 将解析 $suits 参数为 [Suit::Hearts, Suit::Spades]

Symfony翻译

因为 ReadableEnumInterface 可以在 TranslatorInterface 中翻译,所以很容易使用 TranslatableInterface 来枚举。

要翻译可读枚举,只需调用即可

public function trans(TranslatorInterface $translator, string $locale = null): string
{
    return $translator->trans($this->getReadable(), [], $locale);
}

为此目的已添加了一个接口和一个特性。

use Elao\Enum\Bridge\Symfony\Translation\TranslatableEnumInterface;
use Elao\Enum\Bridge\Symfony\Translation\TranslatableEnumTrait;

enum Card: string implements TranslatableEnumInterface
{
    use TranslatableEnumTrait;
    
    #[EnumCase('suit.hearts')]
    case Hearts = '♥︎';    
    // ...
}

然后在 PHP 中使用它

$translated = Card::Hearts->trans($this->translator)

或者在 Twig 中使用它

{{ game.card|trans }}

Doctrine

doctrine/orm 2.11 开始,PHP 8.1 枚举类型 已原生支持

#[Entity]
class Card
{
    #[Column(type: 'string', enumType: Suit::class)]
    public $suit;
}

注意:除非您有特定的需求,如以下所述的 DBAL 类型,我们建议使用官方 ORM 集成来处理受支持的枚举。

但是,PhpEnums 也提供了一些基础类来将您的 PHP 后端枚举保存到数据库中。对于特定于此库的使用案例,如存储一个 flag bag 或受支持的枚举案例集合的 DBAL 类,也或将可用。

在 Symfony 应用中

此配置等同于以下部分,解释了如何创建自定义 Doctrine DBAL 类型

elao_enum:
  doctrine:
    types:
      App\Enum\Suit: ~ # Defaults to `{ class: App\Enum\Suit, default: null, type: single }`
      permissions: { class: App\Enum\Permission } # You can set a name different from the enum FQCN
      permissions_bag: { class: App\Enum\Permissions, type: flagbag } # values are stored as an int and retrieved as FlagBag object
      App\Enum\RequestStatus: { default: 200 } # Default value from enum cases, in case the db value is NULL

它实际上会为您生成并注册类型类,从而节省您编写样板代码。

手动

首先阅读 Doctrine DBAL 文档

扩展 AbstractEnumType

namespace App\Doctrine\DBAL\Type;

use Elao\Enum\Bridge\Doctrine\DBAL\Types\AbstractEnumType;
use App\Enum\Suit;

class SuitType extends AbstractEnumType
{
    protected function getEnumClass(): string
    {
        return Suit::class; // By default, the enum FQCN is used as the DBAL type name as well
    }
}

在您的应用程序引导代码中

use App\Doctrine\DBAL\Type\SuitType;
use Doctrine\DBAL\Types\Type;

Type::addType(Suit::class, SuitType::class);

为了在执行模式操作时将新 "Suit" 类型的底层数据库类型直接转换为 Suit 实例,该类型还必须在数据库平台中注册

$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping(Suit::class, SuitType::class);

然后,将其用作列类型

use App\Enum\Suit;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Card
{
    #[ORM\Column(Suit::class, nullable: false)]
    private Suit $field;
}

故障排除

在 QueryBuilder 中使用枚举实例

当使用枚举实例作为使用Doctrine\ORM\QueryBuilder制作的查询参数,并且使用从扩展生成的DBAL类型时,可能无法正确推断参数类型。

可以显式使用枚举值而不是实例,或者在setParameter()中将注册的DBAL类型作为第三个参数传递,以允许查询构建器正确地将对象转换为数据库值。

例如:

#[ORM\Entity]
class Card
{
    #[ORM\Column(Suit::class, nullable: true)
    protected ?Suit $suit = null;
}

可以使用以下方法之一:

private function findByType(?Suit $suit = null): array 
{
    $qb = $em->createQueryBuilder()
      ->select('c')
      ->from('Card', 'c')
      ->where('c.suit = :suit');
      
    // use a value from constants:
    $qb->setParameter('param1', Suit::SPADES->value);
    
    // or from instances:
    $qb->setParameter('suit', $suit->value);  
    // Use the 3rd parameter to set the DBAL type
    $qb->setParameter('suit', $suit, Suit::class);
    
    // […]
}   

Doctrine ODM

您可以将枚举值以字符串或整数的格式存储在MongoDB数据库中,并利用该库中包含的自定义映射类型将它们作为对象进行操作。

在不久的将来,将为该库特定的用例提供自定义ODM类,例如存储标志包或支持枚举案例的集合。

在 Symfony 应用中

此配置与以下部分等效,解释了如何创建自定义Doctrine ODM类型

elao_enum:
  doctrine_mongodb:
    types:
      App\Enum\Suit: ~ # Defaults to `{ class: App\Enum\Suit, type: single }`
      permissions: { class: App\Enum\Permission } # You can set a name different from the enum FQCN
      another: { class: App\Enum\AnotherEnum, type: collection } # values are stored as an array of integers or strings
      App\Enum\RequestStatus: { default: 200 } # Default value from enum cases, in case the db value is NULL

它实际上会为您生成并注册类型类,从而节省您编写样板代码。

手动

首先阅读Doctrine ODM文档

扩展AbstractEnumTypeAbstractCollectionEnumType

namespace App\Doctrine\ODM\Type;

use Elao\Enum\Bridge\Doctrine\ODM\Types\AbstractEnumType;
use App\Enum\Suit;

class SuitType extends AbstractEnumType
{
    protected function getEnumClass(): string
    {
        return Suit::class; // By default, the enum FQCN is used as the DBAL type name as well
    }
}

在您的应用程序引导代码中

use App\Doctrine\ODM\Type\SuitType;
use Doctrine\ODM\MongoDB\Types\Type;

Type::addType(Suit::class, SuitType::class);

映射

现在可以使用新的类型进行字段映射

use App\Enum\Suit;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

#[MongoDB\Document]
class Card
{
    #[MongoDB\Field(Suit::class)]
    private Suit $field;
}

Faker

PhpEnums库提供了一个EnumProvider faker,允许选择随机的枚举案例

use \Elao\Enum\Bridge\Faker\Provider\EnumProvider;
 
$faker = new Faker\Generator();
$faker->addProvider(new EnumProvider());

$faker->randomEnum(Suit::class) // Select one of the Suit cases, e.g: `Suit::Hearts`
$faker->randomEnums(Suit::class, 2, min: 1) // Select between 1 and 2 enums cases, e.g: `[Suit::Hearts, Suit::Spades]`
$faker->randomEnums(Suit::class, 3, exact: true) // Select exactly 3 enums cases

它的构造函数接收枚举类型别名的映射作为第一个参数

new EnumProvider([
    'Civility' => App\Enum\Civility::class,
    'Suit' => App\Enum\Suit::class,
]);

当与Nelmio Alice的DSL一起使用此提供程序时,这特别有用(见下一节

与Alice一起使用

如果您使用nelmio/alice包及其扩展来生成测试数据,则可以使用nelmio_alice.faker.generator注册Faker提供程序

# config/services.yaml
services:
    Elao\Enum\Bridge\Faker\Provider\EnumProvider:
        arguments:
            - Civility: App\Enum\Civility
              Suit: App\Enum\Suit
        tags: ['nelmio_alice.faker.provider']

以下示例展示了如何在PHP测试数据文件中使用提供程序

return [
    MyEntity::class => [
        'entity1' => [
            'civility' => Civility::MISTER // Select a specific case, using PHP directly
            'suit' => '<randomEnum(App\Enum\Suit)>' // Select a random case
            'suit' => '<randomEnum(Suit)>' // Select a random case, using the FQCN alias
            'permissions' => '<randomEnums(Permissions, 3, false, 1)>' // Select between 1 and 2 enums cases
            'permissions' => '<randomEnums(Permissions, 3, true)>' // Select exactly 3 enums cases
        ]
    ]
]