如何在 Symfony Doctrine 中使用实际的 ENUM 类型?

发布于 2025-01-11 01:04:07 字数 305 浏览 0 评论 0原文

ENUM 类型很棒。它们允许严格的值限制并使代码重构变得容易。不幸的是,PHP 不仅在 8.1 版本之前缺乏这些,而且 Doctrine DBAL 也缺乏,并且没有提供开箱即用的易于使用的解决方案。我正在寻找一个解决方案,允许我:

  • DB中的本机ENUM类型
  • PHP中没有魔术字符串
  • 尽可能少的代码重复
  • PHP 7.4+(不能使用PHP 8.1)

对于那些寻找此类解决方案的人来说,这个问题是自我回答的,因为经过几个小时的奋斗,我对自己所做的事情感到非常自豪。请看下文,希望对您有所帮助:

ENUM types are awesome. They allow strict value restrictions and make code refactoring easy. Unfortunately, PHP not only lacks these until version 8.1, the Doctrine DBAL also lacks behind and does not offer a easy to use solution out of the box. I was looking for a solution that would allow me:

  • native ENUM type in DB
  • no magic strings in PHP
  • as little code repetition as possible
  • PHP 7.4+ (cannot use PHP 8.1)

This question is to be self-answered for those looking for such solution, because after hours of struggle, I am quite proud of what I made. See below, hope it helps:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

剑心龙吟 2025-01-18 01:04:07

首先创建一个扩展Doctrine\DBAL\Types\Type的抽象基类。这允许它用作实体列声明中的类型。

<?php

namespace App\DBAL;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Exception;
use InvalidArgumentException;
use ReflectionClass;

abstract class EnumType extends Type
{
    
    private static ?array $constCacheArray = NULL;
    
    public static function getConstants() : array
    {
        if (self::$constCacheArray == NULL)
            self::$constCacheArray = [];
        
        $calledClass = get_called_class();
        
        if (!array_key_exists($calledClass, self::$constCacheArray)) {
            $reflect = new ReflectionClass($calledClass);
            self::$constCacheArray[$calledClass] = $reflect->getConstants();
        }
        
        return self::$constCacheArray[$calledClass];
    }
    
    public static function isValidName($name, $strict = false) : bool
    {
        $constants = self::getConstants();
        
        if ($strict) {
            return array_key_exists($name, $constants);
        }
        
        $keys = array_map('strtolower', array_keys($constants));
        return in_array(strtolower($name), $keys);
    }
    
    public static function isValidValue($value, $strict = true) : bool
    {
        $values = array_values(self::getConstants());
        return in_array($value, $values, $strict);
    }
    
    protected static string $name;
    
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        $values = array_map(function ($val) {
            return "'" . $val . "'";
        }, self::getConstants());
        
        return "ENUM(" . implode(", ", $values) . ")";
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value;
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        $this->checkValue($value);
        
        return $value;
    }
    
    /**
     * @param $value
     * @throws InvalidArgumentException
     */
    public function checkValue($value): void
    {
        if (!self::isValidValue($value)) {
            throw new InvalidArgumentException("Invalid '" . static::$name . "' value.");
        }
    }
    
    public function getName(): string
    {
        return static::$name;
    }
    
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
    
    public static function getValuesArray(): array
    {
        return self::getConstants();
    }
    
    /**
     * @throws Exception
     */
    public static function getChoicesArray(): array
    {
        throw new Exception("Not implemented");
    }
}

其基础归功于 @Brian Cline

重要的是此类通过 Reflection 提供了一些辅助函数,但它还有继承的功能,允许它用作实际的数据库类型。我将通过下面的例子向您展示用法。

这就是定义新 ENUM 类型的方式:

<?php

namespace App\DBAL;

class AdminRoleType extends EnumType
{
    public const ADMIN = 'ROLE_ADMIN';
    public const SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
    public const CSR = 'ROLE_CSR';
    public const MANAGER = 'ROLE_MANAGER';
    public const ACCOUNTING = 'ROLE_ACCOUNTING';
    
    protected static string $name = 'admin_role';
}

非常简单,对吧?这个开箱即用的功能允许您在 PHP 中实现一些很酷的功能,例如:

$myRole = AdminRoleType::CSR; // 'ROLE_CSR'
$isValidRole = AdminRoleType::isValidValue('ROLE_ADMIN'); // true
$isValidRole = AdminRoleType::isValidName('ADMIN'); // true

但我们仍然没有在数据库表中实现实际的 ENUM 类型。为此,首先将以下内容添加到您的 config/packages/doctrine.yaml 中:

doctrine:
    dbal:
        mapping_types:
            enum: string
        types:
            admin_role: App\DBAL\AdminRoleType

这会将 DB ENUM 类型映射到本地 string 类型(抱歉) ,本机 ENUM 不在此解决方案中,但对于 PHP 8.1 可能(?)。)

最后一步是您的 Entity 类:

    /**
     * @ORM\Column(name="admin_role", type="admin_role")
     */
    private string $admin_role = AdminRoleType::CSR;


    public function getAdminRole(): string
    {
        return $this->admin_role;
    }
    
    /**
     * @param string $admin_role
     * @return $this
     * @throws InvalidArgumentException
     */
    public function setAdminRole(string $admin_role): self
    {
        if(!AdminRoleType::isValidValue($admin_role))
            throw new InvalidArgumentException('Invalid Admin Role');
        
        $this->admin_role = $admin_role;
    
        return $this;
    }

