Skip to content

Commit

Permalink
Add Symfony Bundle & configuration for generating BDAL types
Browse files Browse the repository at this point in the history
  • Loading branch information
ogizanagi committed Dec 14, 2021
1 parent e4905fb commit 7e1d3e4
Show file tree
Hide file tree
Showing 31 changed files with 998 additions and 37 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: pdo_sqlite
coverage: pcov
tools: 'composer:v2,flex'

Expand Down
85 changes: 84 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Elao Enumerations
```php
<?php

namespace App\Enum;

enum Suit: string implements ReadableEnumInterface
{
use ReadableEnumTrait;
Expand Down Expand Up @@ -43,9 +45,11 @@ each case instead of their names.
Use it instead of Symfony's one:

```php
<?php

namespace App\Form\Type;

use App\Config\Suit;
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;
Expand All @@ -66,3 +70,82 @@ class CardType extends AbstractType
// ...
}
```

## Doctrine

Given Doctrine DBAL and ORM [does not provide yet](https://github.com/doctrine/orm/issues/9021) a way to easily write
DBAL types for enums, this library provides some base classes to save your PHP backed enumerations in your database.

### In a Symfony app

This configuration is equivalent to the following sections explaining how to create a custom Doctrine DBAL type:

```yaml
elao_enum:
doctrine:
types:
App\Enum\Suit: ~ # Defaults to `{ class: App\Enum\Suit, default: null }`
permissions: { class: App\Enum\Permission } # You can set a name different from the enum FQCN
App\Enum\RequestStatus: { default: 200 } # Default value from enum cases, in case the db value is NULL
```
It'll actually generate & register the types classes for you, saving you from writing this boilerplate code.
### Manually
Read the
[Doctrine DBAL docs](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html)
first.
Extend the [AbstractEnumType](src/Bridge/Doctrine/DBAL/Types/AbstractEnumType.php):
```php
<?php

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;
}
}
```

In your application bootstrapping code:

```php
<?php

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

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

To convert the underlying database type of your new "Suit" type directly into an instance of `Suit` when performing
schema operations, the type has to be registered with the database platform as well:

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

Then use as:

