From acb4794ebf12faba78c6712e8a0484e0aee93be4 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sun, 29 Dec 2019 11:45:43 +0100 Subject: [PATCH 1/3] collection/dbal: refactor path expression processing --- .../Helpers/ConditionParserHelper.php | 17 +- .../IQueryBuilderFilterFunction.php | 4 + .../CustomFunctions/IQueryBuilderFunction.php | 3 + src/Mapper/Dbal/Helpers/ColumnReference.php | 2 +- src/Mapper/Dbal/QueryBuilderHelper.php | 177 +++++++++++------- .../Collection/ConditionParserHelperTest.phpt | 6 +- 6 files changed, 137 insertions(+), 72 deletions(-) diff --git a/src/Collection/Helpers/ConditionParserHelper.php b/src/Collection/Helpers/ConditionParserHelper.php index e93007c0..1898683d 100644 --- a/src/Collection/Helpers/ConditionParserHelper.php +++ b/src/Collection/Helpers/ConditionParserHelper.php @@ -8,6 +8,7 @@ namespace Nextras\Orm\Collection\Helpers; +use Nextras\Orm\Entity\IEntity; use Nextras\Orm\InvalidArgumentException; @@ -24,7 +25,7 @@ class ConditionParserHelper public static function parsePropertyOperator(string $condition): array { - if (!preg_match('#^(.+?)(!=|<=|>=|=|>|<)?$#', $condition, $matches)) { + if (!\preg_match('#^(.+?)(!=|<=|>=|=|>|<)?$#', $condition, $matches)) { return [$condition, self::OPERATOR_EQUAL]; } else { return [$matches[1], $matches[2] ?? self::OPERATOR_EQUAL]; @@ -32,17 +33,23 @@ public static function parsePropertyOperator(string $condition): array } + /** + * @return array{array, class-string|null} + */ public static function parsePropertyExpr(string $propertyPath): array { - if (!preg_match('#^([\w\\\]+(?:->\w++)*+)\z#', $propertyPath, $matches)) { + if (!\preg_match('#^([\w\\\]+(?:->\w++)*+)\z#', $propertyPath, $matches)) { throw new InvalidArgumentException('Unsupported condition format.'); } $source = null; - $tokens = explode('->', $matches[1]); - if (count($tokens) > 1) { - $source = array_shift($tokens); + $tokens = \explode('->', $matches[1]); + if (\count($tokens) > 1) { + $source = \array_shift($tokens); $source = $source === 'this' ? null : $source; + if ($source !== null && !\is_subclass_of($source, IEntity::class)) { + throw new InvalidArgumentException("Property expression '$propertyPath' uses unknown class '$source'."); + } } return [$tokens, $source]; diff --git a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php b/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php index b968f3eb..68f23d9b 100644 --- a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php +++ b/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php @@ -14,5 +14,9 @@ interface IQueryBuilderFilterFunction { + /** + * @param array $args + * @return array list of Nextras Dbal's condition fragments + */ public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array; } diff --git a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php b/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php index 4cbf79ec..6dbac318 100644 --- a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php +++ b/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php @@ -14,5 +14,8 @@ interface IQueryBuilderFunction { + /** + * @param array $args + */ public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): QueryBuilder; } diff --git a/src/Mapper/Dbal/Helpers/ColumnReference.php b/src/Mapper/Dbal/Helpers/ColumnReference.php index 4991c614..8eccf2fd 100644 --- a/src/Mapper/Dbal/Helpers/ColumnReference.php +++ b/src/Mapper/Dbal/Helpers/ColumnReference.php @@ -15,7 +15,7 @@ class ColumnReference { - /** @var string|array */ + /** @var string|array */ public $column; /** @var PropertyMetadata */ diff --git a/src/Mapper/Dbal/QueryBuilderHelper.php b/src/Mapper/Dbal/QueryBuilderHelper.php index 41febf00..0e86abd1 100644 --- a/src/Mapper/Dbal/QueryBuilderHelper.php +++ b/src/Mapper/Dbal/QueryBuilderHelper.php @@ -63,7 +63,7 @@ public function processApplyFunction(QueryBuilder $builder, string $function, ar { $customFunction = $this->repository->getCollectionFunction($function); if (!$customFunction instanceof IQueryBuilderFunction) { - throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFunction interface."); + throw new InvalidStateException("Custom function $function has to implement " . IQueryBuilderFunction::class . ' interface.'); } return $customFunction->processQueryBuilderFilter($this, $builder, $expr); @@ -75,7 +75,7 @@ public function processFilterFunction(QueryBuilder $builder, array $expr): array $function = isset($expr[0]) ? array_shift($expr) : ICollection::AND; $customFunction = $this->repository->getCollectionFunction($function); if (!$customFunction instanceof IQueryBuilderFilterFunction) { - throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFilterFunction interface."); + throw new InvalidStateException("Custom function $function has to implement " . IQueryBuilderFilterFunction::class . ' interface.'); } return $customFunction->processQueryBuilderFilter($this, $builder, $expr); @@ -84,14 +84,8 @@ public function processFilterFunction(QueryBuilder $builder, array $expr): array public function processPropertyExpr(QueryBuilder $builder, string $propertyExpr): ColumnReference { - [$chain, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($propertyExpr); - $propertyName = array_pop($chain); - [$storageReflection, $alias, $entityMetadata] = $this->normalizeAndAddJoins($chain, $sourceEntity, $builder); - assert($storageReflection instanceof IStorageReflection); - assert($entityMetadata instanceof EntityMetadata); - $propertyMetadata = $entityMetadata->getProperty($propertyName); - $column = $this->toColumnExpr($entityMetadata, $propertyMetadata, $storageReflection, $alias); - return new ColumnReference($column, $propertyMetadata, $entityMetadata, $storageReflection); + [$tokens, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($propertyExpr); + return $this->processTokens($tokens, $sourceEntity, $builder); } @@ -129,79 +123,134 @@ public function normalizeValue($value, ColumnReference $columnReference) /** - * @return array [IStorageReflection $sourceReflection, string $sourceAlias, EntityMetadata $sourceEntityMeta] + * @param array $tokens + * @param class-string<\Nextras\Orm\Entity\IEntity>|null $sourceEntity */ - private function normalizeAndAddJoins(array $levels, $sourceEntity, QueryBuilder $builder): array + private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): ColumnReference { - $sourceMapper = $this->mapper; - $sourceAlias = $builder->getFromAlias(); - $sourceReflection = $sourceMapper->getStorageReflection(); - $sourceEntityMeta = $sourceMapper->getRepository()->getEntityMetadata($sourceEntity); + $lastToken = array_pop($tokens); + \assert($lastToken !== null); - foreach ($levels as $levelIndex => $level) { - $property = $sourceEntityMeta->getProperty($level); + $currentMapper = $this->mapper; + $currentAlias = $builder->getFromAlias(); + $currentReflection = $currentMapper->getStorageReflection(); + $currentEntityMetadata = $currentMapper->getRepository()->getEntityMetadata($sourceEntity); + + foreach ($tokens as $tokenIndex => $token) { + $property = $currentEntityMetadata->getProperty($token); if ($property->relationship === null) { - throw new InvalidArgumentException("Entity {$sourceEntityMeta->className}::\${$level} does not contain a relationship."); + throw new InvalidArgumentException("Entity {$currentEntityMetadata->className}::\${$token} does not contain a relationship."); } - $targetMapper = $this->model->getRepository($property->relationship->repository)->getMapper(); - assert($targetMapper instanceof DbalMapper); - $targetReflection = $targetMapper->getStorageReflection(); - $targetEntityMetadata = $property->relationship->entityMetadata; - - $relType = $property->relationship->type; - if ($relType === Relationship::ONE_HAS_MANY) { - assert($property->relationship->property !== null); - $targetColumn = $targetReflection->convertEntityToStorageKey($property->relationship->property); - $sourceColumn = $sourceReflection->getStoragePrimaryKey()[0]; - $this->makeDistinct($builder); - } elseif ($relType === Relationship::ONE_HAS_ONE && !$property->relationship->isMain) { - assert($property->relationship->property !== null); - $targetColumn = $targetReflection->convertEntityToStorageKey($property->relationship->property); - $sourceColumn = $sourceReflection->getStoragePrimaryKey()[0]; - } elseif ($relType === Relationship::MANY_HAS_MANY) { - $targetColumn = $targetReflection->getStoragePrimaryKey()[0]; - $sourceColumn = $sourceReflection->getStoragePrimaryKey()[0]; - $this->makeDistinct($builder); - - if ($property->relationship->isMain) { - assert($sourceMapper instanceof DbalMapper); - [$joinTable, [$inColumn, $outColumn]] = $sourceMapper->getManyHasManyParameters($property, $targetMapper); - } else { - assert($sourceMapper instanceof DbalMapper); - assert($property->relationship->property !== null); - $sourceProperty = $targetEntityMetadata->getProperty($property->relationship->property); - [$joinTable, [$outColumn, $inColumn]] = $targetMapper->getManyHasManyParameters($sourceProperty, $sourceMapper); - } + [ + $currentAlias, + $currentReflection, + $currentEntityMetadata, + $currentMapper, + ] = $this->processRelationship( + $tokens, + $builder, + $property, + $currentReflection, + $currentMapper, + $currentAlias, + $token, + $tokenIndex + ); + } - $builder->leftJoin($sourceAlias, "[$joinTable]", self::getAlias($joinTable), "[$sourceAlias.$sourceColumn] = [$joinTable.$inColumn]"); + $propertyMetadata = $currentEntityMetadata->getProperty($lastToken); + $column = $this->toColumnExpr($currentEntityMetadata, $propertyMetadata, $currentReflection, $currentAlias); + return new ColumnReference($column, $propertyMetadata, $currentEntityMetadata, $currentReflection); + } + + + /** + * @param array $tokens + * @param mixed $token + * @param int $tokenIndex + * @return array{string, IStorageReflection, EntityMetadata, DbalMapper} + */ + private function processRelationship( + array $tokens, + QueryBuilder $builder, + PropertyMetadata $property, + IStorageReflection $currentReflection, + DbalMapper $currentMapper, + string $currentAlias, + $token, + int $tokenIndex + ): array + { + \assert($property->relationship !== null); + $targetMapper = $this->model->getRepository($property->relationship->repository)->getMapper(); + \assert($targetMapper instanceof DbalMapper); + + $targetReflection = $targetMapper->getStorageReflection(); + $targetEntityMetadata = $property->relationship->entityMetadata; + + $relType = $property->relationship->type; + if ($relType === Relationship::ONE_HAS_MANY) { + \assert($property->relationship->property !== null); + $toColumn = $targetReflection->convertEntityToStorageKey($property->relationship->property); + $fromColumn = $currentReflection->getStoragePrimaryKey()[0]; + $this->makeDistinct($builder); + + } elseif ($relType === Relationship::ONE_HAS_ONE && !$property->relationship->isMain) { + \assert($property->relationship->property !== null); + $toColumn = $targetReflection->convertEntityToStorageKey($property->relationship->property); + $fromColumn = $currentReflection->getStoragePrimaryKey()[0]; + + } elseif ($relType === Relationship::MANY_HAS_MANY) { + $toColumn = $targetReflection->getStoragePrimaryKey()[0]; + $fromColumn = $currentReflection->getStoragePrimaryKey()[0]; + $this->makeDistinct($builder); + + if ($property->relationship->isMain) { + \assert($currentMapper instanceof DbalMapper); + [ + $joinTable, + [$inColumn, $outColumn], + ] = $currentMapper->getManyHasManyParameters($property, $targetMapper); - $sourceAlias = $joinTable; - $sourceColumn = $outColumn; } else { - $targetColumn = $targetReflection->getStoragePrimaryKey()[0]; - $sourceColumn = $sourceReflection->convertEntityToStorageKey($level); + \assert($currentMapper instanceof DbalMapper); + \assert($property->relationship->property !== null); + + $sourceProperty = $targetEntityMetadata->getProperty($property->relationship->property); + [ + $joinTable, + [$outColumn, $inColumn], + ] = $targetMapper->getManyHasManyParameters($sourceProperty, $currentMapper); } - $targetTable = $targetMapper->getTableName(); - $targetAlias = implode('_', array_slice($levels, 0, $levelIndex + 1)); - - $builder->leftJoin($sourceAlias, "[$targetTable]", $targetAlias, "[$sourceAlias.$sourceColumn] = [$targetAlias.$targetColumn]"); + $builder->leftJoin($currentAlias, "[$joinTable]", self::getAlias($joinTable), "[$currentAlias.$fromColumn] = [$joinTable.$inColumn]"); + $currentAlias = $joinTable; + $fromColumn = $outColumn; - $sourceAlias = $targetAlias; - $sourceMapper = $targetMapper; - $sourceReflection = $targetReflection; - $sourceEntityMeta = $targetEntityMetadata; + } else { + $toColumn = $targetReflection->getStoragePrimaryKey()[0]; + $fromColumn = $currentReflection->convertEntityToStorageKey($token); } - return [$sourceReflection, $sourceAlias, $sourceEntityMeta]; + $targetTable = $targetMapper->getTableName(); + $targetAlias = implode('_', array_slice($tokens, 0, $tokenIndex + 1)); + + $builder->leftJoin($currentAlias, "[$targetTable]", $targetAlias, "[$currentAlias.$fromColumn] = [$targetAlias.$toColumn]"); + + return [$targetAlias, $targetReflection, $targetEntityMetadata, $targetMapper]; } /** - * @return string|array + * @return string|array */ - private function toColumnExpr(EntityMetadata $entityMetadata, PropertyMetadata $propertyMetadata, IStorageReflection $storageReflection, string $alias) + private function toColumnExpr( + EntityMetadata $entityMetadata, + PropertyMetadata $propertyMetadata, + IStorageReflection $storageReflection, + string $alias + ) { if ($propertyMetadata->isPrimary && $propertyMetadata->isVirtual) { // primary-proxy $primaryKey = $entityMetadata->getPrimaryKey(); diff --git a/tests/cases/unit/Collection/ConditionParserHelperTest.phpt b/tests/cases/unit/Collection/ConditionParserHelperTest.phpt index 1ed391d6..fe9aa914 100644 --- a/tests/cases/unit/Collection/ConditionParserHelperTest.phpt +++ b/tests/cases/unit/Collection/ConditionParserHelperTest.phpt @@ -30,7 +30,6 @@ class ConditionParserHelperTest extends TestCase Assert::same(['this->column->name', '='], ConditionParserHelper::parsePropertyOperator('this->column->name')); Assert::same(['this->column->name', '!='], ConditionParserHelper::parsePropertyOperator('this->column->name!=')); - Assert::same(['Book->column->name', '='], ConditionParserHelper::parsePropertyOperator('Book->column->name')); Assert::same(['NextrasTests\Orm\Book->column', '='], ConditionParserHelper::parsePropertyOperator('NextrasTests\Orm\Book->column')); } @@ -39,7 +38,6 @@ class ConditionParserHelperTest extends TestCase { Assert::same([['column'], null], ConditionParserHelper::parsePropertyExpr('column')); Assert::same([['column', 'name'], null], ConditionParserHelper::parsePropertyExpr('this->column->name')); - Assert::same([['column', 'name'], 'Book'], ConditionParserHelper::parsePropertyExpr('Book->column->name')); Assert::same([['column'], Book::class], ConditionParserHelper::parsePropertyExpr('NextrasTests\Orm\Book->column')); } @@ -53,6 +51,10 @@ class ConditionParserHelperTest extends TestCase Assert::throws(function () { ConditionParserHelper::parsePropertyExpr('column.name'); }, InvalidArgumentException::class); + + Assert::throws(function () { + ConditionParserHelper::parsePropertyExpr('Book->column->name'); + }, InvalidArgumentException::class); } } From bc362fd1bae06603b05f29d0cb5c489b0ea152f3 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Sun, 29 Dec 2019 11:37:33 +0100 Subject: [PATCH 2/3] embeddables: implement ArrayCollection filtering support --- .../Helpers/ArrayCollectionHelper.php | 4 ++ src/Entity/Embeddable/EmbeddableContainer.php | 3 +- .../Collection/collection.embeddables.phpt | 39 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/cases/integration/Collection/collection.embeddables.phpt diff --git a/src/Collection/Helpers/ArrayCollectionHelper.php b/src/Collection/Helpers/ArrayCollectionHelper.php index 8126bdfe..c3d5222b 100644 --- a/src/Collection/Helpers/ArrayCollectionHelper.php +++ b/src/Collection/Helpers/ArrayCollectionHelper.php @@ -13,6 +13,7 @@ use DateTimeInterface; use Nette\Utils\Arrays; use Nextras\Orm\Collection\ICollection; +use Nextras\Orm\Entity\Embeddable\EmbeddableContainer; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\EntityMetadata; use Nextras\Orm\Entity\Reflection\PropertyMetadata; @@ -212,6 +213,9 @@ private function getValueByTokens(IEntity $entity, array $tokens, EntityMetadata } continue 2; } + } elseif ($propertyMeta->wrapper === EmbeddableContainer::class) { + \assert($propertyMeta->args !== null); + $entityMeta = $propertyMeta->args[EmbeddableContainer::class]['metadata']; } } while (count($tokens) > 0 && $value !== null); diff --git a/src/Entity/Embeddable/EmbeddableContainer.php b/src/Entity/Embeddable/EmbeddableContainer.php index 8e6a3564..e4bd8a2b 100644 --- a/src/Entity/Embeddable/EmbeddableContainer.php +++ b/src/Entity/Embeddable/EmbeddableContainer.php @@ -56,8 +56,7 @@ public function setPropertyEntity(IEntity $entity) public function convertToRawValue($value) { - // this flow path should not happened - throw new InvalidStateException(); + return $value; } diff --git a/tests/cases/integration/Collection/collection.embeddables.phpt b/tests/cases/integration/Collection/collection.embeddables.phpt new file mode 100644 index 00000000..98704bde --- /dev/null +++ b/tests/cases/integration/Collection/collection.embeddables.phpt @@ -0,0 +1,39 @@ +orm->books->findBy(['this->price->cents>=' => 100]); + Assert::same(0, $books1->count()); + Assert::same(0, $books1->countStored()); + + $book = $this->orm->books->getById(1); + $book->price = new Money(100, Currency::CZK()); + $this->orm->persistAndFlush($book); + + $books2 = $this->orm->books->findBy(['this->price->cents>=' => 100]); + Assert::same(1, $books2->count()); + Assert::same(1, $books2->countStored()); + } +} + + +$test = new CollectionEmbeddablesTest($dic); +$test->run(); From c4a327a7d630acb5abd0a7d6f55c8e32a19821d4 Mon Sep 17 00:00:00 2001 From: Jan Skrasek Date: Mon, 30 Dec 2019 19:14:41 +0100 Subject: [PATCH 3/3] embeddables: implement DbalCollection filtering support --- src/Mapper/Dbal/QueryBuilderHelper.php | 62 ++++++++++------- .../StorageReflection/StorageReflection.php | 69 +++++++++---------- tests/db/mssql-init.sql | 2 +- tests/db/mysql-init.sql | 2 +- tests/db/pgsql-init.sql | 2 +- tests/inc/model/book/BooksMapper.php | 8 +++ 6 files changed, 82 insertions(+), 63 deletions(-) diff --git a/src/Mapper/Dbal/QueryBuilderHelper.php b/src/Mapper/Dbal/QueryBuilderHelper.php index 0e86abd1..3a72b31e 100644 --- a/src/Mapper/Dbal/QueryBuilderHelper.php +++ b/src/Mapper/Dbal/QueryBuilderHelper.php @@ -12,6 +12,7 @@ use Nextras\Dbal\QueryBuilder\QueryBuilder; use Nextras\Orm\Collection\Helpers\ConditionParserHelper; use Nextras\Orm\Collection\ICollection; +use Nextras\Orm\Entity\Embeddable\EmbeddableContainer; use Nextras\Orm\Entity\Reflection\EntityMetadata; use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata as Relationship; @@ -26,7 +27,7 @@ /** - * QueryBuilder helper for Nextras\Dbal. + * QueryBuilder helper for Nextras Dbal. */ class QueryBuilderHelper { @@ -128,39 +129,51 @@ public function normalizeValue($value, ColumnReference $columnReference) */ private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): ColumnReference { - $lastToken = array_pop($tokens); + $lastToken = \array_pop($tokens); \assert($lastToken !== null); $currentMapper = $this->mapper; $currentAlias = $builder->getFromAlias(); $currentReflection = $currentMapper->getStorageReflection(); $currentEntityMetadata = $currentMapper->getRepository()->getEntityMetadata($sourceEntity); + $propertyPrefixTokens = ""; foreach ($tokens as $tokenIndex => $token) { $property = $currentEntityMetadata->getProperty($token); - if ($property->relationship === null) { - throw new InvalidArgumentException("Entity {$currentEntityMetadata->className}::\${$token} does not contain a relationship."); + if ($property->relationship !== null) { + [ + $currentAlias, + $currentReflection, + $currentEntityMetadata, + $currentMapper, + ] = $this->processRelationship( + $tokens, + $builder, + $property, + $currentReflection, + $currentMapper, + $currentAlias, + $token, + $tokenIndex + ); + } elseif ($property->wrapper === EmbeddableContainer::class) { + \assert($property->args !== null); + $currentEntityMetadata = $property->args[EmbeddableContainer::class]['metadata']; + $propertyPrefixTokens .= "$token->"; + } else { + throw new InvalidArgumentException("Entity {$currentEntityMetadata->className}::\${$token} does not contain a relationship or an embeddable."); } - - [ - $currentAlias, - $currentReflection, - $currentEntityMetadata, - $currentMapper, - ] = $this->processRelationship( - $tokens, - $builder, - $property, - $currentReflection, - $currentMapper, - $currentAlias, - $token, - $tokenIndex - ); } $propertyMetadata = $currentEntityMetadata->getProperty($lastToken); - $column = $this->toColumnExpr($currentEntityMetadata, $propertyMetadata, $currentReflection, $currentAlias); + $column = $this->toColumnExpr( + $currentEntityMetadata, + $propertyMetadata, + $currentReflection, + $currentAlias, + $propertyPrefixTokens + ); + return new ColumnReference($column, $propertyMetadata, $currentEntityMetadata, $currentReflection); } @@ -249,7 +262,8 @@ private function toColumnExpr( EntityMetadata $entityMetadata, PropertyMetadata $propertyMetadata, IStorageReflection $storageReflection, - string $alias + string $alias, + string $propertyPrefixTokens ) { if ($propertyMetadata->isPrimary && $propertyMetadata->isVirtual) { // primary-proxy @@ -257,7 +271,7 @@ private function toColumnExpr( if (count($primaryKey) > 1) { // composite primary key $pair = []; foreach ($primaryKey as $columnName) { - $columnName = $storageReflection->convertEntityToStorageKey($columnName); + $columnName = $storageReflection->convertEntityToStorageKey($propertyPrefixTokens . $columnName); $pair[] = "{$alias}.{$columnName}"; } return $pair; @@ -268,7 +282,7 @@ private function toColumnExpr( $propertyName = $propertyMetadata->name; } - $columnName = $storageReflection->convertEntityToStorageKey($propertyName); + $columnName = $storageReflection->convertEntityToStorageKey($propertyPrefixTokens . $propertyName); $columnExpr = "{$alias}.{$columnName}"; return $columnExpr; } diff --git a/src/Mapper/Dbal/StorageReflection/StorageReflection.php b/src/Mapper/Dbal/StorageReflection/StorageReflection.php index 1b185f60..75e12c42 100644 --- a/src/Mapper/Dbal/StorageReflection/StorageReflection.php +++ b/src/Mapper/Dbal/StorageReflection/StorageReflection.php @@ -26,8 +26,7 @@ abstract class StorageReflection implements IStorageReflection use SmartObject; const TO_STORAGE = 0; const TO_ENTITY = 1; - const TO_STORAGE_EXPANSION = 2; - const TO_ENTITY_COMPRESSION = 3; + const TO_STORAGE_FLATTENING = 2; /** @var string */ public $manyHasManyStorageNamePattern = '%s_x_%s'; @@ -110,14 +109,12 @@ public function convertEntityToStorage(array $in): array { $out = []; - if (isset($this->mappings[self::TO_STORAGE_EXPANSION])) { - foreach ($this->mappings[self::TO_STORAGE_EXPANSION] as $key => $maps) { - if (isset($in[$key]) || array_key_exists($key, $in)) { - foreach ($maps as $from => $to) { - $in[$to] = $in[$key][$from] ?? null; - } - unset($in[$key]); - } + if (isset($this->mappings[self::TO_STORAGE_FLATTENING])) { + foreach ($this->mappings[self::TO_STORAGE_FLATTENING] as $to => $from) { + $in[$to] = Arrays::get($in, $from, null); + } + foreach ($this->mappings[self::TO_STORAGE_FLATTENING] as $to => $from) { + unset($in[$from[0]]); } } @@ -148,16 +145,6 @@ public function convertStorageToEntity(array $in): array { $out = []; - if (isset($this->mappings[self::TO_ENTITY_COMPRESSION])) { - foreach ($this->mappings[self::TO_ENTITY_COMPRESSION] as $key => $path) { - if (isset($in[$key]) || array_key_exists($key, $in)) { - $ref = &Arrays::getRef($out, $path); - $ref = $in[$key]; - unset($in[$key]); - } - } - } - foreach ($in as $key => $val) { if (isset($this->mappings[self::TO_ENTITY][$key][0])) { $newKey = $this->mappings[self::TO_ENTITY][$key][0]; @@ -167,7 +154,12 @@ public function convertStorageToEntity(array $in): array if (isset($this->mappings[self::TO_ENTITY][$key][1])) { $converter = $this->mappings[self::TO_ENTITY][$key][1]; - $out[$newKey] = $converter($val, $newKey); + $val = $converter($val, $newKey); + } + + if (\stripos($newKey, '->') !== false) { + $ref = &Arrays::getRef($out, \explode('->', $newKey)); + $ref = $val; } else { $out[$newKey] = $val; } @@ -319,35 +311,40 @@ protected function getDefaultMappings(): array $mappings[self::TO_STORAGE][$entityKey] = [$storageKey, null]; } - /** @var array> $toProcess */ + /** @phpstan-var array> $toProcess */ $toProcess = [[$this->entityMetadata, []]]; - while (([$metadata, $path] = \array_shift($toProcess)) !== null) { + while (([$metadata, $tokens] = \array_shift($toProcess)) !== null) { foreach ($metadata->getProperties() as $property) { - if ($property->wrapper !== EmbeddableContainer::class) continue; + if ($property->wrapper !== EmbeddableContainer::class) { + continue; + } $subMetadata = $property->args[EmbeddableContainer::class]['metadata']; \assert($subMetadata instanceof EntityMetadata); - $map = []; - $path[] = $property->name; + $tokens[] = $property->name; foreach ($subMetadata->getProperties() as $subProperty) { - $propertyPath = $path; - $propertyPath[] = $subProperty->name; - $storageKey = \implode($this->embeddableSeparatorPattern, array_map(function($key) { - return $this->formatStorageKey($key); - }, $propertyPath)); + $propertyTokens = $tokens; + $propertyTokens[] = $subProperty->name; + + $propertyKey = \implode('->', $propertyTokens); + $storageKey = \implode( + $this->embeddableSeparatorPattern, + \array_map(function($key) { + return $this->formatStorageKey($key); + }, $propertyTokens) + ); - $mappings[self::TO_ENTITY_COMPRESSION][$storageKey] = $propertyPath; - $map[$subProperty->name] = $storageKey; + $mappings[self::TO_ENTITY][$storageKey] = [$propertyKey]; + $mappings[self::TO_STORAGE][$propertyKey] = [$storageKey]; + $mappings[self::TO_STORAGE_FLATTENING][$propertyKey] = $propertyTokens; if ($subProperty->wrapper === EmbeddableContainer::class) { \assert($subProperty->args !== null); - $toProcess[] = [$subProperty->args[EmbeddableContainer::class]['metadata'], $path]; + $toProcess[] = [$subProperty->args[EmbeddableContainer::class]['metadata'], $tokens]; } } - - $mappings[self::TO_STORAGE_EXPANSION][$property->name] = $map; } } diff --git a/tests/db/mssql-init.sql b/tests/db/mssql-init.sql index 34594e08..65a866e4 100644 --- a/tests/db/mssql-init.sql +++ b/tests/db/mssql-init.sql @@ -40,7 +40,7 @@ CREATE TABLE books ( published_at datetimeoffset NOT NULL, printed_at datetimeoffset, ean_id int, - price_cents int, + price int, price_currency char(3), PRIMARY KEY (id), CONSTRAINT books_authors FOREIGN KEY (author_id) REFERENCES authors (id), diff --git a/tests/db/mysql-init.sql b/tests/db/mysql-init.sql index e7a29a4c..2c53d5fa 100644 --- a/tests/db/mysql-init.sql +++ b/tests/db/mysql-init.sql @@ -40,7 +40,7 @@ CREATE TABLE books ( published_at DATETIME NOT NULL, printed_at DATETIME, ean_id int, - price_cents int, + price int, price_currency char(3), PRIMARY KEY (id), CONSTRAINT books_authors FOREIGN KEY (author_id) REFERENCES authors (id), diff --git a/tests/db/pgsql-init.sql b/tests/db/pgsql-init.sql index 4ce0fd93..7b969835 100644 --- a/tests/db/pgsql-init.sql +++ b/tests/db/pgsql-init.sql @@ -40,7 +40,7 @@ CREATE TABLE "books" ( "published_at" TIMESTAMP NOT NULL, "printed_at" TIMESTAMP, "ean_id" int, - "price_cents" int, + "price" int, "price_currency" char(3), PRIMARY KEY ("id"), CONSTRAINT "books_authors" FOREIGN KEY ("author_id") REFERENCES authors ("id"), diff --git a/tests/inc/model/book/BooksMapper.php b/tests/inc/model/book/BooksMapper.php index be2338f9..732e55e5 100644 --- a/tests/inc/model/book/BooksMapper.php +++ b/tests/inc/model/book/BooksMapper.php @@ -18,4 +18,12 @@ public function findFirstBook() { return $this->toEntity($this->builder()->where('id = 1')); } + + + protected function createStorageReflection() + { + $reflection = parent::createStorageReflection(); + $reflection->setMapping('price->cents', 'price'); + return $reflection; + } }