Skip to content

Commit

Permalink
Merge pull request #363 from nextras/embeddables_collection
Browse files Browse the repository at this point in the history
Embeddables collection filtering
  • Loading branch information
hrach committed Dec 30, 2019
2 parents 7555522 + c4a327a commit 4b8c7a4
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 118 deletions.
4 changes: 4 additions & 0 deletions src/Collection/Helpers/ArrayCollectionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
17 changes: 12 additions & 5 deletions src/Collection/Helpers/ConditionParserHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace Nextras\Orm\Collection\Helpers;

use Nextras\Orm\Entity\IEntity;
use Nextras\Orm\InvalidArgumentException;


Expand All @@ -24,25 +25,31 @@ 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];
}
}


/**
* @return array{array<string>, class-string<IEntity>|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];
Expand Down
3 changes: 1 addition & 2 deletions src/Entity/Embeddable/EmbeddableContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ public function setPropertyEntity(IEntity $entity)

public function convertToRawValue($value)
{
// this flow path should not happened
throw new InvalidStateException();
return $value;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@

interface IQueryBuilderFilterFunction
{
/**
* @param array<mixed> $args
* @return array<mixed> list of Nextras Dbal's condition fragments
*/
public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array;
}
3 changes: 3 additions & 0 deletions src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@

interface IQueryBuilderFunction
{
/**
* @param array<mixed> $args
*/
public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): QueryBuilder;
}
2 changes: 1 addition & 1 deletion src/Mapper/Dbal/Helpers/ColumnReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

class ColumnReference
{
/** @var string|array */
/** @var string|array<string> */
public $column;

/** @var PropertyMetadata */
Expand Down
201 changes: 132 additions & 69 deletions src/Mapper/Dbal/QueryBuilderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +27,7 @@


/**
* QueryBuilder helper for Nextras\Dbal.
* QueryBuilder helper for Nextras Dbal.
*/
class QueryBuilderHelper
{
Expand Down Expand Up @@ -63,7 +64,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);
Expand All @@ -75,7 +76,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);
Expand All @@ -84,14 +85,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);
}


Expand Down Expand Up @@ -129,86 +124,154 @@ public function normalizeValue($value, ColumnReference $columnReference)


/**
* @return array [IStorageReflection $sourceReflection, string $sourceAlias, EntityMetadata $sourceEntityMeta]
* @param array<string> $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);

foreach ($levels as $levelIndex => $level) {
$property = $sourceEntityMeta->getProperty($level);
if ($property->relationship === null) {
throw new InvalidArgumentException("Entity {$sourceEntityMeta->className}::\${$level} does not contain a relationship.");
$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) {
[
$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.");
}
}

$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);
}
$propertyMetadata = $currentEntityMetadata->getProperty($lastToken);
$column = $this->toColumnExpr(
$currentEntityMetadata,
$propertyMetadata,
$currentReflection,
$currentAlias,
$propertyPrefixTokens
);

$builder->leftJoin($sourceAlias, "[$joinTable]", self::getAlias($joinTable), "[$sourceAlias.$sourceColumn] = [$joinTable.$inColumn]");
return new ColumnReference($column, $propertyMetadata, $currentEntityMetadata, $currentReflection);
}


/**
* @param array<string> $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<string>
*/
private function toColumnExpr(EntityMetadata $entityMetadata, PropertyMetadata $propertyMetadata, IStorageReflection $storageReflection, string $alias)
private function toColumnExpr(
EntityMetadata $entityMetadata,
PropertyMetadata $propertyMetadata,
IStorageReflection $storageReflection,
string $alias,
string $propertyPrefixTokens
)
{
if ($propertyMetadata->isPrimary && $propertyMetadata->isVirtual) { // primary-proxy
$primaryKey = $entityMetadata->getPrimaryKey();
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;
Expand All @@ -219,7 +282,7 @@ private function toColumnExpr(EntityMetadata $entityMetadata, PropertyMetadata $
$propertyName = $propertyMetadata->name;
}

$columnName = $storageReflection->convertEntityToStorageKey($propertyName);
$columnName = $storageReflection->convertEntityToStorageKey($propertyPrefixTokens . $propertyName);
$columnExpr = "{$alias}.{$columnName}";
return $columnExpr;
}
Expand Down

0 comments on commit 4b8c7a4

Please sign in to comment.