Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SQL Enum support #179

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<server name="KERNEL_CLASS" value="App\Kernel"/>
<env name="MONGODB_URL" value="mongodb://localhost:27017" />
<env name="MONGODB_DB" value="enum-test" />
<env name="DOCTRINE_DBAL_URL" value="sqlite:///%kernel.cache_dir%/db.sqlite" />
<!-- <env name="DOCTRINE_DBAL_URL" value="pdo-mysql://user:pass@localhost:3306/doctrine_tests" /> -->
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[direct]=0&amp;max[self]=0&amp;max[total]=9999&amp;verbose=1"/>
<env name="SYMFONY_PHPUNIT_REQUIRE" value="phpspec/prophecy-phpunit"/>
<env name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
Expand Down
36 changes: 33 additions & 3 deletions src/Bridge/Doctrine/Common/AbstractTypesDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,39 @@ public function dumpToFile(string $file, array $types)
file_put_contents($file, $this->dump($types));
}

public static function getTypeClassname(string $class, string $type): string
/**
* Returns FQCN for given Enum
*
* If name is FQCN already, then the resulting FQCN will be MARKER\Originalnamespaceofenum\EnumclassnameSuffix
* If name is custom string, then the resulting FQCN will be MARKER\Originalnamespaceofenum\CustomnamepascalcaseSuffix
*/
public static function getTypeFullyQualifiedClassName(string $enumClass, string $type, string $name): string
{
return sprintf('%s\\%s%s', static::getMarker(), $class, static::getSuffixes()[$type]);
$fqcn = sprintf('%s\\%s', static::getMarker(), $enumClass);

$classname = basename(str_replace('\\', '/', $fqcn));
$ns = substr($fqcn, 0, -\strlen($classname) - 1);

if (str_contains($name, '\\')) {
$name = $classname;
} else {
$name = static::getPascalCase($name);
}

return sprintf('%s\\%s%s', $ns, $name, static::getSuffixes()[$type]);
}

public static function getPascalCase(string $string): string
{
return ucfirst(
preg_replace_callback(
'/:([a-z])/i',
function (array $word): string {
return ucfirst(strtolower($word[1]));
},
preg_replace('/[^\da-z]+/i', ':', $string)
)
);
}

private function dump(array $types): string
Expand All @@ -35,7 +65,7 @@ private function dump(array $types): string

