Skip to content

Commit

Permalink
implement PHP 8.1 Enum support [closes #585]
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Apr 15, 2023
1 parent dc5570a commit 8279a2d
Show file tree
Hide file tree
Showing 16 changed files with 100 additions and 97 deletions.
2 changes: 0 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
26 changes: 7 additions & 19 deletions docs/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,22 @@ Data is accessible through properties. You have to annotate all properties that
* @property string $name
* @property DateTimeImmutable $born
* @property string|null $web
* @property AdminLevel $adminLevel
* @property-read int $age
*/
class Member extends Nextras\Orm\Entity\Entity
{
}

enum AdminLevel: int {
case Full = 1;
case Moderator = 2;
}
```

Phpdoc property definition consists of its type and name. If you would like to use read-only property, define it with `@property-read` annotation; such annotation is useful to define properties which are based on values of other properties. Properties could be optional/nullable; to do that, just provide another type - `null` or you could use it by prefixing the type name with a question mark - `?string`.

If you put some value into the property, the value will be validated by property type annotation. Type casting is performed if it is possible and safe. Supported types are `null`, `string`, `int`, `float`, `array`, `mixed` and object types. Validation is provided on all properties, except for properties defined with property wrapper - in that case validation should do its property wrapper.
If you put some value into the property, the value will be validated by property type annotation. Type casting is performed if it is possible and safe. Supported types are `null`, `string`, `int`, `float`, `array`, `mixed`, enum (backed) types and object types. Validation is provided on all properties, except for properties defined with property wrapper - in that case validation is responsibility of the property wrapper. PHP 8.1 enums are validated and their backed value is used for the storage layer.

Nextras Orm also provides enhanced support for date time handling. However, only "safe" `DateTimeImmutable` instances are supported as a property type. You may put a common `DateTime` instance as a value, but it will be automatically converted to DateTimeImmutable. Also, auto date string conversion is supported.

Expand Down Expand Up @@ -91,7 +97,6 @@ Each property can be annotated with a modifier. Modifiers are optional and provi
- `{virtual}` - marks property as "do not persist in storage";
- `{embeddable}` - encapsulates multiple properties into one wrapping object;
- `{wrapper PropertyWrapperClassName}` - sets property wrapper;
- `{enum self::TYPE_*}` - enables extended validation against values enumeration; we recommend using object enum types instead of scalar enum types;
- `{1:m TargetEntity::$property}` - see [relationships](relationships).
- `{m:1 TargetEntity::$property}` - see [relationships](relationships).
- `{m:m TargetEntity::$property}` - see [relationships](relationships).
Expand Down Expand Up @@ -231,23 +236,6 @@ class JsonWrapper extends ImmutableValuePropertyWrapper
}
```

#### `{enum}`

You can easily validate passed value by value enumeration. To set the enumeration validation, use `enum` modifier with the list of constants (separated by a space); or pass a constant name with a wildcard.

```php
/**
* ...
* @property int $type {enum self::TYPE_*}
*/
class Event extends Nextras\Orm\Entity\Entity
{
const TYPE_PUBLIC = 0;
const TYPE_PRIVATE = 1;
const TYPE_ANOTHER = 2;
}
```

### Entity dependencies

Your entity can require some dependency to work. Orm comes with `Nextras\Orm\Repository\IDependencyProvider` interface, which takes care about injecting needed dependencies. If you use `OrmExtension` for `Nette\DI`, it will automatically call standard DI injections (injection methods and `@inject` annotation). Dependencies are injected when an entity is attached to the repository.
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Embeddable/Embeddable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
50 changes: 50 additions & 0 deletions src/Entity/PropertyWrapper/BackedEnumWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Entity\PropertyWrapper;


use BackedEnum;
use Nextras\Orm\Entity\ImmutableValuePropertyWrapper;
use Nextras\Orm\Exception\NullValueException;
use function array_key_first;
use function assert;
use function is_int;
use function is_string;
use function is_subclass_of;


final class BackedEnumWrapper extends ImmutableValuePropertyWrapper
{
public function setInjectedValue($value): bool
{
if ($value === null && !$this->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);
}
}
5 changes: 5 additions & 0 deletions src/Entity/Reflection/MetadataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
namespace Nextras\Orm\Entity\Reflection;


use BackedEnum;
use DateTime;
use Nette\Utils\Reflection;
use Nextras\Orm\Collection\ICollection;
use Nextras\Orm\Entity\Embeddable\EmbeddableContainer;
use Nextras\Orm\Entity\Embeddable\IEmbeddable;
use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\IProperty;
use Nextras\Orm\Entity\PropertyWrapper\BackedEnumWrapper;
use Nextras\Orm\Exception\InvalidStateException;
use Nextras\Orm\Exception\NotSupportedException;
use Nextras\Orm\Relationships\HasMany;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
8 changes: 4 additions & 4 deletions tests/cases/integration/Collection/collection.where.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 7 additions & 7 deletions tests/cases/integration/Entity/entity.embeddable.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
}

Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions tests/cases/integration/Entity/entity.nullValidation.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/unit/Collection/FetchPairsHelperTest.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions tests/db/array-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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();
Expand Down
13 changes: 5 additions & 8 deletions tests/inc/Currency.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
2 changes: 1 addition & 1 deletion tests/inc/Money.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/**
* @property-read int $cents
* @property-read Currency $currency {wrapper TestEnumPropertyWrapper}
* @property-read Currency $currency
*/
class Money extends Embeddable
{
Expand Down
38 changes: 0 additions & 38 deletions tests/inc/TestEnumPropertyWrapper.php

This file was deleted.

Loading

0 comments on commit 8279a2d

Please sign in to comment.