diff --git a/src/Collection/EntityIterator.php b/src/Collection/EntityIterator.php index 06a5e40a..f0d43058 100644 --- a/src/Collection/EntityIterator.php +++ b/src/Collection/EntityIterator.php @@ -8,7 +8,11 @@ use Nette\Utils\Arrays; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IEntityHasPreloadContainer; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Exception\InvalidStateException; +use function assert; +use function count; +use function spl_object_hash; /** @@ -87,24 +91,23 @@ public function count(): int } - public function getPreloadValues(string $property): array + public function getPreloadValues(PropertyMetadata $property): array { - if (isset($this->preloadCache[$property])) { - return $this->preloadCache[$property]; + $cacheKey = spl_object_hash($property); + if (isset($this->preloadCache[$cacheKey])) { + return $this->preloadCache[$cacheKey]; } $values = []; foreach ($this->iteratable as $entity) { - // property may not exist when using STI - if ($entity->getMetadata()->hasProperty($property)) { - // relationship may be already nulled in removed entity - $value = $entity->getRawValue($property); - if ($value !== null) { - $values[] = $value; - } + // $checkPropertyExistence = false - property may not exist when using STI + $value = $entity->getRawValue($property->path ?? $property->name, false); + // relationship may be already null-ed in removed entity + if ($value !== null) { + $values[] = $value; } } - return $this->preloadCache[$property] = $values; + return $this->preloadCache[$cacheKey] = $values; } } diff --git a/src/Collection/IEntityPreloadContainer.php b/src/Collection/IEntityPreloadContainer.php index 3bc4032e..35a54797 100644 --- a/src/Collection/IEntityPreloadContainer.php +++ b/src/Collection/IEntityPreloadContainer.php @@ -3,11 +3,14 @@ namespace Nextras\Orm\Collection; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; + + interface IEntityPreloadContainer { /** - * Returns array of $property values for preloading. + * Returns array of values in $propertyMetadata position for preloading. * @phpstan-return list */ - public function getPreloadValues(string $property): array; + public function getPreloadValues(PropertyMetadata $propertyMetadata): array; } diff --git a/src/Collection/MultiEntityIterator.php b/src/Collection/MultiEntityIterator.php index 794e76fa..1642e53c 100644 --- a/src/Collection/MultiEntityIterator.php +++ b/src/Collection/MultiEntityIterator.php @@ -8,7 +8,11 @@ use Nette\Utils\Arrays; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IEntityHasPreloadContainer; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Exception\InvalidStateException; +use function assert; +use function count; +use function spl_object_hash; /** @@ -105,26 +109,25 @@ public function count(): int } - public function getPreloadValues(string $property): array + public function getPreloadValues(PropertyMetadata $property): array { - if (isset($this->preloadCache[$property])) { - return $this->preloadCache[$property]; + $cacheKey = spl_object_hash($property); + if (isset($this->preloadCache[$cacheKey])) { + return $this->preloadCache[$cacheKey]; } $values = []; foreach ($this->data as $entities) { foreach ($entities as $entity) { - // property may not exist when using STI - if ($entity->getMetadata()->hasProperty($property)) { - // relationship may be already nulled in removed entity - $value = $entity->getRawValue($property); - if ($value !== null) { - $values[] = $value; - } + // $checkPropertyExistence = false - property may not exist when using STI + $value = $entity->getRawValue($property->path ?? $property->name, false); + // relationship may be already null-ed in removed entity + if ($value !== null) { + $values[] = $value; } } } - return $this->preloadCache[$property] = $values; + return $this->preloadCache[$cacheKey] = $values; } } diff --git a/src/Entity/AbstractEntity.php b/src/Entity/AbstractEntity.php index 014bfd15..7ad27c8f 100644 --- a/src/Entity/AbstractEntity.php +++ b/src/Entity/AbstractEntity.php @@ -13,6 +13,7 @@ use Nextras\Orm\Relationships\IRelationshipCollection; use Nextras\Orm\Relationships\IRelationshipContainer; use Nextras\Orm\Repository\IRepository; +use function array_shift; use function assert; use function get_class; @@ -130,23 +131,39 @@ public function setRawValue(string $name, $value): void } - public function &getRawValue(string $name) + public function &getRawValue($name, bool $checkPropertyExistence = false) { - $property = $this->metadata->getProperty($name); + $path = (array) $name; + $name = array_shift($path); + + if (!$checkPropertyExistence && !$this->metadata->hasProperty($name)) { + $value = null; + return $value; + } + + $propertyMetadata = $this->metadata->getProperty($name); if (!isset($this->validated[$name])) { - $this->initProperty($property, $name); + $this->initProperty($propertyMetadata, $name); } $value = $this->data[$name]; - if ($value instanceof IProperty) { + if (count($path) > 0) { + if (!$value instanceof IMultiPropertyPropertyContainer) { + throw new InvalidStateException("Path to raw value doesn't go through IMultiPropertyPropertyContainer property."); + } + + $value = $value->getRawValueOf($path, $checkPropertyExistence); + return $value; + + } elseif ($value instanceof IProperty) { $value = $value->getRawValue(); return $value; } - if ($property->isVirtual) { - $value = $this->internalGetValue($property, $name); + if ($propertyMetadata->isVirtual) { + $value = $this->internalGetValue($propertyMetadata, $name); return $value; } @@ -229,18 +246,18 @@ public function __clone() $this->data['id'] = null; $this->persistedId = null; $this->data[$name] = clone $this->data[$name]; - $this->data[$name]->setPropertyEntity($this); + $this->data[$name]->onAttach($this, $metadataProperty); $this->data[$name]->set($data); $this->data['id'] = $id; $this->persistedId = $persistedId; } elseif ($this->data[$name] instanceof IRelationshipContainer) { $this->data[$name] = clone $this->data[$name]; - $this->data[$name]->setPropertyEntity($this); + $this->data[$name]->onAttach($this, $metadataProperty); } elseif ($this->data[$name] instanceof EmbeddableContainer) { $this->data[$name] = clone $this->data[$name]; - $this->data[$name]->setPropertyEntity($this); + $this->data[$name]->onAttach($this, $metadataProperty); } else { $this->data[$name] = clone $this->data[$name]; @@ -499,7 +516,7 @@ private function createPropertyWrapper(PropertyMetadata $metadata): IProperty assert($wrapper instanceof IProperty); if ($wrapper instanceof IEntityAwareProperty) { - $wrapper->setPropertyEntity($this); + $wrapper->onAttach($this, $metadata); } return $wrapper; diff --git a/src/Entity/Embeddable/Embeddable.php b/src/Entity/Embeddable/Embeddable.php index 83190375..ca7f8601 100644 --- a/src/Entity/Embeddable/Embeddable.php +++ b/src/Entity/Embeddable/Embeddable.php @@ -6,6 +6,7 @@ use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IEntityAwareProperty; use Nextras\Orm\Entity\ImmutableDataTrait; +use Nextras\Orm\Entity\IMultiPropertyPropertyContainer; use Nextras\Orm\Entity\IProperty; use Nextras\Orm\Entity\IPropertyContainer; use Nextras\Orm\Entity\Reflection\PropertyMetadata; @@ -27,6 +28,9 @@ abstract class Embeddable implements IEmbeddable /** @var IEntity|null */ protected $parentEntity; + /** @var PropertyMetadata */ + protected $propertyMetadata; + /** * @param array|null $data @@ -73,9 +77,53 @@ public function getRawValue(): array } - public function onAttach(IEntity $entity): void + public function &getRawValueOf(array $path, bool $checkPropertyExistence = true) + { + $name = array_shift($path); + + if (!$checkPropertyExistence && !$this->metadata->hasProperty($name)) { + $value = null; + return $value; + } + + $propertyMetadata = $this->metadata->getProperty($name); + + if (!isset($this->validated[$name])) { + $this->initProperty($propertyMetadata, $name); + } + + $value = $this->data[$name]; + + if (count($path) > 0) { + if (!$value instanceof IMultiPropertyPropertyContainer) { + throw new InvalidStateException("Path to raw value doesn't go through IMultiPropertyPropertyContainer property."); + } + + $value = $value->getRawValueOf($path, $checkPropertyExistence); + return $value; + + } elseif ($value instanceof IProperty) { + $value = $value->getRawValue(); + return $value; + } + + return $value; + } + + + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void { $this->parentEntity = $entity; + $this->propertyMetadata = $propertyMetadata; + + foreach ($this->data as $key => $property) { + if ($property instanceof IEntityAwareProperty) { + $property->onAttach( + $entity, + $this->metadata->getProperty($key)->withPath($this->propertyMetadata->path) + ); + } + } } @@ -140,12 +188,8 @@ private function createPropertyWrapper(PropertyMetadata $metadata): IProperty $wrapper = new $class($metadata); assert($wrapper instanceof IProperty); - if ($wrapper instanceof IEntityAwareProperty) { - if ($this->parentEntity === null) { - throw new InvalidStateException(""); - } else { - $wrapper->setPropertyEntity($this->parentEntity); - } + if ($wrapper instanceof IEntityAwareProperty && $this->parentEntity !== null) { + $wrapper->onAttach($this->parentEntity, $metadata->withPath($this->propertyMetadata->path)); } return $wrapper; diff --git a/src/Entity/Embeddable/EmbeddableContainer.php b/src/Entity/Embeddable/EmbeddableContainer.php index 4fff76d2..45ef795c 100644 --- a/src/Entity/Embeddable/EmbeddableContainer.php +++ b/src/Entity/Embeddable/EmbeddableContainer.php @@ -6,6 +6,7 @@ use Nette\SmartObject; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\IEntityAwareProperty; +use Nextras\Orm\Entity\IMultiPropertyPropertyContainer; use Nextras\Orm\Entity\IPropertyContainer; use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Exception\InvalidArgumentException; @@ -17,7 +18,7 @@ use function count; -class EmbeddableContainer implements IPropertyContainer, IEntityAwareProperty +class EmbeddableContainer implements IPropertyContainer, IMultiPropertyPropertyContainer, IEntityAwareProperty { use SmartObject; @@ -50,9 +51,14 @@ public function __construct(PropertyMetadata $propertyMetadata) } - public function setPropertyEntity(IEntity $entity): void + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void { $this->entity = $entity; + $this->metadata = $propertyMetadata->withPath($propertyMetadata->path ?? []); // force creation + + if ($this->value !== null) { + $this->value->onAttach($entity, $this->metadata); + } } @@ -106,6 +112,16 @@ public function getRawValue() } + public function getRawValueOf(array $path, bool $checkPropertyExistence = true) + { + if ($this->value !== null) { + return $this->value->getRawValueOf($path, $checkPropertyExistence); + } + + return null; + } + + public function setInjectedValue($value): bool { assert($this->entity !== null); @@ -118,7 +134,7 @@ public function setInjectedValue($value): bool if ($value !== null) { assert($value instanceof IEmbeddable); - $value->onAttach($this->entity); + $value->onAttach($this->entity, $this->metadata); } $this->value = $value; diff --git a/src/Entity/Embeddable/IEmbeddable.php b/src/Entity/Embeddable/IEmbeddable.php index 50fd447c..5146c875 100644 --- a/src/Entity/Embeddable/IEmbeddable.php +++ b/src/Entity/Embeddable/IEmbeddable.php @@ -4,6 +4,7 @@ use Nextras\Orm\Entity\IEntity; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; interface IEmbeddable @@ -37,10 +38,19 @@ public function setRawValue(array $data): void; public function getRawValue(): array; + /** + * Returns raw value for specific property. + * @param string[] $path + * @phpstan-param list $path + * @return mixed + */ + public function getRawValueOf(array $path, bool $checkPropertyExistence = true); + + /** * Attaches entity to embeddable object. * This is called after injecting embeddable into property wrapper. * @internal */ - public function onAttach(IEntity $entity): void; + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void; } diff --git a/src/Entity/IEntity.php b/src/Entity/IEntity.php index e9b66b01..5b32c2fe 100644 --- a/src/Entity/IEntity.php +++ b/src/Entity/IEntity.php @@ -53,10 +53,12 @@ public function setRawValue(string $name, $value): void; /** * Returns raw value. - * Raw value is normalized value which is suitable unique identification and storing. + * Raw value is normalized value to be suitable for storing. + * @param string|string[] $name + * @phpstan-param string|list $name * @return mixed */ - public function &getRawValue(string $name); + public function &getRawValue($name, bool $checkPropertyExistence = true); /** diff --git a/src/Entity/IEntityAwareProperty.php b/src/Entity/IEntityAwareProperty.php index 8f55eca6..c2a6ad8c 100644 --- a/src/Entity/IEntityAwareProperty.php +++ b/src/Entity/IEntityAwareProperty.php @@ -3,7 +3,23 @@ namespace Nextras\Orm\Entity; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; + + +/** + * Property that requires entity & nesting property path hierarchy to work correctly. + */ interface IEntityAwareProperty extends IProperty { - public function setPropertyEntity(IEntity $entity): void; + /** + * Attaches entity to the property. + * + * Passed property metadata is properly configured in context of entity/embeddable nesting property path hierarchy. + * If you need access metadata before property is attached, use the constructor passed metadata. The codee should + * throw whe the hierarchy is needed before this attachment. + * + * @internal + * @ignore + */ + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void; } diff --git a/src/Entity/IMultiPropertyPropertyContainer.php b/src/Entity/IMultiPropertyPropertyContainer.php new file mode 100644 index 00000000..c28a19ad --- /dev/null +++ b/src/Entity/IMultiPropertyPropertyContainer.php @@ -0,0 +1,12 @@ +path = $path; + $copy->path[] = $copy->name; + return $copy; + } + + public function getWrapperPrototype(): IProperty { if ($this->wrapperPrototype === null) { diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php index 9eb9f19b..e4c457a3 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php @@ -118,7 +118,8 @@ public function getIterator(IEntity $parent, ICollection $collection): Iterator protected function execute(DbalCollection $collection, IEntity $parent): MultiEntityIterator { $preloadIterator = $parent instanceof IEntityHasPreloadContainer ? $parent->getPreloadContainer() : null; - $values = $preloadIterator !== null ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')]; + $idPropertyMetadata = $parent->getMetadata()->getProperty('id'); + $values = $preloadIterator !== null ? $preloadIterator->getPreloadValues($idPropertyMetadata) : [$parent->getValue('id')]; $builder = $collection->getQueryBuilder(); $cacheKey = $this->calculateCacheKey($builder, $values); @@ -206,7 +207,8 @@ public function getIteratorCount(IEntity $parent, ICollection $collection): int protected function executeCounts(DbalCollection $collection, IEntity $parent): array { $preloadIterator = $parent instanceof IEntityHasPreloadContainer ? $parent->getPreloadContainer() : null; - $values = $preloadIterator !== null ? $preloadIterator->getPreloadValues('id') : [$parent->getValue('id')]; + $idPropertyMetadata = $parent->getMetadata()->getProperty('id'); + $values = $preloadIterator !== null ? $preloadIterator->getPreloadValues($idPropertyMetadata) : [$parent->getValue('id')]; $builder = $collection->getQueryBuilder(); $cacheKey = $this->calculateCacheKey($builder, $values); diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasOne.php b/src/Mapper/Dbal/RelationshipMapperManyHasOne.php index 76a976f8..7fc3260e 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasOne.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasOne.php @@ -50,7 +50,7 @@ public function getIterator(IEntity $parent, ICollection $collection): Iterator { assert($collection instanceof DbalCollection); $container = $this->execute($collection, $parent); - $container->setDataIndex($parent->getRawValue($this->metadata->name)); + $container->setDataIndex($parent->getRawValue($this->metadata->path ?? $this->metadata->name)); return new ArrayIterator(iterator_to_array($container)); } @@ -72,7 +72,9 @@ public function clearCache(): void protected function execute(DbalCollection $collection, IEntity $parent): MultiEntityIterator { $preloadContainer = $parent instanceof IEntityHasPreloadContainer ? $parent->getPreloadContainer() : null; - $values = $preloadContainer !== null ? $preloadContainer->getPreloadValues($this->metadata->name) : [$parent->getRawValue($this->metadata->name)]; + $values = $preloadContainer !== null + ? $preloadContainer->getPreloadValues($this->metadata) + : [$parent->getRawValue($this->metadata->path ?? $this->metadata->name)]; $builder = $collection->getQueryBuilder(); $cacheKey = $this->calculateCacheKey($builder, $values); diff --git a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php index 88111512..8ccedf46 100644 --- a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php @@ -98,7 +98,8 @@ public function getIterator(IEntity $parent, ICollection $collection): Iterator protected function execute(DbalCollection $collection, IEntity $parent): MultiEntityIterator { $preloadContainer = $parent instanceof IEntityHasPreloadContainer ? $parent->getPreloadContainer() : null; - $values = $preloadContainer !== null ? $preloadContainer->getPreloadValues('id') : [$parent->getValue('id')]; + $idPropertyMetadata = $parent->getMetadata()->getProperty('id'); + $values = $preloadContainer !== null ? $preloadContainer->getPreloadValues($idPropertyMetadata) : [$parent->getValue('id')]; $builder = $collection->getQueryBuilder(); $cacheKey = $this->calculateCacheKey($builder, $values); @@ -125,6 +126,10 @@ protected function execute(DbalCollection $collection, IEntity $parent): MultiEn */ protected function fetchByOnePassStrategy(QueryBuilder $builder, array $values): MultiEntityIterator { + if (count($values) === 0) { + return new MultiEntityIterator([]); + } + $builder = clone $builder; $builder->andWhere('%column IN %any', "{$builder->getFromAlias()}.{$this->joinStorageKey}", $values); @@ -229,7 +234,8 @@ public function getIteratorCount(IEntity $parent, ICollection $collection): int protected function executeCounts(DbalCollection $collection, IEntity $parent): array { $preloadContainer = $parent instanceof IEntityHasPreloadContainer ? $parent->getPreloadContainer() : null; - $values = $preloadContainer !== null ? $preloadContainer->getPreloadValues('id') : [$parent->getValue('id')]; + $idPropertyMetadata = $parent->getMetadata()->getProperty('id'); + $values = $preloadContainer !== null ? $preloadContainer->getPreloadValues($idPropertyMetadata) : [$parent->getValue('id')]; $builder = $collection->getQueryBuilder(); $cacheKey = $this->calculateCacheKey($builder, $values); diff --git a/src/Mapper/Memory/RelationshipMapperManyHasOne.php b/src/Mapper/Memory/RelationshipMapperManyHasOne.php index e068e7a8..862f48a0 100644 --- a/src/Mapper/Memory/RelationshipMapperManyHasOne.php +++ b/src/Mapper/Memory/RelationshipMapperManyHasOne.php @@ -26,7 +26,7 @@ public function __construct(PropertyMetadata $metadata) public function getIterator(IEntity $parent, ICollection $collection): Iterator { - $key = $parent->getRawValue($this->metadata->name); + $key = $parent->getRawValue($this->metadata->path ?? $this->metadata->name); return new ArrayIterator( $key !== null ? [$collection->getByIdChecked($key)] : [] ); diff --git a/src/Relationships/HasMany.php b/src/Relationships/HasMany.php index a5119e40..9dd5da37 100644 --- a/src/Relationships/HasMany.php +++ b/src/Relationships/HasMany.php @@ -81,13 +81,10 @@ public function __construct(PropertyMetadata $metadata) } - /** - * @internal - * @ignore - */ - public function setPropertyEntity(IEntity $parent): void + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void { - $this->parent = $parent; + $this->parent = $entity; + $this->metadata = $propertyMetadata; } diff --git a/src/Relationships/HasOne.php b/src/Relationships/HasOne.php index aac359fd..bb2061c4 100644 --- a/src/Relationships/HasOne.php +++ b/src/Relationships/HasOne.php @@ -9,6 +9,7 @@ use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\Exception\InvalidArgumentException; +use Nextras\Orm\Exception\InvalidStateException; use Nextras\Orm\Exception\NullValueException; use Nextras\Orm\Mapper\IRelationshipMapper; use Nextras\Orm\Repository\IRepository; @@ -35,11 +36,17 @@ abstract class HasOne implements IRelationshipContainer */ protected $collection; + /** @var bool */ + protected $isValueLoaded = true; + + /** @var bool */ + protected $isValueFromStorage = false; + /** @var mixed|null */ protected $primaryValue; - /** @var IEntity|null|false */ - protected $value = false; + /** @var IEntity|null */ + protected $value; /** * @var IRepository|null @@ -65,13 +72,15 @@ public function __construct(PropertyMetadata $metadata) } - /** - * @internal - * @ignore - */ - public function setPropertyEntity(IEntity $parent): void + public function onAttach(IEntity $entity, PropertyMetadata $propertyMetadata): void { - $this->parent = $parent; + $this->parent = $entity; + $this->metadata = $propertyMetadata; + + if (!$this->isValueLoaded) { + // init value + $this->getEntity(); + } } @@ -86,7 +95,10 @@ public function convertToRawValue($value) public function setRawValue($value): void { + $isChanged = $this->primaryValue !== $value; $this->primaryValue = $value; + $this->isValueLoaded = !$isChanged && $value === null; + $this->isValueFromStorage = true; } @@ -98,6 +110,7 @@ public function getRawValue() public function setInjectedValue($value): bool { + $this->isValueFromStorage = false; return $this->set($value); } @@ -117,7 +130,7 @@ public function hasInjectedValue(): bool public function isLoaded(): bool { - return $this->value !== false; + return $this->isValueLoaded; } @@ -132,9 +145,20 @@ public function set($value, bool $allowNull = false): bool return false; } - $value = $this->createEntity($value, $allowNull); + if ($this->parent === null) { + if ($value instanceof IEntity) { + $this->value = $value; + $this->primaryValue = $value !== null && $value->hasValue('id') ? $value->getValue('id') : null; + } else { + $this->primaryValue = $value; + } + $this->isValueLoaded = false; + return true; + } + + $entity = $this->createEntity($value, $allowNull); + $isChanged = $this->isChanged($entity); - $isChanged = $this->isChanged($value); if ($isChanged) { $this->modify(); $oldValue = $this->value; @@ -142,29 +166,29 @@ public function set($value, bool $allowNull = false): bool $primaryValue = $this->getPrimaryValue(); $oldValue = $primaryValue !== null ? $this->getTargetRepository()->getById($primaryValue) : null; } - $this->updateRelationship($oldValue, $value, $allowNull); + $this->updateRelationship($oldValue, $entity, $allowNull); } else { - $this->initReverseRelationship($value); + $this->initReverseRelationship($entity); } - $this->primaryValue = $value !== null && $value->isPersisted() ? $value->getValue('id') : null; - $this->value = $value; + $this->isValueLoaded = true; + $this->primaryValue = $entity !== null && $entity->hasValue('id') ? $entity->getValue('id') : null; + $this->value = $entity; return $isChanged; } public function getEntity(): ?IEntity { - if ($this->value === false) { - $this->set($this->fetchValue()); + if (!$this->isValueLoaded && $this->value === null) { + $this->initValue(); } if ($this->value === null && !$this->metadata->isNullable) { throw new NullValueException($this->parent, $this->metadata); } - assert($this->value === null || $this->value instanceof IEntity); return $this->value; } @@ -175,17 +199,33 @@ public function isModified(): bool } - protected function fetchValue(): ?IEntity + protected function initValue(): void { - if (!$this->parent->isAttached()) { - return null; + if ($this->parent === null) { + throw new InvalidStateException('Relationship is not attached to a parent entity.'); + } + if ($this->isValueFromStorage) { + // load the value using relationship mapper to utilize preload container and not to validate if + // relationship's entity is really present in the database; + $this->set($this->fetchValue()); } else { - $collection = $this->getCollection(); - return iterator_to_array($collection->getIterator())[0] ?? null; + // load value directly to utilize value check + if ($this->value !== null && $this->value->hasValue('id')) { + $this->set($this->value->getValue('id')); + } else { + $this->set($this->primaryValue); + } } } + protected function fetchValue(): ?IEntity + { + $collection = $this->getCollection(); + return iterator_to_array($collection->getIterator())[0] ?? null; + } + + /** * @return mixed|null */ @@ -232,15 +272,7 @@ protected function getCollection(): ICollection protected function createEntity($entity, bool $allowNull): ?IEntity { if ($entity instanceof IEntity) { - if ($this->parent->isAttached()) { - $repository = $this->parent->getRepository()->getModel() - ->getRepository($this->metadataRelationship->repository); - $repository->attach($entity); - - } elseif ($entity->isAttached()) { - $repository = $entity->getRepository()->getModel()->getRepositoryForEntity($this->parent); - $repository->attach($this->parent); - } + $this->attachIfPossible($entity); return $entity; } elseif ($entity === null) { @@ -250,7 +282,11 @@ protected function createEntity($entity, bool $allowNull): ?IEntity return null; } elseif (is_scalar($entity)) { - return $this->getTargetRepository()->getById($entity); + $result = $this->getTargetRepository()->getById($entity); + if ($result === null && $entity !== null) { + throw new InvalidArgumentException("Entity with primary key '$entity' was not found."); + } + return $result; } else { throw new InvalidArgumentException('Value is not a valid entity representation.'); @@ -258,10 +294,23 @@ protected function createEntity($entity, bool $allowNull): ?IEntity } - /** - * @param IEntity|null $newValue - */ - protected function isChanged($newValue): bool + protected function attachIfPossible(IEntity $entity): void + { + if ($this->parent === null) return; + + if ($this->parent->isAttached()) { + $repository = $this->parent->getRepository()->getModel() + ->getRepository($this->metadataRelationship->repository); + $repository->attach($entity); + + } elseif ($entity->isAttached()) { + $repository = $entity->getRepository()->getModel()->getRepositoryForEntity($this->parent); + $repository->attach($this->parent); + } + } + + + protected function isChanged(?IEntity $newValue): bool { if ($this->value instanceof IEntity && $newValue instanceof IEntity) { return $this->value !== $newValue; @@ -272,7 +321,7 @@ protected function isChanged($newValue): bool return true; } elseif ($newValue instanceof IEntity && $newValue->isPersisted()) { - // value is persited entity or null + // value is persisted entity or null // newValue is persisted entity return $this->getPrimaryValue() !== $newValue->getValue('id'); diff --git a/src/Relationships/OneHasOne.php b/src/Relationships/OneHasOne.php index aaa49f33..0fac1907 100644 --- a/src/Relationships/OneHasOne.php +++ b/src/Relationships/OneHasOne.php @@ -18,10 +18,19 @@ protected function createCollection(): ICollection } + public function setRawValue($value): void + { + parent::setRawValue($value); + if (!$this->metadataRelationship->isMain) { + $this->isValueLoaded = false; + } + } + + public function getRawValue() { - if ($this->primaryValue === null && $this->value === false && !$this->metadataRelationship->isMain) { - $this->getEntity(); // init the value + if ($this->primaryValue === null && !$this->isValueLoaded && !$this->metadataRelationship->isMain) { + $this->initValue(); } return parent::getRawValue(); } @@ -29,7 +38,7 @@ public function getRawValue() public function hasInjectedValue(): bool { - if ($this->primaryValue === null && $this->value === false && !$this->metadataRelationship->isMain) { + if ($this->primaryValue === null && !$this->isValueLoaded && !$this->metadataRelationship->isMain) { return $this->fetchValue() !== null; } return parent::hasInjectedValue(); diff --git a/tests/cases/integration/Collection/collection.embeddables.phpt b/tests/cases/integration/Collection/collection.embeddables.phpt index 4aac2ada..8259329d 100644 --- a/tests/cases/integration/Collection/collection.embeddables.phpt +++ b/tests/cases/integration/Collection/collection.embeddables.phpt @@ -9,7 +9,6 @@ namespace NextrasTests\Orm\Integration\Collection; use Nextras\Orm\Exception\InvalidArgumentException; -use NextrasTests\Orm\Currency; use NextrasTests\Orm\DataTestCase; use NextrasTests\Orm\Money; use Tester\Assert; @@ -27,7 +26,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, $this->orm->currencies->getByIdChecked('CZK')); $this->orm->persistAndFlush($book); $books2 = $this->orm->books->findBy(['price->cents>=' => 1000]); diff --git a/tests/cases/integration/Entity/entity.embeddable.phpt b/tests/cases/integration/Entity/entity.embeddable.phpt index c68a4ab3..f703a163 100644 --- a/tests/cases/integration/Entity/entity.embeddable.phpt +++ b/tests/cases/integration/Entity/entity.embeddable.phpt @@ -9,9 +9,9 @@ namespace NextrasTests\Orm\Entity; use Nextras\Orm\Exception\InvalidArgumentException; +use Nextras\Orm\Exception\InvalidStateException; use Nextras\Orm\Exception\NullValueException; use NextrasTests\Orm\Book; -use NextrasTests\Orm\Currency; use NextrasTests\Orm\DataTestCase; use NextrasTests\Orm\Money; use Tester\Assert; @@ -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, 'CZK'); Assert::same(1000, $book->price->cents); - Assert::same(Currency::CZK(), $book->price->currency); + Assert::same('CZK', $book->price->currency->code); $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('CZK', $book->price->currency->code); $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, 'CZK'); + $book->origPrice = new Money(330, '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' => '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, 'CZK'); Assert::same(1000, $book->price->cents); $book->price = null; @@ -99,6 +99,39 @@ class EntityEmbeddableTest extends DataTestCase $book->price = null; }, NullValueException::class); } + + + public function testInvalidRelationship(): void + { + Assert::throws(function (): void { + $book = $this->orm->books->getByIdChecked(1); + $book->price = new Money(1000, 'GBP'); + }, InvalidArgumentException::class, "Entity with primary key 'GBP' was not found."); + } + + + public function testRelationships(): void + { + Assert::throws(function (): void { + $money = new Money(100, 'CZK'); + $money->currency; + }, InvalidStateException::class, 'Relationship is not attached to a parent entity.'); + + $money = new Money(100, 'CZK'); + $book = $this->orm->books->getByIdChecked(1); + $book->price = $money; + Assert::same('CZK', $money->currency->code); + + Assert::throws(function (): void { + $money = new Money(100, 'CZK'); + $book = new Book(); + $book->price = $money; + $money->currency; + }, InvalidStateException::class, 'Entity is not attached to a repository. Use IEntity::isAttached() method to check the state.'); + + $money = new Money(100, $this->orm->currencies->getById('CZK')); + Assert::same('CZK', $money->currency->code); + } } diff --git a/tests/cases/integration/Mapper/file.general.phpt b/tests/cases/integration/Mapper/file.general.phpt index 345bd2e6..72b7e86c 100644 --- a/tests/cases/integration/Mapper/file.general.phpt +++ b/tests/cases/integration/Mapper/file.general.phpt @@ -16,6 +16,7 @@ use NextrasTests\Orm\Author; use NextrasTests\Orm\AuthorsRepository; use NextrasTests\Orm\Book; use NextrasTests\Orm\BooksRepository; +use NextrasTests\Orm\CurrenciesRepository; use NextrasTests\Orm\EansRepository; use NextrasTests\Orm\Model; use NextrasTests\Orm\Publisher; @@ -114,10 +115,12 @@ class FileMapperTest extends TestCase $factory = new SimpleModelFactory( new Cache(new MemoryStorage()), [ + // @phpstan-ignore-next-line + 'authors' => new AuthorsRepository(new TestFileMapper($fileName('authors'))), // @phpstan-ignore-next-line 'books' => new BooksRepository(new TestFileMapper($fileName('books'))), // @phpstan-ignore-next-line - 'authors' => new AuthorsRepository(new TestFileMapper($fileName('authors'))), + 'currencies' => new CurrenciesRepository(new TestFileMapper($fileName('currencies'))), // @phpstan-ignore-next-line 'publishers' => new PublishersRepository(new TestFileMapper($fileName('publishers'))), // @phpstan-ignore-next-line diff --git a/tests/cases/integration/Relationships/relationships.oneHasOne.phpt b/tests/cases/integration/Relationships/relationships.oneHasOne.phpt index 61c72c4f..8674332e 100644 --- a/tests/cases/integration/Relationships/relationships.oneHasOne.phpt +++ b/tests/cases/integration/Relationships/relationships.oneHasOne.phpt @@ -219,7 +219,7 @@ class RelationshipOneHasOneTest extends DataTestCase } - public function testGetRawValue(): void + public function testGetRawValueOnNonPrimarySide(): void { $ean = new Ean(); $ean->code = '1234'; diff --git a/tests/cases/unit/Collection/EntityIteratorTest.phpt b/tests/cases/unit/Collection/EntityIteratorTest.phpt index 9e8366f8..3b023cc6 100644 --- a/tests/cases/unit/Collection/EntityIteratorTest.phpt +++ b/tests/cases/unit/Collection/EntityIteratorTest.phpt @@ -10,9 +10,11 @@ namespace NextrasTests\Orm\Collection; use Mockery; use Nextras\Orm\Collection\EntityIterator; use Nextras\Orm\Entity\Entity; -use Nextras\Orm\Entity\Reflection\EntityMetadata; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; use NextrasTests\Orm\TestCase; use Tester\Assert; +use function count; +use function iterator_to_array; $dic = require_once __DIR__ . '/../../../bootstrap.php'; @@ -28,17 +30,10 @@ class EntityIteratorTest extends TestCase Mockery::mock(Entity::class), Mockery::mock(Entity::class), ]; - $metadata = Mockery::mock(EntityMetadata::class); - $metadata->shouldReceive('hasProperty')->twice()->andReturn(true); - $metadata->shouldReceive('hasProperty')->once()->andReturn(false); - $metadata->shouldReceive('hasProperty')->once()->andReturn(true); - $data[0]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[0]->shouldReceive('getRawValue')->once()->with('id')->andReturn(123); - $data[1]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[1]->shouldReceive('getRawValue')->once()->with('id')->andReturn(321); - $data[2]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[3]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[3]->shouldReceive('getRawValue')->once()->with('id')->andReturn(789); + $data[0]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(123); + $data[1]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(321); + $data[2]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(null); + $data[3]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(789); $iterator = new EntityIterator($data); Assert::same(4, count($iterator)); @@ -50,7 +45,11 @@ class EntityIteratorTest extends TestCase Assert::same($data, iterator_to_array($iterator)); Assert::same($data, iterator_to_array($iterator)); // check iterator rewind - Assert::same([123, 321, 789], $iterator->getPreloadValues('id')); + + $idPropertyMetadata = new PropertyMetadata(); + $idPropertyMetadata->name = 'id'; + + Assert::same([123, 321, 789], $iterator->getPreloadValues($idPropertyMetadata)); } diff --git a/tests/cases/unit/Collection/FetchPairsHelperTest.phpt b/tests/cases/unit/Collection/FetchPairsHelperTest.phpt index 48fd7f2f..093706b5 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, new Currency('CZK', 'CZK'))] ), $this->e( Book::class, - ['price' => new Money(200, Currency::CZK())] + ['price' => new Money(200, new Currency('CZK', 'CZK'))] ), ]); Assert::same( diff --git a/tests/cases/unit/Collection/MultiEntityIteratorTest.phpt b/tests/cases/unit/Collection/MultiEntityIteratorTest.phpt index b7744be9..e5313a5f 100644 --- a/tests/cases/unit/Collection/MultiEntityIteratorTest.phpt +++ b/tests/cases/unit/Collection/MultiEntityIteratorTest.phpt @@ -10,9 +10,11 @@ namespace NextrasTests\Orm\Collection; use Mockery; use Nextras\Orm\Collection\MultiEntityIterator; use Nextras\Orm\Entity\Entity; -use Nextras\Orm\Entity\Reflection\EntityMetadata; +use Nextras\Orm\Entity\Reflection\PropertyMetadata; use NextrasTests\Orm\TestCase; use Tester\Assert; +use function count; +use function iterator_to_array; $dic = require_once __DIR__ . '/../../../bootstrap.php'; @@ -26,17 +28,10 @@ class MultiEntityIteratorTest extends TestCase 10 => [Mockery::mock(Entity::class), Mockery::mock(Entity::class)], 12 => [Mockery::mock(Entity::class), Mockery::mock(Entity::class)], ]; - $metadata = Mockery::mock(EntityMetadata::class); - $metadata->shouldReceive('hasProperty')->once()->andReturn(true); - $metadata->shouldReceive('hasProperty')->once()->andReturn(false); - $metadata->shouldReceive('hasProperty')->twice()->andReturn(true); - $data[10][0]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[10][0]->shouldReceive('getRawValue')->once()->with('id')->andReturn(123); - $data[10][1]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[12][0]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[12][0]->shouldReceive('getRawValue')->once()->with('id')->andReturn(321); - $data[12][1]->shouldReceive('getMetadata')->once()->andReturn($metadata); - $data[12][1]->shouldReceive('getRawValue')->once()->with('id')->andReturn(456); + $data[10][0]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(123); + $data[10][1]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(null); + $data[12][0]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(321); + $data[12][1]->shouldReceive('getRawValue')->once()->with('id', false)->andReturn(456); $iterator = new MultiEntityIterator($data); $iterator->setDataIndex(12); @@ -46,8 +41,11 @@ class MultiEntityIteratorTest extends TestCase $data[12][0]->shouldReceive('setPreloadContainer')->once()->with($iterator); $data[12][1]->shouldReceive('setPreloadContainer')->once()->with($iterator); + $idPropertyMetadata = new PropertyMetadata(); + $idPropertyMetadata->name = 'id'; + Assert::same($data[12], iterator_to_array($iterator)); - Assert::same([123, 321, 456], $iterator->getPreloadValues('id')); + Assert::same([123, 321, 456], $iterator->getPreloadValues($idPropertyMetadata)); $iterator->setDataIndex(13); Assert::same(0, count($iterator)); diff --git a/tests/db/array-data.php b/tests/db/array-data.php index dd866374..360cb571 100644 --- a/tests/db/array-data.php +++ b/tests/db/array-data.php @@ -38,13 +38,18 @@ $orm->tags->persist($tag2); $orm->tags->persist($tag3); +$currencyCZK = new Currency('CZK', 'Ceska koruna'); +$orm->persistAndFlush($currencyCZK); +$currencyEUR = new Currency('EUR', 'Euro'); +$orm->persistAndFlush($currencyEUR); + $book1 = new Book(); $book1->title = 'Book 1'; $book1->author = $author1; $book1->translator = $author1; $book1->publisher = $publisher1; $book1->publishedAt = new \DateTimeImmutable('2017-04-20 20:00:00'); -$book1->price = new Money(50, Currency::CZK()); +$book1->price = new Money(50, $currencyCZK); $book1->tags->set([$tag1, $tag2]); $orm->books->persist($book1); @@ -53,7 +58,7 @@ $book2->author = $author1; $book2->publisher = $publisher2; $book2->publishedAt = new \DateTimeImmutable('2017-04-20 18:00:00'); -$book2->price = new Money(150, Currency::CZK()); +$book2->price = new Money(150, $currencyCZK); $book2->tags->set([$tag2, $tag3]); $orm->books->persist($book2); @@ -63,7 +68,7 @@ $book3->translator = $author2; $book3->publisher = $publisher3; $book3->publishedAt = new \DateTimeImmutable('2017-04-20 19:00:00'); -$book3->price = new Money(20, Currency::CZK()); +$book3->price = new Money(20, $currencyCZK); $book3->tags->set([$tag3]); $orm->books->persist($book3); @@ -74,7 +79,7 @@ $book4->publisher = $publisher1; $book4->nextPart = $book3; $book4->publishedAt = new \DateTimeImmutable('2017-04-20 17:00:00'); -$book4->price = new Money(220, Currency::CZK()); +$book4->price = new Money(220, $currencyCZK); $orm->books->persist($book4); $tagFollower1 = new TagFollower(); diff --git a/tests/db/mssql-data.sql b/tests/db/mssql-data.sql index fdc00dbf..e427e958 100644 --- a/tests/db/mssql-data.sql +++ b/tests/db/mssql-data.sql @@ -1,5 +1,6 @@ DELETE FROM books_x_tags; DELETE FROM books; +DELETE FROM currencies; DELETE FROM eans; DELETE FROM tags; DELETE FROM authors; @@ -31,6 +32,9 @@ SET IDENTITY_INSERT tags OFF; DBCC checkident ('tags', reseed, 3) WITH NO_INFOMSGS; +INSERT INTO currencies (code, name) VALUES ('CZK', 'Ceska koruna'); +INSERT INTO currencies (code, name) VALUES ('EUR', 'Euro'); + SET IDENTITY_INSERT books ON; INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (1, 1, 1, 'Book 1', NULL, 1, DATEADD(ss, 4, CURRENT_TIMESTAMP), 50, 'CZK'); INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (2, 1, NULL, 'Book 2', NULL, 2, DATEADD(ss, 2, CURRENT_TIMESTAMP), 150, 'CZK'); diff --git a/tests/db/mssql-init.sql b/tests/db/mssql-init.sql index b74483bd..b5337ea3 100644 --- a/tests/db/mssql-init.sql +++ b/tests/db/mssql-init.sql @@ -34,6 +34,13 @@ CREATE TABLE eans PRIMARY KEY (id) ); +CREATE TABLE currencies +( + code CHAR(3) NOT NULL, + name VARCHAR(50) NOT NULL, + PRIMARY KEY (code) +); + CREATE TABLE books ( id int NOT NULL IDENTITY (1,1), @@ -54,7 +61,9 @@ CREATE TABLE books CONSTRAINT books_translator FOREIGN KEY (translator_id) REFERENCES authors (id), CONSTRAINT books_next_part FOREIGN KEY (next_part) REFERENCES books (id), CONSTRAINT books_publisher FOREIGN KEY (publisher_id) REFERENCES publishers (publisher_id), - CONSTRAINT books_ena FOREIGN KEY (ean_id) REFERENCES eans (id) + CONSTRAINT books_ean FOREIGN KEY (ean_id) REFERENCES eans (id), + CONSTRAINT books_price_currency FOREIGN KEY (price_currency) REFERENCES currencies (code), + CONSTRAINT books_orig_price_Currency FOREIGN KEY (orig_price_currency) REFERENCES currencies (code) ); CREATE INDEX book_title ON books (title); diff --git a/tests/db/mysql-data.sql b/tests/db/mysql-data.sql index 4891796a..b3cc7671 100644 --- a/tests/db/mysql-data.sql +++ b/tests/db/mysql-data.sql @@ -1,6 +1,7 @@ SET FOREIGN_KEY_CHECKS = 0; TRUNCATE books_x_tags; TRUNCATE books; +TRUNCATE currencies; TRUNCATE eans; TRUNCATE tags; TRUNCATE authors; @@ -21,6 +22,9 @@ INSERT INTO tags (id, name, is_global) VALUES (1, 'Tag 1', 'y'); INSERT INTO tags (id, name, is_global) VALUES (2, 'Tag 2', 'y'); INSERT INTO tags (id, name, is_global) VALUES (3, 'Tag 3', 'n'); +INSERT INTO currencies (code, name) VALUES ('CZK', 'Ceska koruna'); +INSERT INTO currencies (code, name) VALUES ('EUR', 'Euro'); + INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (1, 1, 1, 'Book 1', NULL, 1, DATE_ADD(NOW(), INTERVAL 4 SECOND), 50, 'CZK'); INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (2, 1, NULL, 'Book 2', NULL, 2, DATE_ADD(NOW(), INTERVAL 2 SECOND), 150, 'CZK'); INSERT INTO books (id, author_id, translator_id, title, next_part, publisher_id, published_at, price, price_currency) VALUES (3, 2, 2, 'Book 3', NULL, 3, DATE_ADD(NOW(), INTERVAL 3 SECOND), 20, 'CZK'); diff --git a/tests/db/mysql-init.sql b/tests/db/mysql-init.sql index 1d900f72..1f4770d7 100644 --- a/tests/db/mysql-init.sql +++ b/tests/db/mysql-init.sql @@ -34,6 +34,13 @@ CREATE TABLE eans PRIMARY KEY (id) ) AUTO_INCREMENT = 1; +CREATE TABLE currencies +( + code CHAR(3) NOT NULL, + name VARCHAR(50) NOT NULL, + PRIMARY KEY (code) +); + CREATE TABLE books ( id int NOT NULL AUTO_INCREMENT, @@ -54,7 +61,9 @@ CREATE TABLE books CONSTRAINT books_translator FOREIGN KEY (translator_id) REFERENCES authors (id), CONSTRAINT books_next_part FOREIGN KEY (next_part) REFERENCES books (id), CONSTRAINT books_publisher FOREIGN KEY (publisher_id) REFERENCES publishers (publisher_id), - CONSTRAINT books_ena FOREIGN KEY (ean_id) REFERENCES eans (id) + CONSTRAINT books_ean FOREIGN KEY (ean_id) REFERENCES eans (id), + CONSTRAINT books_price_currency FOREIGN KEY (price_currency) REFERENCES currencies (code), + CONSTRAINT books_orig_price_currency FOREIGN KEY (orig_price_currency) REFERENCES currencies (code) ) AUTO_INCREMENT = 4; CREATE INDEX book_title ON books (title); diff --git a/tests/db/pgsql-data.sql b/tests/db/pgsql-data.sql index 52ed037c..31ca3d2a 100644 --- a/tests/db/pgsql-data.sql +++ b/tests/db/pgsql-data.sql @@ -1,6 +1,7 @@ TRUNCATE books_x_tags CASCADE; TRUNCATE books CASCADE; TRUNCATE tags CASCADE; +TRUNCATE currencies CASCADE; TRUNCATE eans CASCADE; TRUNCATE authors CASCADE; TRUNCATE publishers CASCADE; @@ -29,6 +30,10 @@ INSERT INTO "tags" ("id", "name", "is_global") VALUES (3, 'Tag 3', 'n'); SELECT setval('tags_id_seq', 3, true); +INSERT INTO "currencies" ("code", "name") VALUES ('CZK', 'Ceska koruna'); +INSERT INTO "currencies" ("code", "name") VALUES ('EUR', 'Euro'); + + INSERT INTO "books" ("id", "author_id", "translator_id", "title", "next_part", "publisher_id", "published_at", "price", "price_currency") VALUES (1, 1, 1, 'Book 1', NULL, 1, NOW() + interval '4 seconds', 50, 'CZK'); INSERT INTO "books" ("id", "author_id", "translator_id", "title", "next_part", "publisher_id", "published_at", "price", "price_currency") VALUES (2, 1, NULL, 'Book 2', NULL, 2, NOW() + interval '2 seconds', 150, 'CZK'); INSERT INTO "books" ("id", "author_id", "translator_id", "title", "next_part", "publisher_id", "published_at", "price", "price_currency") VALUES (3, 2, 2, 'Book 3', NULL, 3, NOW() + interval '3 seconds', 20, 'CZK'); diff --git a/tests/db/pgsql-init.sql b/tests/db/pgsql-init.sql index 65823a7f..923b9a63 100644 --- a/tests/db/pgsql-init.sql +++ b/tests/db/pgsql-init.sql @@ -34,6 +34,13 @@ CREATE TABLE "eans" PRIMARY KEY ("id") ); +CREATE TABLE "currencies" +( + "code" CHAR(3) NOT NULL, + "name" VARCHAR(50) NOT NULL, + PRIMARY KEY ("code") +); + CREATE TABLE "books" ( "id" SERIAL4 NOT NULL, @@ -54,7 +61,9 @@ CREATE TABLE "books" CONSTRAINT "books_translator" FOREIGN KEY ("translator_id") REFERENCES authors ("id"), CONSTRAINT "books_next_part" FOREIGN KEY ("next_part") REFERENCES books ("id"), CONSTRAINT "books_publisher" FOREIGN KEY ("publisher_id") REFERENCES publishers ("publisher_id"), - CONSTRAINT "books_ean" FOREIGN KEY ("ean_id") REFERENCES eans ("id") + CONSTRAINT "books_ean" FOREIGN KEY ("ean_id") REFERENCES eans ("id"), + CONSTRAINT "books_price_currency" FOREIGN KEY ("price_currency") REFERENCES currencies ("code"), + CONSTRAINT "books_orig_price_currency" FOREIGN KEY ("orig_price_currency") REFERENCES currencies ("code") ); CREATE INDEX "book_title" ON "books" ("title"); diff --git a/tests/inc/Currency.php b/tests/inc/Currency.php deleted file mode 100644 index b5f5a1b0..00000000 --- a/tests/inc/Currency.php +++ /dev/null @@ -1,15 +0,0 @@ - $cents, 'currency' => $currency, diff --git a/tests/inc/model/Model.php b/tests/inc/model/Model.php index 3d1cfcfd..f18ac5a5 100644 --- a/tests/inc/model/Model.php +++ b/tests/inc/model/Model.php @@ -12,6 +12,7 @@ * @property-read BooksRepository $books * @property-read BookCollectionsRepository $bookColletions * @property-read ContentsRepository $contents + * @property-read CurrenciesRepository $currencies * @property-read EansRepository $eans * @property-read LogsRepository $logs * @property-read PhotoAlbumsRepository $photoAlbums diff --git a/tests/inc/model/currencies/CurrenciesMapper.php b/tests/inc/model/currencies/CurrenciesMapper.php new file mode 100644 index 00000000..487a982e --- /dev/null +++ b/tests/inc/model/currencies/CurrenciesMapper.php @@ -0,0 +1,21 @@ + + */ +class CurrenciesMapper extends DbalMapper +{ +} diff --git a/tests/inc/model/currencies/CurrenciesRepository.php b/tests/inc/model/currencies/CurrenciesRepository.php new file mode 100644 index 00000000..a9381759 --- /dev/null +++ b/tests/inc/model/currencies/CurrenciesRepository.php @@ -0,0 +1,25 @@ + + */ +class CurrenciesRepository extends Repository +{ + public static function getEntityClassNames(): array + { + return [Currency::class]; + } +} diff --git a/tests/inc/model/currencies/Currency.php b/tests/inc/model/currencies/Currency.php new file mode 100644 index 00000000..2c4a59e6 --- /dev/null +++ b/tests/inc/model/currencies/Currency.php @@ -0,0 +1,29 @@ +code = $code; + $this->name = $name; + } +}