$namespaces = [];
foreach ($types as [$enumClass, $type, $name, $default]) {
$fqcn = self::getTypeClassname($enumClass, $type);
michnovka marked this conversation as resolved.
Show resolved Hide resolved
$fqcn = self::getTypeFullyQualifiedClassName($enumClass, $type, $name);
$classname = basename(str_replace('\\', '/', $fqcn));
$ns = substr($fqcn, 0, -\strlen($classname) - 1);

Expand Down
42 changes: 42 additions & 0 deletions src/Bridge/Doctrine/DBAL/Types/AbstractEnumSQLDeclarationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace Elao\Enum\Bridge\Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Elao\Enum\Exception\LogicException;

/**
* Base class for string enumerations with an `ENUM(...values)` column definition
*/
abstract class AbstractEnumSQLDeclarationType extends AbstractEnumType
{
/**
* {@inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
{
if (!$platform instanceof AbstractMySQLPlatform) {
throw new LogicException('SQL ENUM type is not supported on the current platform');
}

$values = array_map(
function ($val) {
return "'{$val->value}'";
},
($this->getEnumClass())::cases()
);

return 'ENUM(' . implode(', ', $values) . ')';
}
}
17 changes: 13 additions & 4 deletions src/Bridge/Doctrine/DBAL/Types/TypesDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
namespace Elao\Enum\Bridge\Doctrine\DBAL\Types;

use Elao\Enum\Bridge\Doctrine\Common\AbstractTypesDumper;
use Elao\Enum\Exception\LogicException;

/**
* @internal
*/
class TypesDumper extends AbstractTypesDumper
{
public const TYPE_SINGLE = 'single';
public const TYPE_SCALAR = 'scalar';
public const TYPE_ENUM = 'enum';
public const TYPES = [
self::TYPE_SINGLE,
self::TYPE_SCALAR,
self::TYPE_ENUM,
];

protected function getTypeCode(
Expand Down Expand Up @@ -49,7 +52,12 @@ public function getName(): string
PHP;
}

$baseClass = AbstractEnumType::class;
$baseClass = match ($type) {
self::TYPE_SCALAR => AbstractEnumType::class,
self::TYPE_ENUM => AbstractEnumSQLDeclarationType::class,
default => throw new LogicException(sprintf('Unexpected type "%s"', $type)),
};

$this->appendDefaultOnNullMethods($code, $enumClass, $defaultOnNull);

return <<<PHP
Expand Down Expand Up @@ -96,7 +104,8 @@ protected function onNullFromPhp(): int|string|null
protected static function getSuffixes(): array
{
return [
self::TYPE_SINGLE => 'Type',
self::TYPE_SCALAR => 'Type',
self::TYPE_ENUM => 'Type',
];
}
}
14 changes: 13 additions & 1 deletion src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ private function addDoctrineDbalSection(ArrayNodeDefinition $rootNode)
->addDefaultsIfNotSet()
->fixXmlConfig('type')
->children()
->booleanNode('enum_sql_declaration')
->defaultValue(false)
->info('If true, generate DBAL types with an ENUM SQL declaration with enum values instead of a VARCHAR/INT (Your platform must support it)')
->end()
->arrayNode('types')
->beforeNormalization()
->always(static function (array $values): array {
Expand Down Expand Up @@ -71,8 +75,16 @@ private function addDoctrineDbalSection(ArrayNodeDefinition $rootNode)
->end()
->enumNode('type')
->values(DBALTypesDumper::TYPES)
->info(<<<TXT
Which column definition to use and the way the enumeration values are stored in the database:
- scalar: VARCHAR/INT based on BackedEnum type
- enum: ENUM(...values) as strings based on BackedEnum type (Your platform must support it)
Default is either "scalar" or "enum", controlled by the `elao_enum.doctrine.enum_sql_declaration` option.
Default for flagged enums is "int".
TXT
)
->cannotBeEmpty()
->defaultValue(DBALTypesDumper::TYPE_SINGLE)
->defaultValue(null)
->end()
->variableNode('default')
->info('Default enumeration case on NULL')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Elao\Enum\Bridge\Doctrine\DBAL\Types\TypesDumper as DBALTypesDumper;
use Elao\Enum\Bridge\Doctrine\ODM\Types\TypesDumper as ODMTypesDumper;
use Elao\Enum\Bridge\Symfony\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
use Elao\Enum\FlagBag;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
Expand Down Expand Up @@ -46,20 +47,20 @@ public function load(array $configs, ContainerBuilder $container)
{
$config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs);

// Use Symfony's 6.1 backed enum resolver once available:
// TODO: Use Symfony's 6.1 backed enum resolver once available:
if (class_exists(\Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver::class)) {
$container->removeDefinition(BackedEnumValueResolver::class);
}

if ($types = $config['doctrine']['types'] ?? false) {
$container->setParameter(
'.elao_enum.doctrine_types',
array_map(static function (string $name, array $v): array {
array_map(function (string $name, array $v) use ($config): array {
$default = $v['default'];

return [
$v['class'],
$v['type'],
$this->resolveDbalType($v, $this->usesEnumSQLDeclaration($config)),
$name,
// Symfony DI parameters do not support enum cases (yet?).
// Does not fail in an array parameter, but the PhpDumper generate incorrect code for now.
Expand Down Expand Up @@ -95,7 +96,10 @@ private function prependDoctrineDbalConfig(array $config, ContainerBuilder $cont

$doctrineTypesConfig = [];
foreach ($types as $name => $value) {
$doctrineTypesConfig[$name] = DBALTypesDumper::getTypeClassname($value['class'], $value['type']);
$doctrineTypesConfig[$name] = DBALTypesDumper::getTypeFullyQualifiedClassName($value['class'], $this->resolveDbalType(
$value,
$this->usesEnumSQLDeclaration($config)
), $name);
}

$container->prependExtensionConfig('doctrine', [
Expand All @@ -113,9 +117,19 @@ private function prependDoctrineOdmConfig(array $config, ContainerBuilder $conta

$doctrineTypesConfig = [];
foreach ($types as $name => $value) {
$doctrineTypesConfig[$name] = ODMTypesDumper::getTypeClassname($value['class'], $value['type']);
$doctrineTypesConfig[$name] = ODMTypesDumper::getTypeFullyQualifiedClassName($value['class'], $value['type'], $name);
}

$container->prependExtensionConfig('doctrine_mongodb', ['types' => $doctrineTypesConfig]);
}

private function resolveDbalType(array $config, bool $useEnumSQLDeclaration): string
{
return $config['type'] ?? ($useEnumSQLDeclaration ? DBALTypesDumper::TYPE_ENUM : DBALTypesDumper::TYPE_SCALAR);
}

private function usesEnumSQLDeclaration(array $config): bool
{
return $config['doctrine']['enum_sql_declaration'];
}
}
10 changes: 9 additions & 1 deletion src/Bridge/Symfony/Bundle/config/schema/elao_enum.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@
<xsd:sequence>
<xsd:element name="type" type="doctrine_type" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="enum_sql_declaration" default="false" />
<xsd:attribute name="enum_sql_declaration" type="xsd:boolean" default="false" />
</xsd:complexType>

<xsd:complexType name="doctrine_type">
<xsd:attribute name="class" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="default" type="xsd:string"/>
<xsd:attribute name="type">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="scalar"/>
<xsd:enumeration value="enum"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>

<xsd:simpleType name="doctrine_type_type">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\Suit" name="suit" />
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\Permissions" name="permissions" />
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\RequestStatus" name="request_status" default="200" />
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\Suit" name="suit_enum" type="enum" />
</elao-enum:doctrine>
</elao-enum:config>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:elao-enum="http://elao.com/schema/dic/elao_enum"
xsi:schemaLocation="http://elao.com/schema/dic/elao_enum http://elao.com/schema/dic/elao_enum/elao_enum.xsd">

<elao-enum:config>
<elao-enum:doctrine enum_sql_declaration="true">
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\Suit" name="suit" />
<elao-enum:type class="Elao\Enum\Tests\Fixtures\Enum\Permissions" name="permissions" type="scalar" />
</elao-enum:doctrine>
</elao-enum:config>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ elao_enum:
request_status:
class: Elao\Enum\Tests\Fixtures\Enum\RequestStatus
default: !php/const Elao\Enum\Tests\Fixtures\Enum\RequestStatus::Success
suit_enum:
class: Elao\Enum\Tests\Fixtures\Enum\Suit
type: enum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
elao_enum:
doctrine:
enum_sql_declaration: true
types:
suit: Elao\Enum\Tests\Fixtures\Enum\Suit
permissions:
class: Elao\Enum\Tests\Fixtures\Enum\Permissions
type: scalar
4 changes: 1 addition & 3 deletions tests/Fixtures/Integration/Symfony/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ framework:

doctrine:
dbal:
driver: 'pdo_sqlite'
path: '%kernel.cache_dir%/db.sqlite'
charset: 'UTF8'
url: '%env(resolve:DOCTRINE_DBAL_URL)%'

orm:
auto_generate_proxy_classes: '%kernel.debug%'
Expand Down
17 changes: 17 additions & 0 deletions tests/Fixtures/Integration/Symfony/config/mysql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
doctrine:
orm:
mappings:
AppMySQL:
is_bundle: false
type: attribute
dir: '%kernel.project_dir%/src/EntityMySQL'
prefix: 'App\Entity'
alias: App

elao_enum:
doctrine:
types:
suit_sql_enum:
class: App\Enum\Suit
type: enum
default: !php/const App\Enum\Suit::Spades
45 changes: 45 additions & 0 deletions tests/Fixtures/Integration/Symfony/src/EntityMySQL/CardSQLEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <contact@elao.com>
*/

namespace App\EntityMySQL;

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

#[ORM\Entity]
#[ORM\Table(name: 'cards_sql_enum')]
class CardSQLEnum
{
#[ORM\Id]
#[ORM\Column(type: 'string')]
#[ORM\GeneratedValue(strategy: 'NONE')]
private string $uuid;

#[ORM\Column(type: 'suit_sql_enum', nullable: true)]
private ?Suit $suit;

public function __construct(string $uuid, ?Suit $suit = null)
{
$this->uuid = $uuid;
$this->suit = $suit;
}

public function getUuid(): string
{
return $this->uuid;
}

public function getSuit(): ?Suit
{
return $this->suit;
}
}
4 changes: 4 additions & 0 deletions tests/Fixtures/Integration/Symfony/src/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getProjectDir() . '/config/config.yml');

if (preg_match('/^pdo-mysql:\/\//i', $_ENV['DOCTRINE_DBAL_URL'])) {
$loader->load($this->getProjectDir() . '/config/mysql.yml');
}

if (class_exists(DoctrineMongoDBBundle::class)) {
$loader->load($this->getProjectDir() . '/config/mongodb.yml');
}
Expand Down
Loading