```php
<?php

use App\Enum\Suit;

class Card
{
/** @Column(Suit::class, nullable=false) */
private Suit $field;
}
```
11 changes: 10 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,19 @@
"php": ">=8.1"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"doctrine/dbal": "^3.2",
"doctrine/doctrine-bundle": "^2.5",
"doctrine/orm": "^2.10",
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/filesystem": "^5.4|^6.0",
"symfony/form": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/http-kernel": "^5.4|^6.0",
"symfony/phpunit-bridge": "^5.4|^6.0",
"symfony/var-dumper": "^5.4|^6.0"
"symfony/var-dumper": "^5.4|^6.0",
"symfony/yaml": "^5.4|^6.0"
},
"extra": {
"branch-alias": {
Expand Down
13 changes: 3 additions & 10 deletions src/Bridge/Doctrine/DBAL/Types/AbstractEnumType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,22 @@ abstract class AbstractEnumType extends Type
/**
* The enum FQCN for which we should make the DBAL conversion.
*
* @return string
* @psalm-return class-string<\BackedEnum>
*/
abstract protected function getEnumClass(): string;

/**
* What should be returned on null value from the database.
*
* @return mixed
*/
protected function onNullFromDatabase()
protected function onNullFromDatabase(): ?\BackedEnum
{
return null;
}

/**
* What should be returned on null value from PHP.
*
* @return mixed
*/
protected function onNullFromPhp()
protected function onNullFromPhp(): int|string|null
{
return null;
}
Expand Down Expand Up @@ -120,10 +115,8 @@ public function getBindingType(): int
* Cast the value from database to proper enumeration internal type.
*
* @param int|string $value
*
* @return mixed
*/
protected function cast($value)
private function cast($value): int|string
{
return $this->isIntBackedEnum() ? (int) $value : (string) $value;
}
Expand Down
21 changes: 11 additions & 10 deletions src/Bridge/Doctrine/DBAL/Types/TypesDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
class TypesDumper
{
/**
* @param string $file
* @param array $types
*
* @return void
*/
public function dumpToFile(string $file, array $types)
Expand All @@ -35,7 +32,7 @@ public static function getTypeClassname(string $class): string

private function dump(array $types): string
{
array_walk($types, static function (&$type) {
array_walk($types, static function (& $type) {
$type = array_pad($type, 3, null);
});

Expand Down Expand Up @@ -70,7 +67,7 @@ private function getTypeCode(
string $classname,
string $enumClass,
string $name,
int|string|null $defaultOnNull = null
\BackedEnum|int|string|null $defaultOnNull = null
): string {
$code = <<<PHP
protected function getEnumClass(): string
Expand Down Expand Up @@ -110,19 +107,23 @@ private static function getMarker(): string
return 'ELAO_ENUM_DT_DBAL';
}

private function appendDefaultOnNullMethods(string &$code, string $enumClass, int|string|null $defaultOnNull): void
private function appendDefaultOnNullMethods(string & $code, string $enumClass, \BackedEnum|int|string|null $defaultOnNull): void
{
if ($defaultOnNull !== null) {
$defaultOnNullCode = var_export($defaultOnNull, true);
$defaultOnNullCode = var_export(
$defaultOnNull instanceof \BackedEnum ? $defaultOnNull->value : $defaultOnNull,
true,
);

$code .= <<<PHP
protected function onNullFromDatabase()
protected function onNullFromDatabase(): ?\BackedEnum
{
return \\{$enumClass}::get({$defaultOnNullCode});
return \\{$enumClass}::from($defaultOnNullCode);
}
protected function onNullFromPhp()
protected function onNullFromPhp(): int|string|null
{
return {$defaultOnNullCode};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <[email protected]>
*/

namespace Elao\Enum\Bridge\Symfony\Bundle\DependencyInjection\Compiler;

use Elao\Enum\Bridge\Doctrine\DBAL\Types\TypesDumper;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class DoctrineDBALTypesPass implements CompilerPassInterface
{
/** @var string */
private $typesFilePath;

public function __construct(string $typesFilePath)
{
$this->typesFilePath = $typesFilePath;
}

public function process(ContainerBuilder $container)
{
if (!$container->hasParameter('.elao_enum.doctrine_types')) {
return;
}

$types = $container->getParameter('.elao_enum.doctrine_types');

(new TypesDumper())->dumpToFile($this->typesFilePath, $types);

$container->getDefinition('doctrine.dbal.connection_factory')->setFile($this->typesFilePath);
}
}
79 changes: 79 additions & 0 deletions src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "elao/enum" package.
*
* Copyright (C) Elao
*
* @author Elao <[email protected]>
*/

namespace Elao\Enum\Bridge\Symfony\Bundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('elao_enum');

$this->addDoctrineDbalSection($treeBuilder->getRootNode());

return $treeBuilder;
}

private function addDoctrineDbalSection(ArrayNodeDefinition $rootNode)
{
$rootNode->children()
->arrayNode('doctrine')
->addDefaultsIfNotSet()
->fixXmlConfig('type')
->children()
->arrayNode('types')
->beforeNormalization()
->always(static function (array $values): array {
// Allows reusing type name as the enum class implicitly
foreach ($values as $name => &$config) {
if (null === $config) {
$config['class'] = $name;
continue;
}

if (\is_array($config) && !isset($config['class'])) {
$config['class'] = $name;
}
}

return $values;
})
->end()
->useAttributeAsKey('name')
->arrayPrototype()
->beforeNormalization()
// Allows passing the class as string directly instead of the whole config array
->ifString()->then(static function (string $v): array { return ['class' => $v]; })
->end()
->children()
->scalarNode('class')
->cannotBeEmpty()
->validate()
->ifTrue(static function (string $class): bool {return !is_a($class, \BackedEnum::class, true); })
->thenInvalid(sprintf('Invalid class. Expected instance of "%s"', \BackedEnum::class) . '. Got %s.')
->end()
->end()
->variableNode('default')
->info('Default enumeration case on NULL')
->cannotBeEmpty()
->defaultValue(null)
->end()
->end()
->end()
->end()
->end();
}
}
Loading

0 comments on commit 7e1d3e4

Please sign in to comment.