Skip to content

Commit

Permalink
Merge 8279a2d into e45cf63
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach authored Apr 15, 2023
2 parents e45cf63 + 8279a2d commit 08709b3
Show file tree
Hide file tree
Showing 21 changed files with 134 additions and 140 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/AbstractEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public function getRawValues(bool $modifiedOnly = false): array
} else {
$out[$name] = $this->getProperty($name)->getRawValue();
if ($out[$name] === null && !$propertyMetadata->isNullable) {
throw new NullValueException($this, $propertyMetadata);
throw new NullValueException($propertyMetadata);
}
}
}
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
2 changes: 1 addition & 1 deletion src/Entity/Embeddable/EmbeddableContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public function setInjectedValue($value): bool
if ($value !== null && !$value instanceof $this->instanceType) {
throw new InvalidArgumentException("Value has to be instance of {$this->instanceType}" . ($this->metadata->isNullable ? ' or a null.' : '.'));
} elseif ($value === null && !$this->metadata->isNullable) {
throw new NullValueException($this->entity, $this->metadata);
throw new NullValueException($this->metadata);
}

if ($value !== null) {
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);
}
}
13 changes: 11 additions & 2 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 @@ -174,7 +176,7 @@ protected function parseAnnotations(ReflectionClass $reflection, array $methods)
{
preg_match_all(
'~^[ \t*]* @property(|-read|-write)[ \t]+([^\s$]+)[ \t]+\$(\w+)(.*)$~um',
(string) $reflection->getDocComment(), $matches, PREG_SET_ORDER
(string) $reflection->getDocComment(), $matches, PREG_SET_ORDER,
);

$properties = [];
Expand All @@ -183,6 +185,7 @@ protected function parseAnnotations(ReflectionClass $reflection, array $methods)

$property = new PropertyMetadata();
$property->name = $variable;
$property->containerClassname = $reflection->getName();
$property->isReadonly = $isReadonly;

$this->parseAnnotationTypes($property, $type);
Expand Down Expand Up @@ -228,7 +231,7 @@ protected function parseAnnotationTypes(PropertyMetadata $property, string $type
$typeLower = substr($typeLower, 1);
$type = substr($type, 1);
}
if (strpos($type, '[') !== false) { // string[]
if (str_contains($type, '[')) { // string[]
$type = 'array';
} elseif (isset($types[$typeLower])) {
$type = $typeLower;
Expand All @@ -240,12 +243,18 @@ 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;
}

$property->isNullable = $isNullable || isset($parsedTypes['null']) || isset($parsedTypes['NULL']) || isset($parsedTypes['mixed']);
unset($parsedTypes['null'], $parsedTypes['NULL']);
if (count($parsedTypes) < 1) {
throw new NotSupportedException("Property {$this->currentReflection->name}::\${$property->name} without a type definition is not supported.");
}
$property->types = $parsedTypes;
}

Expand Down
55 changes: 22 additions & 33 deletions src/Entity/Reflection/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,47 +24,35 @@ class PropertyMetadata
use SmartObject;


/** @var string property name */
public $name = '';
public string $name = '';
public string $containerClassname = '';
public ?string $wrapper = null;

/** @var string|null */
public $wrapper;
public ?string $hasGetter = null;
public ?string $hasSetter = null;

/** @var string|null */
public $hasGetter;
/** @var non-empty-array<string, bool> of allowed types defined as keys */
public array $types;

/** @var string|null */
public $hasSetter;

/** @var array<string, bool> of allowed types defined as keys */
public $types = [];

/** @var bool */
public $isPrimary = false;

/** @var bool */
public $isNullable = false;

/** @var bool */
public $isReadonly = false;

/** @var bool */
public $isVirtual = false;

/** @var mixed */
public $defaultValue;

/** @var PropertyRelationshipMetadata|null */
public $relationship;
public bool $isPrimary = false;
public bool $isNullable = false;
public bool $isReadonly = false;
public bool $isVirtual = false;
public mixed $defaultValue = null;
public ?PropertyRelationshipMetadata $relationship = null;

/** @var array<string, mixed>|null */
public $args;
public ?array $args = null;

/** @var array<mixed>|null array of allowed values */
public $enum;
public ?array $enum = null;

private ?IProperty $wrapperPrototype = null;


/** @var IProperty|null */
private $wrapperPrototype;
public function __construct()
{
}


public function getWrapperPrototype(): IProperty
Expand All @@ -85,6 +73,7 @@ public function __sleep()
// we skip wrapperPrototype which may not be serializable and is created lazily
return [
'name',
'containerClassname',
'wrapper',
'hasGetter',
'hasSetter',
Expand Down
6 changes: 2 additions & 4 deletions src/Exception/NullValueException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
namespace Nextras\Orm\Exception;


use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\Entity\Reflection\PropertyMetadata;


class NullValueException extends InvalidArgumentException
{
public function __construct(IEntity $entity, PropertyMetadata $propertyMetadata)
public function __construct(PropertyMetadata $propertyMetadata)
{
$class = get_class($entity);
parent::__construct("Property {$class}::\${$propertyMetadata->name} is not nullable.");
parent::__construct("Property {$propertyMetadata->containerClassname}::\${$propertyMetadata->name} is not nullable.");
}
}
4 changes: 2 additions & 2 deletions src/Relationships/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public function getEntity(): ?IEntity

if ($value === null && !$this->metadata->isNullable) {
assert($this->parent !== null);
throw new NullValueException($this->parent, $this->metadata);
throw new NullValueException($this->metadata);
}

return $value;
Expand Down Expand Up @@ -313,7 +313,7 @@ protected function createEntity($entity, bool $allowNull): ?IEntity

} elseif ($entity === null) {
if (!$this->metadata->isNullable && !$allowNull) {
throw new NullValueException($this->getParentEntity(), $this->metadata);
throw new NullValueException($this->metadata);
}
return null;

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
Loading

0 comments on commit 08709b3

Please sign in to comment.