如您所见,如果您尝试执行以下操作,代码将引发异常设置一些不允许的字符串你的枚举。

当您进行迁移时,输出应该如下所示:

ALTER TABLE admin CHANGE admin_role admin_role ENUM('ROLE_ADMIN', 'ROLE_SUPER_ADMIN', 'ROLE_CSR', 'ROLE_MANAGER', 'ROLE_ACCOUNTING') NOT NULL COMMENT '(DC2Type:admin_role)'

就是这样。当您使用 PHP 时,请记住使用 AdminRoleType:: 类而不是魔术字符串。如果您需要在枚举中添加/删除项目,只需从枚举类中添加/删除 public const 即可。

Start by creating an abstract base class which extends Doctrine\DBAL\Types\Type. This allows it to be used as a type in Entity column declarations.

<?php

namespace App\DBAL;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Exception;
use InvalidArgumentException;
use ReflectionClass;

abstract class EnumType extends Type
{
    
    private static ?array $constCacheArray = NULL;
    
    public static function getConstants() : array
    {
        if (self::$constCacheArray == NULL)
            self::$constCacheArray = [];
        
        $calledClass = get_called_class();
        
        if (!array_key_exists($calledClass, self::$constCacheArray)) {
            $reflect = new ReflectionClass($calledClass);
            self::$constCacheArray[$calledClass] = $reflect->getConstants();
        }
        
        return self::$constCacheArray[$calledClass];
    }
    
    public static function isValidName($name, $strict = false) : bool
    {
        $constants = self::getConstants();
        
        if ($strict) {
            return array_key_exists($name, $constants);
        }
        
        $keys = array_map('strtolower', array_keys($constants));
        return in_array(strtolower($name), $keys);
    }
    
    public static function isValidValue($value, $strict = true) : bool
    {
        $values = array_values(self::getConstants());
        return in_array($value, $values, $strict);
    }
    
    protected static string $name;
    
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        $values = array_map(function ($val) {
            return "'" . $val . "'";
        }, self::getConstants());
        
        return "ENUM(" . implode(", ", $values) . ")";
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value;
    }
    
    /**
     * @param $value
     * @param AbstractPlatform $platform
     * @return mixed
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        $this->checkValue($value);
        
        return $value;
    }
    
    /**
     * @param $value
     * @throws InvalidArgumentException
     */
    public function checkValue($value): void
    {
        if (!self::isValidValue($value)) {
            throw new InvalidArgumentException("Invalid '" . static::$name . "' value.");
        }
    }
    
    public function getName(): string
    {
        return static::$name;
    }
    
    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
    
    public static function getValuesArray(): array
    {
        return self::getConstants();
    }
    
    /**
     * @throws Exception
     */
    public static function getChoicesArray(): array
    {
        throw new Exception("Not implemented");
    }
}

Credit for the base of this goes to @Brian Cline

Whats important is that this class provides some helper functions with Reflection, but it also has inherited functions that allow it to be used as actual DB type. I will show you the usage with an example below.

This is how you define a new ENUM type:

<?php

namespace App\DBAL;

class AdminRoleType extends EnumType
{
    public const ADMIN = 'ROLE_ADMIN';
    public const SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
    public const CSR = 'ROLE_CSR';
    public const MANAGER = 'ROLE_MANAGER';
    public const ACCOUNTING = 'ROLE_ACCOUNTING';
    
    protected static string $name = 'admin_role';
}

Pretty simple, right? This out of the box allows you to some cool things in PHP such as:

$myRole = AdminRoleType::CSR; // 'ROLE_CSR'
$isValidRole = AdminRoleType::isValidValue('ROLE_ADMIN'); // true
$isValidRole = AdminRoleType::isValidName('ADMIN'); // true

But still we did not achieve actual ENUM type in our DB table. To do this, first add the following to your config/packages/doctrine.yaml:

doctrine:
    dbal:
        mapping_types:
            enum: string
        types:
            admin_role: App\DBAL\AdminRoleType

This maps DB ENUM type to local string type (sorry, native ENUMs are not in this solution, but for PHP 8.1 could(?) be possible.)

The last step is your Entity class:

    /**
     * @ORM\Column(name="admin_role", type="admin_role")
     */
    private string $admin_role = AdminRoleType::CSR;


    public function getAdminRole(): string
    {
        return $this->admin_role;
    }
    
    /**
     * @param string $admin_role
     * @return $this
     * @throws InvalidArgumentException
     */
    public function setAdminRole(string $admin_role): self
    {
        if(!AdminRoleType::isValidValue($admin_role))
            throw new InvalidArgumentException('Invalid Admin Role');
        
        $this->admin_role = $admin_role;
    
        return $this;
    }

As you can see the code will throw an exception if you try to set some string that is not allowed value for your ENUM.

And when you do migration, the output should look like:

ALTER TABLE admin CHANGE admin_role admin_role ENUM('ROLE_ADMIN', 'ROLE_SUPER_ADMIN', 'ROLE_CSR', 'ROLE_MANAGER', 'ROLE_ACCOUNTING') NOT NULL COMMENT '(DC2Type:admin_role)'

That's it. When you work in PHP, remember to use AdminRoleType:: class instead of magic strings. If you need to add/remove item in enum, just add/remove public const from the enum class.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文