Skip to content

Commit

Permalink
Merge pull request #12 from thunderer/doctrine-type
Browse files Browse the repository at this point in the history
PlatenumDoctrineType
  • Loading branch information
thunderer authored Apr 19, 2020
2 parents 265a222 + 31cb814 commit ab1b98f
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 1 deletion.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
composer-install:
PHP_VERSION=7.4 docker-compose run --rm composer composer install
composer-update:
PHP_VERSION=7.4 docker-compose run --rm composer composer update
composer-require:
PHP_VERSION=7.4 docker-compose run --rm composer composer require ${PACKAGE}
composer-require-dev:
PHP_VERSION=7.4 docker-compose run --rm composer composer require --dev ${PACKAGE}

test: test-phpunit
test-phpunit:
PHP_VERSION=7.4 docker-compose run --rm php php -v
Expand All @@ -22,3 +31,6 @@ travis-job:

qa-psalm:
PHP_VERSION=7.4 docker-compose run --rm php php vendor/bin/psalm --no-cache

run-php:
PHP_VERSION=7.4 docker-compose run --rm php php ${FILE}
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,39 @@ final class AccountStatus
}
```

## Persistence

Enumerations are frequently used in entities and mapped in ORMs. Register your custom Doctrine enum type by calling dedicated `PlatenumDoctrineType` static method:

```php
PlatenumDoctrineType::registerString('currency', Currency::class);
PlatenumDoctrineType::registerInteger('accountStatus', AccountStatus::class);
```

The alias provided as a first argument can be then used as a Doctrine type, as shown in the listings below (equivalent XML and PHP mapping):

```xml
<entity name="App\Entity" table="app_entity">
<id name="id" type="bigint" column="id" />
<field name="currencyCode" type="currency" column="currency_code" />
<field name="status" type="accountStatus" column="status" />
</entity>
```

```php
final class Entity
{
public static function loadMetadata(ClassMetadata $m): void
{
$m->setPrimaryTable(['name' => 'doctrine_entity']);

$m->mapField(['fieldName' => 'id', 'type' => 'bigint', 'id' => true]);
$m->mapField(['fieldName' => 'code', 'type' => 'currency', 'columnName' => 'code']);
$m->mapField(['fieldName' => 'status', 'type' => 'accountStatus', 'columnName' => 'status']);
}
}
```

## Reasons

There are already a few `enum` libraries in the PHP ecosystem. Why another one? There are several reasons to do so:
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"ext-json": "*",
"phpunit/phpunit": ">=6.0",
"hirak/prestissimo": "^0.3.8",
"vimeo/psalm": "^3.10"
"vimeo/psalm": "^3.10",
"doctrine/dbal": "^2.9",
"doctrine/orm": "^2.7"
},
"autoload": {
"psr-4": {
Expand Down
125 changes: 125 additions & 0 deletions src/Doctrine/PlatenumDoctrineType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Doctrine;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Thunder\Platenum\Enum\EnumTrait;

/** @psalm-suppress PropertyNotSetInConstructor */
final class PlatenumDoctrineType extends Type
{
/** @var class-string */
private $platenumClass;
/** @var string */
private $platenumAlias;
/** @var callable */
private $platenumCallback;
/** @var callable(array,AbstractPlatform):string */
private $platenumSql;

/**
* @param string $alias
* @param class-string $class
*/
public static function registerInteger(string $alias, string $class): void
{
/** @psalm-suppress MissingClosureParamType */
$toInteger = function($value): int {
return (int)$value;
};
$sql = function(array $declaration, AbstractPlatform $platform): string {
return $platform->getIntegerTypeDeclarationSQL([]);
};

static::registerCallback($alias, $class, $toInteger, $sql);
}

/**
* @param string $alias
* @param class-string $class
*/
public static function registerString(string $alias, string $class): void
{
/** @psalm-suppress MissingClosureParamType */
$toString = function($value): string {
return (string)$value;
};
$sql = function(array $declaration, AbstractPlatform $platform): string {
return $platform->getVarcharTypeDeclarationSQL([]);
};

static::registerCallback($alias, $class, $toString, $sql);
}

/**
* @param string $alias
* @param class-string $class
* @param callable $callback
* @param callable(array<mixed>,AbstractPlatform):string $sql
*/
private static function registerCallback(string $alias, string $class, callable $callback, callable $sql): void
{
if(static::hasType($alias)) {
throw new \LogicException(sprintf('Alias `%s` was already registered in PlatenumDoctrineType.', $class));
}
if(false === in_array(EnumTrait::class, static::allTraitsOf($class))) {
throw new \LogicException(sprintf('PlatenumDoctrineType allows only Platenum enumerations, `%s` given.', $class));
}

static::addType($alias, static::class);

/** @var static $type */
$type = static::getType($alias);
$type->platenumAlias = $alias;
$type->platenumClass = $class;
$type->platenumCallback = $callback;
$type->platenumSql = $sql;
}

/**
* @param class-string $class
* @psalm-return list<string>
*/
private static function allTraitsOf(string $class): array
{
$traits = [];

do {
$traits = array_merge(class_uses($class, true), $traits);
} while($class = get_parent_class($class));

foreach ($traits as $trait => $same) {
$traits = array_merge(class_uses($trait, true), $traits);
}

return array_values(array_unique($traits));
}

public function getName(): string
{
return $this->platenumAlias;
}

public function getSQLDeclaration(array $declaration, AbstractPlatform $platform): string
{
return ($this->platenumSql)($declaration, $platform);
}

public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
/** @psalm-suppress MixedMethodCall */
return ($this->platenumCallback)($value->getValue());
}

public function convertToPHPValue($value, AbstractPlatform $platform)
{
/** @psalm-suppress MixedMethodCall */
return ($this->platenumClass)::fromValue(($this->platenumCallback)($value));
}

public function requiresSQLCommentHint(AbstractPlatform $platform): bool
{
return true;
}
}
47 changes: 47 additions & 0 deletions tests/DoctrineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests;

use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
use Thunder\Platenum\Doctrine\PlatenumDoctrineType;
use Thunder\Platenum\Tests\Fake\DoctrineEntity;
use Thunder\Platenum\Tests\Fake\DoctrineIntEnum;
use Thunder\Platenum\Tests\Fake\DoctrineStringEnum;

/**
* @author Tomasz Kowalczyk <[email protected]>
*/
final class DoctrineTest extends AbstractTestCase
{
public function testCreateFromMember(): void
{
$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'dbname' => ':memory:']);
$connection->exec('CREATE TABLE doctrine_entity (
id INTEGER NOT NULL PRIMARY KEY,
int_value INTEGER NOT NULL,
string_value VARCHAR(20) NOT NULL
)');
$configuration = new Configuration();
$configuration->setMetadataDriverImpl(new StaticPHPDriver([__DIR__.'/Fake']));
$configuration->setProxyDir(__DIR__.'/../var/doctrine');
$configuration->setProxyNamespace('Platenum\\Doctrine');

PlatenumDoctrineType::registerInteger('intEnum', DoctrineIntEnum::class);
PlatenumDoctrineType::registerString('stringEnum', DoctrineStringEnum::class);

$entity = new DoctrineEntity(1337, DoctrineIntEnum::FIRST(), DoctrineStringEnum::TWO());
$em = EntityManager::create($connection, $configuration);
$em->persist($entity);
$em->flush();
$em->clear();

$foundEntity = $em->find(DoctrineEntity::class, 1337);
$this->assertInstanceOf(DoctrineEntity::class, $foundEntity);
$this->assertSame($entity->getId(), $foundEntity->getId());
$this->assertSame($entity->getIntValue(), $foundEntity->getIntValue());
$this->assertSame($entity->getStringValue(), $foundEntity->getStringValue());
}
}
44 changes: 44 additions & 0 deletions tests/Fake/DoctrineEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Fake;

use Doctrine\ORM\Mapping\ClassMetadata;
use Thunder\Platenum\Enum\ConstantsEnumTrait;

final class DoctrineEntity
{
private $id;
private $intValue;
private $stringValue;

public function __construct(int $id, DoctrineIntEnum $int, DoctrineStringEnum $string)
{
$this->id = $id;
$this->intValue = $int;
$this->stringValue = $string;
}

public static function loadMetadata(ClassMetadata $metadata)
{
$metadata->setPrimaryTable(['name' => 'doctrine_entity']);

$metadata->mapField(['id' => true, 'fieldName' => 'id', 'type' => 'integer']);
$metadata->mapField(['fieldName' => 'intValue', 'columnName' => 'int_value', 'type' => 'intEnum']);
$metadata->mapField(['fieldName' => 'stringValue', 'columnName' => 'string_value', 'type' => 'stringEnum']);
}

public function getId()
{
return $this->id;
}

public function getIntValue(): DoctrineIntEnum
{
return $this->intValue;
}

public function getStringValue(): DoctrineStringEnum
{
return $this->stringValue;
}
}
17 changes: 17 additions & 0 deletions tests/Fake/DoctrineIntEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Fake;

use Thunder\Platenum\Enum\ConstantsEnumTrait;

/**
* @method static static FIRST()
* @method static static SECOND()
*/
final class DoctrineIntEnum
{
use ConstantsEnumTrait;

private const FIRST = 1;
private const SECOND = 2;
}
17 changes: 17 additions & 0 deletions tests/Fake/DoctrineStringEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Fake;

use Thunder\Platenum\Enum\ConstantsEnumTrait;

/**
* @method static static ONE()
* @method static static TWO()
*/
final class DoctrineStringEnum
{
use ConstantsEnumTrait;

private const ONE = 'one';
private const TWO = 'two';
}

0 comments on commit ab1b98f

Please sign in to comment.