From 128f8c412d3ccf520b15c2ee6f22be37b31c1e68 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sat, 15 Apr 2023 14:15:20 +0200 Subject: [PATCH] implement PHP 8.1 Enum support [closes #585] --- composer.json | 2 - src/Entity/Embeddable/Embeddable.php | 2 +- src/Entity/Reflection/MetadataParser.php | 5 ++ src/Entity/Wrapper/BackedEnumWrapper.php | 50 +++++++++++++++++++ .../Collection/collection.embeddables.phpt | 2 +- .../Collection/collection.where.phpt | 8 +-- .../integration/Entity/entity.embeddable.phpt | 14 +++--- .../Entity/entity.nullValidation.phpt | 6 +++ .../unit/Collection/FetchPairsHelperTest.phpt | 4 +- tests/db/array-data.php | 8 +-- tests/inc/Currency.php | 13 ++--- tests/inc/Money.php | 2 +- tests/inc/TestEnumPropertyWrapper.php | 38 -------------- tests/inc/model/ean/Ean.php | 6 +-- tests/inc/model/ean/EanType.php | 11 ++-- 15 files changed, 93 insertions(+), 78 deletions(-) create mode 100644 src/Entity/Wrapper/BackedEnumWrapper.php delete mode 100644 tests/inc/TestEnumPropertyWrapper.php diff --git a/composer.json b/composer.json index ffdebd10..9ef40cda 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,6 @@ "nette/di": "^3.1", "nette/neon": "~3.0", "nette/tester": "~2.5", - "marc-mabe/php-enum": "~4.6", "mockery/mockery": ">=1.5.1", "phpstan/extension-installer": "1.2.0", "phpstan/phpstan": "1.10.9", @@ -41,7 +40,6 @@ "phpstan/phpstan-strict-rules": "1.4.5", "nextras/multi-query-parser": "~1.0", "nextras/orm-phpstan": "~1.0@dev", - "marc-mabe/php-enum-phpstan": "dev-master", "tracy/tracy": "~2.3" }, "autoload": { diff --git a/src/Entity/Embeddable/Embeddable.php b/src/Entity/Embeddable/Embeddable.php index 11726aa6..270cf7b1 100644 --- a/src/Entity/Embeddable/Embeddable.php +++ b/src/Entity/Embeddable/Embeddable.php @@ -144,7 +144,7 @@ private function createPropertyWrapper(PropertyMetadata $metadata): IProperty if ($wrapper instanceof IEntityAwareProperty) { if ($this->parentEntity === null) { - throw new InvalidStateException(""); + throw new InvalidStateException("Embeddable cannot contain a property having IEntityAwareProperty wrapper because embeddable is instanced before setting/attaching to its entity."); } else { $wrapper->onEntityAttach($this->parentEntity); } diff --git a/src/Entity/Reflection/MetadataParser.php b/src/Entity/Reflection/MetadataParser.php index 728e1ff4..97f3a649 100644 --- a/src/Entity/Reflection/MetadataParser.php +++ b/src/Entity/Reflection/MetadataParser.php @@ -3,6 +3,7 @@ namespace Nextras\Orm\Entity\Reflection; +use BackedEnum; use DateTime; use Nette\Utils\Reflection; use Nextras\Orm\Collection\ICollection; @@ -10,6 +11,7 @@ use Nextras\Orm\Entity\Embeddable\IEmbeddable; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IProperty; +use Nextras\Orm\Entity\Wrapper\BackedEnumWrapper; use Nextras\Orm\Exception\InvalidStateException; use Nextras\Orm\Exception\NotSupportedException; use Nextras\Orm\Relationships\HasMany; @@ -241,6 +243,9 @@ protected function parseAnnotationTypes(PropertyMetadata $property, string $type if ($type === DateTime::class || is_subclass_of($type, DateTime::class)) { throw new NotSupportedException("Type '{$type}' in {$this->currentReflection->name}::\${$property->name} property is not supported anymore. Use \DateTimeImmutable or \Nextras\Dbal\Utils\DateTimeImmutable type."); } + if (is_subclass_of($type, BackedEnum::class)) { + $property->wrapper = BackedEnumWrapper::class; + } } $parsedTypes[$type] = true; } diff --git a/src/Entity/Wrapper/BackedEnumWrapper.php b/src/Entity/Wrapper/BackedEnumWrapper.php new file mode 100644 index 00000000..fc530583 --- /dev/null +++ b/src/Entity/Wrapper/BackedEnumWrapper.php @@ -0,0 +1,50 @@ +propertyMetadata->isNullable) { + throw new NullValueException($this->propertyMetadata); + } + + return parent::setInjectedValue($value); + } + + + public function convertToRawValue(mixed $value): mixed + { + if ($value === null) return null; + $type = array_key_first($this->propertyMetadata->types); + assert($value instanceof $type); + assert($value instanceof BackedEnum); + return $value->value; + } + + + public function convertFromRawValue(mixed $value): ?BackedEnum + { + if ($value === null) { + if ($this->propertyMetadata->isNullable) return null; + throw new NullValueException($this->propertyMetadata); + } + + assert(is_int($value) || is_string($value)); + $type = array_key_first($this->propertyMetadata->types); + assert(is_subclass_of($type, BackedEnum::class)); + return $type::from($value); + } +} diff --git a/tests/cases/integration/Collection/collection.embeddables.phpt b/tests/cases/integration/Collection/collection.embeddables.phpt index e53d8797..7b7dd40a 100644 --- a/tests/cases/integration/Collection/collection.embeddables.phpt +++ b/tests/cases/integration/Collection/collection.embeddables.phpt @@ -30,7 +30,7 @@ class CollectionEmbeddablesTest extends DataTestCase Assert::same(0, $books1->countStored()); $book = $this->orm->books->getByIdChecked(1); - $book->price = new Money(1000, Currency::CZK()); + $book->price = new Money(1000, Currency::CZK); $this->orm->persistAndFlush($book); $books2 = $this->orm->books->findBy(['price->cents>=' => 1000]); diff --git a/tests/cases/integration/Collection/collection.where.phpt b/tests/cases/integration/Collection/collection.where.phpt index 91e34446..ce95a0f9 100644 --- a/tests/cases/integration/Collection/collection.where.phpt +++ b/tests/cases/integration/Collection/collection.where.phpt @@ -98,25 +98,25 @@ class CollectionWhereTest extends DataTestCase public function testFilterByPropertyWrapper(): void { - $ean8 = new Ean(EanType::EAN8()); + $ean8 = new Ean(EanType::EAN8); $ean8->code = '123'; $ean8->book = $this->orm->books->getByIdChecked(1); $this->orm->persist($ean8); - $ean13 = new Ean(EanType::EAN13()); + $ean13 = new Ean(EanType::EAN13); $ean13->code = '456'; $ean13->book = $this->orm->books->getByIdChecked(2); $this->orm->persistAndFlush($ean13); Assert::count(2, $this->orm->eans->findAll()); - $eans = $this->orm->eans->findBy(['type' => EanType::EAN8()]); + $eans = $this->orm->eans->findBy(['type' => EanType::EAN8]); Assert::count(1, $eans); $fetched = $eans->fetch(); Assert::notNull($fetched); Assert::equal('123', $fetched->code); - $eans = $this->orm->eans->findBy(['type' => EanType::EAN13()]); + $eans = $this->orm->eans->findBy(['type' => EanType::EAN13]); Assert::count(1, $eans); $fetched = $eans->fetch(); Assert::notNull($fetched); diff --git a/tests/cases/integration/Entity/entity.embeddable.phpt b/tests/cases/integration/Entity/entity.embeddable.phpt index 3a807ed0..40b8e6ef 100644 --- a/tests/cases/integration/Entity/entity.embeddable.phpt +++ b/tests/cases/integration/Entity/entity.embeddable.phpt @@ -25,9 +25,9 @@ class EntityEmbeddableTest extends DataTestCase public function testBasic(): void { $book = $this->orm->books->getByIdChecked(1); - $book->price = new Money(1000, Currency::CZK()); + $book->price = new Money(1000, Currency::CZK); Assert::same(1000, $book->price->cents); - Assert::same(Currency::CZK(), $book->price->currency); + Assert::same(Currency::CZK, $book->price->currency); $this->orm->persistAndFlush($book); $this->orm->clear(); @@ -36,7 +36,7 @@ class EntityEmbeddableTest extends DataTestCase Assert::notNull($book->price); Assert::same(1000, $book->price->cents); - Assert::same(Currency::CZK(), $book->price->currency); + Assert::same(Currency::CZK, $book->price->currency); $book->price = null; $this->orm->persistAndFlush($book); @@ -50,8 +50,8 @@ class EntityEmbeddableTest extends DataTestCase public function testMultiple(): void { $book = $this->orm->books->getByIdChecked(1); - $book->price = new Money(1000, Currency::CZK()); - $book->origPrice = new Money(330, Currency::EUR()); + $book->price = new Money(1000, Currency::CZK); + $book->origPrice = new Money(330, Currency::EUR); $this->orm->persistAndFlush($book); $this->orm->clear(); @@ -73,7 +73,7 @@ class EntityEmbeddableTest extends DataTestCase Assert::throws(function (): void { $book = new Book(); // @phpstan-ignore-next-line - $book->price = (object) ['price' => 100, 'currency' => Currency::CZK()]; + $book->price = (object) ['price' => 100, 'currency' => Currency::CZK]; }, InvalidArgumentException::class); } @@ -82,7 +82,7 @@ class EntityEmbeddableTest extends DataTestCase { $book = $this->orm->books->getByIdChecked(1); - $book->price = new Money(1000, Currency::CZK()); + $book->price = new Money(1000, Currency::CZK); Assert::same(1000, $book->price->cents); $book->price = null; diff --git a/tests/cases/integration/Entity/entity.nullValidation.phpt b/tests/cases/integration/Entity/entity.nullValidation.phpt index 4360bab4..be72a794 100644 --- a/tests/cases/integration/Entity/entity.nullValidation.phpt +++ b/tests/cases/integration/Entity/entity.nullValidation.phpt @@ -11,6 +11,7 @@ use Nextras\Orm\Exception\InvalidArgumentException; use Nextras\Orm\Exception\InvalidStateException; use Nextras\Orm\Exception\NullValueException; use NextrasTests\Orm\Book; +use NextrasTests\Orm\Ean; use NextrasTests\Orm\TestCase; use Tester\Assert; @@ -32,6 +33,11 @@ class EntityNullValidationTest extends TestCase $book->author = null; // @phpstan-ignore-line }, NullValueException::class, 'Property NextrasTests\Orm\Book::$author is not nullable.'); + Assert::throws(function (): void { + $ean = new Ean(); + $ean->type = null; // @phpstan-ignore-line + }, NullValueException::class, 'Property NextrasTests\Orm\Ean::$type is not nullable.'); + $book = new Book(); $book->translator = null; } diff --git a/tests/cases/unit/Collection/FetchPairsHelperTest.phpt b/tests/cases/unit/Collection/FetchPairsHelperTest.phpt index bc4ec155..3c046044 100644 --- a/tests/cases/unit/Collection/FetchPairsHelperTest.phpt +++ b/tests/cases/unit/Collection/FetchPairsHelperTest.phpt @@ -175,11 +175,11 @@ class FetchPairsHelperTest extends TestCase $data = new ArrayIterator([ $this->e( Book::class, - ['price' => new Money(100, Currency::CZK())] + ['price' => new Money(100, Currency::CZK)] ), $this->e( Book::class, - ['price' => new Money(200, Currency::CZK())] + ['price' => new Money(200, Currency::CZK)] ), ]); Assert::same( diff --git a/tests/db/array-data.php b/tests/db/array-data.php index 8db63bc2..bf3c0b08 100644 --- a/tests/db/array-data.php +++ b/tests/db/array-data.php @@ -44,7 +44,7 @@ $book1->translator = $author1; $book1->publisher = $publisher1; $book1->publishedAt = new \DateTimeImmutable('2021-12-14 21:10:04'); -$book1->price = new Money(50, Currency::CZK()); +$book1->price = new Money(50, Currency::CZK); $book1->tags->set([$tag1, $tag2]); $orm->books->persist($book1); @@ -53,7 +53,7 @@ $book2->author = $author1; $book2->publisher = $publisher2; $book2->publishedAt = new \DateTimeImmutable('2021-12-14 21:10:02'); -$book2->price = new Money(150, Currency::CZK()); +$book2->price = new Money(150, Currency::CZK); $book2->tags->set([$tag2, $tag3]); $orm->books->persist($book2); @@ -63,7 +63,7 @@ $book3->translator = $author2; $book3->publisher = $publisher3; $book3->publishedAt = new \DateTimeImmutable('2021-12-14 21:10:03'); -$book3->price = new Money(20, Currency::CZK()); +$book3->price = new Money(20, Currency::CZK); $book3->tags->set([$tag3]); $orm->books->persist($book3); @@ -74,7 +74,7 @@ $book4->publisher = $publisher1; $book4->nextPart = $book3; $book4->publishedAt = new \DateTimeImmutable('2021-12-14 21:10:01'); -$book4->price = new Money(220, Currency::CZK()); +$book4->price = new Money(220, Currency::CZK); $orm->books->persist($book4); $tagFollower1 = new TagFollower(); diff --git a/tests/inc/Currency.php b/tests/inc/Currency.php index b5f5a1b0..39ff7072 100644 --- a/tests/inc/Currency.php +++ b/tests/inc/Currency.php @@ -3,13 +3,10 @@ namespace NextrasTests\Orm; -use MabeEnum\Enum; - - -class Currency extends Enum +enum Currency: string { - const CZK = 'CZK'; - const EUR = 'EUR'; - const GBP = 'GBP'; - const USD = 'USD'; + case CZK = 'CZK'; + case EUR = 'EUR'; + case GBP = 'GBP'; + case USD = 'USD'; } diff --git a/tests/inc/Money.php b/tests/inc/Money.php index af075fb3..33f49919 100644 --- a/tests/inc/Money.php +++ b/tests/inc/Money.php @@ -8,7 +8,7 @@ /** * @property-read int $cents - * @property-read Currency $currency {wrapper TestEnumPropertyWrapper} + * @property-read Currency $currency */ class Money extends Embeddable { diff --git a/tests/inc/TestEnumPropertyWrapper.php b/tests/inc/TestEnumPropertyWrapper.php deleted file mode 100644 index 34875d54..00000000 --- a/tests/inc/TestEnumPropertyWrapper.php +++ /dev/null @@ -1,38 +0,0 @@ -types) === 1); - $this->enumClass = key($propertyMetadata->types); - assert(class_exists($this->enumClass)); - } - - - public function convertToRawValue($value) - { - assert($value instanceof Enum); - return $value->getValue(); - } - - - public function convertFromRawValue($value) - { - $enumClass = $this->enumClass; - return $enumClass::byValue($value); - } -} diff --git a/tests/inc/model/ean/Ean.php b/tests/inc/model/ean/Ean.php index c5955ca5..e8dbc886 100644 --- a/tests/inc/model/ean/Ean.php +++ b/tests/inc/model/ean/Ean.php @@ -10,13 +10,13 @@ * @property int|null $id {primary} * @property string $code * @property Book $book {1:1 Book::$ean} - * @property EanType $type {wrapper TestEnumPropertyWrapper} + * @property EanType $type */ class Ean extends Entity { - public function __construct(EanType $type = null) + public function __construct(EanType $type = EanType::EAN8) { parent::__construct(); - $this->type = $type ?? EanType::EAN8(); + $this->type = $type; } } diff --git a/tests/inc/model/ean/EanType.php b/tests/inc/model/ean/EanType.php index 63fef079..1eef8f51 100644 --- a/tests/inc/model/ean/EanType.php +++ b/tests/inc/model/ean/EanType.php @@ -3,12 +3,9 @@ namespace NextrasTests\Orm; -use MabeEnum\Enum; - - -class EanType extends Enum +enum EanType: int { - const EAN13 = 1; - const EAN8 = 2; - const CODE39 = 3; + case EAN13 = 1; + case EAN8 = 2; + case CODE39 = 3; }