diff --git a/Makefile b/Makefile index 8615151..3fc5ecd 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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} diff --git a/README.md b/README.md index b0ae814..755e36b 100644 --- a/README.md +++ b/README.md @@ -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 + + + + + +``` + +```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: diff --git a/composer.json b/composer.json index e5e120d..dd4b778 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/Doctrine/PlatenumDoctrineType.php b/src/Doctrine/PlatenumDoctrineType.php new file mode 100644 index 0000000..5eb8247 --- /dev/null +++ b/src/Doctrine/PlatenumDoctrineType.php @@ -0,0 +1,125 @@ +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,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 + */ + 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; + } +} diff --git a/tests/DoctrineTest.php b/tests/DoctrineTest.php new file mode 100644 index 0000000..b7493c0 --- /dev/null +++ b/tests/DoctrineTest.php @@ -0,0 +1,47 @@ + + */ +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()); + } +} diff --git a/tests/Fake/DoctrineEntity.php b/tests/Fake/DoctrineEntity.php new file mode 100644 index 0000000..5cce7ff --- /dev/null +++ b/tests/Fake/DoctrineEntity.php @@ -0,0 +1,44 @@ +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; + } +} diff --git a/tests/Fake/DoctrineIntEnum.php b/tests/Fake/DoctrineIntEnum.php new file mode 100644 index 0000000..940dec1 --- /dev/null +++ b/tests/Fake/DoctrineIntEnum.php @@ -0,0 +1,17 @@ +