Doctrine自定义PHP8枚举类型:逆向工程缺失枚举值上下文问题
问题背景
我正在开发适配PHP 8枚举的MySQL枚举类型,替代原有基于STRING的枚举映射方式:
原映射写法:
#[ORM\Column(type: Types::STRING, length: 16, enumType: Language::class)]
改为自定义类型映射:
#[ORM\Column(type: 'enum', enumType: Language::class)]
目标是防止插入非法值破坏对象hydration,已实现自定义Doctrine类型EnumType,首次迁移成功生成MySQL枚举,但后续Doctrine无法执行diff操作。添加getMappedDatabaseTypes解决类型识别后,调用getSQLDeclaration时缺少MySQL枚举值上下文,无法重构SQL声明。
当前自定义EnumType代码
<?php namespace App\Doctrine\DBAL\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; class EnumType extends Type { public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { $enumType = $fieldDeclaration['enumType'] ?? null; if (!\is_subclass_of($enumType, \BackedEnum::class)) { throw new \LogicException('Doctrine "enum"类型要求在"enumType"列映射属性中使用原生PHP 8 backed enum类。'); } // TODO 转义包含引号的值 return \sprintf( 'ENUM(%s)', \implode(',', \array_map(fn($e) => \sprintf("'%s'", $e->value), $enumType::cases())) ); } public function getName(): string { return 'enum'; } public function requiresSQLCommentHint(AbstractPlatform $platform): bool { return true; } }
解决方案
1. 在注释中存储PHP枚举类名
修改EnumType的getComment方法,将对应的PHP枚举类名追加到列注释中,让数据库Schema携带枚举类型上下文:
public function getComment(array $fieldDeclaration, AbstractPlatform $platform): string { $enumType = $fieldDeclaration['enumType'] ?? null; $baseComment = parent::getComment($fieldDeclaration, $platform); return $baseComment . ($enumType ? sprintf(' (EnumClass:%s)', $enumType) : ''); }
2. 解析注释填充枚举类型上下文
创建自定义MySQLSchemaManager,解析列注释中的EnumClass信息并注入到字段声明选项中:
<?php namespace App\Doctrine\DBAL\Schema; use Doctrine\DBAL\Schema\MySQLSchemaManager; class CustomMySQLSchemaManager extends MySQLSchemaManager { protected function parseColumnComment(string $comment): array { $options = parent::parseColumnComment($comment); if (preg_match('/\(EnumClass:([a-zA-Z0-9_\\\\]+)\)/', $comment, $matches)) { $options['enumType'] = $matches[1]; } return $options; } }
创建对应的SchemaManager工厂类,让Doctrine使用自定义SchemaManager:
<?php namespace App\Doctrine\DBAL\Schema; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\SchemaManagerFactory; class CustomSchemaManagerFactory implements SchemaManagerFactory { public function createSchemaManager(Connection $connection): AbstractSchemaManager { $platform = $connection->getDatabasePlatform(); return new CustomMySQLSchemaManager($connection, $platform); } }
在Doctrine配置中指定工厂类:
# config/packages/doctrine.yaml doctrine: dbal: schema_manager_factory: App\Doctrine\DBAL\Schema\CustomSchemaManagerFactory
3. 完善类型转换逻辑
添加枚举与数据库值的双向转换方法,确保对象hydration和数据存储的正确性:
public function convertToPHPValue($value, AbstractPlatform $platform) { if ($value === null) { return null; } /** @var class-string<\BackedEnum> $enumType */ $enumType = $this->getFieldDeclaration()['enumType'] ?? throw new \LogicException('Enum type not specified'); return $enumType::from($value); } public function convertToDatabaseValue($value, AbstractPlatform $platform) { if ($value === null) { return null; } if (!$value instanceof \BackedEnum) { throw new \InvalidArgumentException('Value must be a backed enum instance'); } return $value->value; }
4. 修复枚举值转义问题
替换手动引号包裹,使用数据库平台的字符串转义方法避免SQL语法错误:
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { $enumType = $fieldDeclaration['enumType'] ?? null; if (!\is_subclass_of($enumType, \BackedEnum::class)) { throw new \LogicException('Doctrine "enum"类型要求在"enumType"列映射属性中使用原生PHP 8 backed enum类。'); } $values = array_map(fn($e) => $platform->quoteString($e->value), $enumType::cases()); return sprintf('ENUM(%s)', implode(',', $values)); }
5. 配置MySQL类型映射
添加getMappedDatabaseTypes方法,让Doctrine将MySQL原生enum类型映射到自定义EnumType:
public function getMappedDatabaseTypes(AbstractPlatform $platform): array { if ($platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform) { return ['enum']; } return []; }
验证效果
完成上述修改后,重新生成并执行迁移,后续运行doctrine:schema:update --dump-sql时,Doctrine将能正确识别MySQL枚举定义,基于PHP枚举类生成对比SQL,解决diff操作失败的问题。
内容的提问来源于stack exchange,提问作者Alain

