diff --git a/doc/collection-functions.texy b/doc/collection-functions.texy index fb162b96..775688a6 100644 --- a/doc/collection-functions.texy +++ b/doc/collection-functions.texy @@ -1,51 +1,33 @@ Collection Functions #################### -Collection custom functions are a powerful extension point that will allow you to write a custom collection filtering or sorting behavior. +Collection functions are a powerful extension point that will allow you to write a custom filtering or ordering behavior. -First, you have to define if you want to write "filter function" or just a plain "function". - -- **filter functions** - - easy to write - - can be nested - - limited functionality - - `Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFilterFunction` - - `Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFilterFunction` -- **functions** - - difficult to write - - cannot be nested - - unlimited functionality - - `Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFunction` - - `Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFunction` - -Custom functions are defined as classes implementing specific interfaces. Orm comes with two sets of interfaces: Dbal interfaces and Array interfaces. +To implement custom filtering or ordering, you have do write your implementation. Ideally, you write it for both - Dbal & Array, because you may need it in both cases (e.g. on persisted / unpersisted relationship collection). Both of the Dbal & Array implementation brings their own interface you have to implement in your collection functions. /--div .[note] **Why we have ArrayCollection and DbalCollection?** - Collection itself is independent from storage implementation, it's your choice if your custom function will work in both cases - for `ArrayCollection` and `DbalCollection`. Let's remind you, ArrayCollections are commonly used in relationships, when you set new entities into the relationship, until the relationship is persisted, you will use an ArrayCollection. -\-- - -Filtering functions can be used in `ICollection::findBy()` method. The expression is similar to using `OR` function (which is internally implemented as a custom function). Pass the function identifier (we recommend function's class name) and then function's arguments to apply the function. - -/--php -$collection->findBy([FilterFunction::class, 'arg1', 'arg2']); - -// or nested -$collection->findBy([ - ICollection::OR, - [FilterFunction::class, 'arg1', 'arg2'], - [AnotherFunction::class, 'arg3'], -]); + Collection itself is independent from storage implementation. It is your choice if your collection function will work in both cases - for `ArrayCollection` and `DbalCollection`. Let's remind you, `ArrayCollection`s are commonly used in relationships when you set new entities into the relationship but until the relationship is persisted, you will work on an `ArrayCollection`. \-- -The plain functions must be applied by `ICollection::applyFunction()` method. +Collection functions can be used in `ICollection::findBy()` or `ICollection::orderBy()` method. The basic filtering is also done through collection functions so you can reuse it in your collection function's composition. First, pass the function identifier (we recommend function's class name) and then function's arguments as an array argument. /--php -$collection->applyFunction(Function::class, 'arg1', 'arg2'); +// use directly a function call definition +$collection->findBy([MyFunction::class, 'arg1', 'arg2']); + +// or compose & nest them together +$collection->findBy( + [ + ICollection::OR, + [MyFunction::class, 'arg1', 'arg2'], + [AnotherFunction::class, 'arg3'], + ] +); \-- -Functions are registered per Repository. To do so override `Repository::createCollectionFunction($name)` method to return your custom function instances. +Functions are registered per repository. To do so, override `Repository::createCollectionFunction($name)` method to return your collection functions' instances. /--php class UsersRepository extends Nextras\Orm\Repository\Repository @@ -53,8 +35,8 @@ class UsersRepository extends Nextras\Orm\Repository\Repository // ... public function createCollectionFunction(string $name) { - if ($name === MyCustomFunction::class) { - return new MyCustomFUnction(); + if ($name === MyFunction::class) { + return new MyFunction(); } else { return parent::createCollectionFunction($name); } @@ -62,50 +44,83 @@ class UsersRepository extends Nextras\Orm\Repository\Repository } \-- -Like Filtering Function -======================= +Dbal Functions +============== -Let's create "LIKE" filtering function. Filtering by like expression is a little bit different in each database engine. +To implement Dbal's collection function your class has to implement `Netras\Orm\Collection\Functions\IQueryBuilderFunction` interface. -.[note] -PostgreSQL is case-sensitive, so you should apply lower function & functional index; These modifications are case-specific, therefore the LIKE functionality is not provided in Orm by default. +The only required method takes: +- DbalQueryBuilderHelper for easier user input processing, +- QueryBuilder for creating table joins, +- user input/function parameters. + +Collection function has to return `DbalExpressionResult` object. This objects holds bits of SQL clauses which may be processed by Netras Dbal's SqlProcessor. Because you are not adding filters directly to QueryBuilder but rather return them, you may compose multiple regular and custom collection functions together. + +Let's see an example: a "Like" collection function; We want to compare any (property) expression through SQL's LIKE operator to a user-input value. + +/--php +$users->findBy( + [LikeFunction::class, 'phone', '+420'] +); +\-- + +In the example we would like to use LikeFunction to filter users by their phones, which starts with `+420` prefix. Our function will implement IQueryBuilderFunction interface and will receive `$args` witch `phone` and `+420` user inputs. But, the first argument may be quite dynamic. What if user pass `address->zipcode` (e.g. a relationship expression) instead of simple `phone`, such expression would require table joins, and doing it all by hand would be difficult. Therefore Orm always pass a DbalQueryBuilderHelper which will handle this for you, even in the simple case. Use `processPropertyExpr` method to obtain a DbalResultExpression for `phone` argument. Then just append needed SQL to the expression, e.g. LIKE operator with an Dbal's argument. That's all! /--php -use Nette\Utils\Strings; use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; -use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFilterFunction; -use Nextras\Orm\Mapper\Dbal\QueryBuilderHelper; -use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFilterFunction; +use Nextras\Orm\Collection\Helpers\DbalExpressionResult; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; -final class LikeFilterFunction implements IArrayFilterFunction, IQueryBuilderFilterFunction +final class LikeFunction implements IQueryBuilderFunction { - public function processArrayFilter(ArrayCollectionHelper $helper, IEntity $entity, array $args): bool + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult { - // check if we received enough arguments - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); + \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1])); - // get the value and check if it starts with the requested string - $value = $helper->getValue($entity, $args[0])->value; - return Strings::startsWith($value, $args[1]); + $expression = $helper->processPropertyExpr($builder, $args[0]); + return $expression->append('LIKE %like_', $args[1]); } +} +\-- +The value that is processed by helper may not be just a column, but another expression returned from other collection function. - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array - { - // check if we received enough arguments - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); +Array Functions +=============== - // convert expression to column name (also this autojoins needed tables) - $column = $helper->processPropertyExpr($builder, $args[0])->column; - return ['%column LIKE %like_', $column, $args[1]]; - } -} -\-- +Array collection functions implements `Netras\Orm\Collection\Functions\IArrayFunction` interface. It is different to Dbal's interface, because the filtering now happens directly in PHP. + +The only required method takes: +- ArrayCollectionHelper for easier entity property processing, +- IEntity entity to check if should (not) be filtered out, +- user input/function parameters. -In the example we implement LIKE (with placeholder at the end) both for Array & Dbal collection. As you can see, you can write custom SQL as well as you can filter the entity by any PHP code. The final usage is quite simple. +Array collection functions returns mixed value, it depends in which context they will be evaluated. In filtering context the value will be interpreted as "true-thy" to indicate if the entity should be filtered out; in ordering context the value will be used for comparison of two entities. + +Let's see an example: a "Like" collection function; We want to compare any (property) expression to passed user-input value with prefix comparison. + +.[note] +PostgreSQL is case-sensitive, so you should apply the lower function & a functional index; These modifications are case-specific, therefore the LIKE functionality is not provided in Orm by default. + +Our function will implement `IArrayFunction` and will receive helper objects & user-input. The same as in Dbal's example, the user property argument may vary from simple property access to traversing through relationship. Let's use helper to get the property expression value holder, from which we will obtain the specific value. Then we simply compare it with user-input argument by Nette's string helper. /--php -$users->findBy([LikeFilterFunction::class, 'name', 'Jon']); +use Nette\Utils\Strings; +use Nextras\Orm\Collection\Functions\IArrayFunction; +use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; +use Nextras\Orm\Entity\IEntity; + +final class LikeFunction implements IArrayFunction +{ + public function processArrayExpression(ArrayCollectionHelper $helper, IEntity $entity, array $args) + { + \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1])); + + $value = $helper->getValue($entity, $args[0])->value; + return Strings::startsWith($value, $args[1]); + } \-- diff --git a/src/Collection/ArrayCollection.php b/src/Collection/ArrayCollection.php index cd0b3f5d..a9e70915 100644 --- a/src/Collection/ArrayCollection.php +++ b/src/Collection/ArrayCollection.php @@ -107,19 +107,15 @@ public function findBy(array $where): ICollection } - public function orderBy(string $propertyPath, string $direction = self::ASC): ICollection + public function orderBy($expression, string $direction = self::ASC): ICollection { $collection = clone $this; - $collection->collectionSorter[] = [$propertyPath, $direction]; - return $collection; - } - - - public function orderByMultiple(array $properties): ICollection - { - $collection = clone $this; - foreach ($properties as $property => $direction) { - $collection->collectionSorter[] = [$property, $direction]; + if (is_array($expression) && !isset($expression[0])) { + foreach ($expression as $subExpression => $subDirection) { + $collection->collectionSorter[] = [$subExpression, $subDirection]; + } + } else { + $collection->collectionSorter[] = [$expression, $direction]; } return $collection; } @@ -141,14 +137,6 @@ public function limitBy(int $limit, int $offset = null): ICollection } - public function applyFunction(string $functionName, ...$args): ICollection - { - $collection = clone $this; - $collection->collectionFunctions[] = $this->getHelper()->createFunction($functionName, $args); - return $collection; - } - - public function fetch(): ?IEntity { if (!$this->fetchIterator) { @@ -190,7 +178,6 @@ public function getIterator(): Iterator $collection->relationshipMapper = null; $collection->relationshipParent = null; $entityIterator = $this->relationshipMapper->getIterator($this->relationshipParent, $collection); - } else { $this->processData(); $entityIterator = new EntityIterator(array_values($this->data)); diff --git a/src/Collection/Collection.php b/src/Collection/Collection.php deleted file mode 100644 index a630f43f..00000000 --- a/src/Collection/Collection.php +++ /dev/null @@ -1,16 +0,0 @@ -getHelper()->processFilterFunction($collection->queryBuilder, $where); - $collection->queryBuilder->andWhere(...$filterArgs); + $expression = $collection->getHelper()->processFilterFunction($collection->queryBuilder, $where); + if ($expression->isHavingClause) { + $collection->queryBuilder->andHaving(...$expression->args); + } else { + $collection->queryBuilder->andWhere(...$expression->args); + } return $collection; } - public function orderBy(string $propertyPath, string $direction = ICollection::ASC): ICollection - { - return $this->orderByMultiple([$propertyPath => $direction]); - } - - - public function orderByMultiple(array $properties): ICollection + public function orderBy($expression, string $direction = ICollection::ASC): ICollection { $collection = clone $this; - $helper = $collection->getHelper(); - $builder = $collection->queryBuilder; - foreach ($properties as $propertyPath => $direction) { - $helper->processOrder($builder, $propertyPath, $direction); + if (is_array($expression)) { + if (!isset($expression[0])) { + foreach ($expression as $subExpression => $subDirection) { + $orderArgs = $collection->getHelper()->processOrder($collection->queryBuilder, $subExpression, $subDirection); + $collection->queryBuilder->addOrderBy('%ex', $orderArgs); + } + } else { + $orderArgs = $collection->getHelper()->processOrder($collection->queryBuilder, $expression, $direction); + $collection->queryBuilder->addOrderBy('%ex', $orderArgs); + } + } else { + $orderArgs = $collection->getHelper()->processOrder($collection->queryBuilder, $expression, $direction); + $collection->queryBuilder->addOrderBy('%ex', $orderArgs); } return $collection; } @@ -142,18 +149,6 @@ public function limitBy(int $limit, int $offset = null): ICollection } - public function applyFunction(string $functionName, ...$args): ICollection - { - $collection = clone $this; - $collection->queryBuilder = $collection->getHelper()->processApplyFunction( - $collection->queryBuilder, - $functionName, - $args - ); - return $collection; - } - - public function fetch(): ?IEntity { if (!$this->fetchIterator) { @@ -322,7 +317,7 @@ protected function getHelper() { if ($this->helper === null) { $repository = $this->mapper->getRepository(); - $this->helper = new QueryBuilderHelper($repository->getModel(), $repository, $this->mapper); + $this->helper = new DbalQueryBuilderHelper($repository->getModel(), $repository, $this->mapper); } return $this->helper; diff --git a/src/Collection/EmptyCollection.php b/src/Collection/EmptyCollection.php index 3693b1fc..049f8f08 100644 --- a/src/Collection/EmptyCollection.php +++ b/src/Collection/EmptyCollection.php @@ -15,7 +15,7 @@ use Nextras\Orm\NoResultException; -final class EmptyCollection implements ICollection +class EmptyCollection implements ICollection { /** @var IRelationshipMapper|null */ private $relationshipMapper; @@ -51,13 +51,7 @@ public function findBy(array $where): ICollection } - public function orderBy(string $propertyPath, string $direction = self::ASC): ICollection - { - return clone $this; - } - - - public function orderByMultiple(array $properties): ICollection + public function orderBy($propertyPath, string $direction = self::ASC): ICollection { return clone $this; } @@ -75,12 +69,6 @@ public function limitBy(int $limit, int $offset = null): ICollection } - public function applyFunction(string $functionName, ...$args): ICollection - { - return clone $this; - } - - public function fetch(): ?IEntity { return null; diff --git a/src/Collection/Functions/ConjunctionOperatorFunction.php b/src/Collection/Functions/ConjunctionOperatorFunction.php new file mode 100644 index 00000000..14e6a3c8 --- /dev/null +++ b/src/Collection/Functions/ConjunctionOperatorFunction.php @@ -0,0 +1,68 @@ +normalizeFunctions($args) as $arg) { + $callback = $helper->createFilter($arg); + if (!$callback($entity)) { + return false; + } + } + return true; + } + + + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult + { + $isHavingClause = false; + $processedArgs = []; + foreach ($this->normalizeFunctions($args) as $collectionFunctionArgs) { + $expression = $helper->processFilterFunction($builder, $collectionFunctionArgs); + $processedArgs[] = $expression->args; + $isHavingClause = $isHavingClause || $expression->isHavingClause; + } + return new DbalExpressionResult(['%and', $processedArgs], $isHavingClause); + } + + + /** + * Normalize directly entered column => value expression to collection call definition array. + * @param array $args + * @return array + */ + private function normalizeFunctions(array $args): array + { + if (isset($args[0])) { + return $args; + } + + $processedArgs = []; + foreach ($args as $argName => $argValue) { + [$argName, $operator] = ConditionParserHelper::parsePropertyOperator($argName); + $processedArgs[] = [ValueOperatorFunction::class, $operator, $argName, $argValue]; + } + return $processedArgs; + } +} diff --git a/src/Collection/Functions/DisjunctionOperatorFunction.php b/src/Collection/Functions/DisjunctionOperatorFunction.php new file mode 100644 index 00000000..9aedbf6c --- /dev/null +++ b/src/Collection/Functions/DisjunctionOperatorFunction.php @@ -0,0 +1,68 @@ +normalizeFunctions($args) as $arg) { + $callback = $helper->createFilter($arg); + if ($callback($entity)) { + return true; + } + } + return false; + } + + + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult + { + $isHavingClause = false; + $processedArgs = []; + foreach ($this->normalizeFunctions($args) as $collectionFunctionArgs) { + $expression = $helper->processFilterFunction($builder, $collectionFunctionArgs); + $processedArgs[] = $expression->args; + $isHavingClause = $isHavingClause || $expression->isHavingClause; + } + return new DbalExpressionResult(['%or', $processedArgs], $isHavingClause); + } + + + /** + * Normalize directly entered column => value expression to collection call definition array. + * @param array $args + * @return array + */ + private function normalizeFunctions(array $args): array + { + if (isset($args[0])) { + return $args; + } + + $processedArgs = []; + foreach ($args as $argName => $argValue) { + [$argName, $operator] = ConditionParserHelper::parsePropertyOperator($argName); + $processedArgs[] = [ValueOperatorFunction::class, $operator, $argName, $argValue]; + } + return $processedArgs; + } +} diff --git a/src/Collection/Functions/IArrayFunction.php b/src/Collection/Functions/IArrayFunction.php new file mode 100644 index 00000000..dee1d4cc --- /dev/null +++ b/src/Collection/Functions/IArrayFunction.php @@ -0,0 +1,24 @@ + $args + * @return mixed + */ + public function processArrayExpression(ArrayCollectionHelper $helper, IEntity $entity, array $args); +} diff --git a/src/Collection/Functions/IQueryBuilderFunction.php b/src/Collection/Functions/IQueryBuilderFunction.php new file mode 100644 index 00000000..cca679f4 --- /dev/null +++ b/src/Collection/Functions/IQueryBuilderFunction.php @@ -0,0 +1,28 @@ + $args + */ + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult; +} diff --git a/src/Collection/Functions/ValueOperatorFunction.php b/src/Collection/Functions/ValueOperatorFunction.php new file mode 100644 index 00000000..b5e4faca --- /dev/null +++ b/src/Collection/Functions/ValueOperatorFunction.php @@ -0,0 +1,156 @@ +getValue($entity, $args[1]); + if ($valueReference === null) { + return false; + } + + if ($valueReference->propertyMetadata !== null) { + $targetValue = $helper->normalizeValue($args[2], $valueReference->propertyMetadata, true); + } else { + $targetValue = $args[2]; + } + + if ($valueReference->isMultiValue) { + foreach ($valueReference->value as $subValue) { + if ($this->arrayEvaluate($operator, $targetValue, $subValue)) { + return true; + } + } + return false; + } else { + return $this->arrayEvaluate($operator, $targetValue, $valueReference->value); + } + } + + + private function arrayEvaluate(string $operator, $targetValue, $sourceValue): bool + { + if ($operator === ConditionParserHelper::OPERATOR_EQUAL) { + if (is_array($targetValue)) { + return in_array($sourceValue, $targetValue, true); + } else { + return $sourceValue === $targetValue; + } + } elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) { + if (is_array($targetValue)) { + return !in_array($sourceValue, $targetValue, true); + } else { + return $sourceValue !== $targetValue; + } + } elseif ($operator === ConditionParserHelper::OPERATOR_GREATER) { + return $sourceValue > $targetValue; + } elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_GREATER) { + return $sourceValue >= $targetValue; + } elseif ($operator === ConditionParserHelper::OPERATOR_SMALLER) { + return $sourceValue < $targetValue; + } elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_SMALLER) { + return $sourceValue <= $targetValue; + } else { + throw new InvalidArgumentException(); + } + } + + + /** + * @param array $args + */ + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult + { + \assert(\count($args) === 3); + + $operator = $args[0]; + $expression = $helper->processPropertyExpr($builder, $args[1]); + + if ($expression->valueNormalizer !== null) { + $cb = $expression->valueNormalizer; + $value = $cb($args[2]); + } else { + $value = $args[2]; + } + + // extract column names for multiOr simplification + $eArgs = $expression->args; + if ( + \count($eArgs) === 2 + && $eArgs[0] === '%column' + && \is_array($eArgs[1]) + && \is_string($eArgs[1][0]) + ) { + $columns = $eArgs[1]; + } else { + $columns = null; + } + + if ($operator === ConditionParserHelper::OPERATOR_EQUAL) { + if (\is_array($value)) { + if ($value) { + if ($columns !== null) { + $value = \array_map(function ($value) use ($columns) { + return \array_combine($columns, $value); + }, $value); + return new DbalExpressionResult(['%multiOr', $value], $expression->isHavingClause); + } else { + return $expression->append('IN %any', $value); + } + } else { + return new DbalExpressionResult(['1=0'], $expression->isHavingClause); + } + } elseif ($value === null) { + return $expression->append('IS NULL'); + } else { + return $expression->append('= %any', $value); + } + + } elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) { + if (\is_array($value)) { + if ($value) { + if ($columns !== null) { + $value = \array_map(function ($value) use ($columns) { + return \array_combine($columns, $value); + }, $value); + return new DbalExpressionResult(['NOT (%multiOr)', $value], $expression->isHavingClause); + } else { + return $expression->append('NOT IN %any', $value); + } + } else { + return new DbalExpressionResult(['1=1'], $expression->isHavingClause); + } + } elseif ($value === null) { + return $expression->append('IS NOT NULL'); + } else { + return $expression->append('!= %any', $value); + } + + } else { + return $expression->append("$operator %any", $value); + } + } +} diff --git a/src/Collection/Helpers/ArrayCollectionHelper.php b/src/Collection/Helpers/ArrayCollectionHelper.php index 5bf9b178..1b3c2f82 100644 --- a/src/Collection/Helpers/ArrayCollectionHelper.php +++ b/src/Collection/Helpers/ArrayCollectionHelper.php @@ -12,6 +12,7 @@ use DateTimeImmutable; use DateTimeInterface; use Nette\Utils\Arrays; +use Nextras\Orm\Collection\Functions\IArrayFunction; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\Embeddable\EmbeddableContainer; use Nextras\Orm\Entity\IEntity; @@ -21,8 +22,6 @@ use Nextras\Orm\InvalidArgumentException; use Nextras\Orm\InvalidStateException; use Nextras\Orm\Mapper\IMapper; -use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFilterFunction; -use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFunction; use Nextras\Orm\Repository\IRepository; @@ -42,66 +41,71 @@ public function __construct(IRepository $repository) } - public function createFunction(string $function, array $expr): Closure - { - $customFunction = $this->repository->getCollectionFunction($function); - if (!$customFunction instanceof IArrayFunction) { - throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFunction interface."); - } - - return function (array $entities) use ($customFunction, $expr) { - /** @var IEntity[] $entities */ - return $customFunction->processArrayFilter($this, $entities, $expr); - }; - } - - public function createFilter(array $expr): Closure { $function = isset($expr[0]) ? array_shift($expr) : ICollection::AND; $customFunction = $this->repository->getCollectionFunction($function); - - if (!$customFunction instanceof IArrayFilterFunction) { - throw new InvalidStateException("Custom function $function has to implement IQueryBuilderFilterFunction interface."); + if (!$customFunction instanceof IArrayFunction) { + throw new InvalidStateException("Collection function $function has to implement " . IArrayFunction::class . ' interface.'); } return function (IEntity $entity) use ($customFunction, $expr) { - return $customFunction->processArrayFilter($this, $entity, $expr); + return $customFunction->processArrayExpression($this, $entity, $expr); }; } - public function createSorter(array $conditions): Closure + public function createSorter(array $expressions): Closure { - $columns = []; - foreach ($conditions as $pair) { - [$column, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($pair[0]); - $sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntity); - $columns[] = [$column, $pair[1], $sourceEntityMeta]; + $parsedExpressions = []; + foreach ($expressions as $expression) { + if (is_array($expression[0])) { + if (!isset($expression[0][0])) { + throw new InvalidArgumentException(); + } + $function = array_shift($expression[0]); + $collectionFunction = $this->repository->getCollectionFunction($function); + if (!$collectionFunction instanceof IArrayFunction) { + throw new InvalidStateException("Collection function $function has to implement " . IArrayFunction::class . ' interface.'); + } + $parsedExpressions[] = [$collectionFunction, $expression[1], $expression[0]]; + } else { + [$column, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($expression[0]); + $sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntity); + $parsedExpressions[] = [$column, $expression[1], $sourceEntityMeta]; + } } - return function ($a, $b) use ($columns) { - foreach ($columns as $pair) { - $a_ref = $this->getValueByTokens($a, $pair[0], $pair[2]); - $b_ref = $this->getValueByTokens($b, $pair[0], $pair[2]); - if ($a_ref === null || $b_ref === null) { - throw new InvalidStateException('Comparing entities that should not be included in the result. Possible missing filtering configuration for required entity type based on Single Table Inheritance.'); + return function ($a, $b) use ($parsedExpressions) { + foreach ($parsedExpressions as $expression) { + if ($expression[0] instanceof IArrayFunction) { + \assert(\is_array($expression[2])); + $_a = $expression[0]->processArrayExpression($this, $a, $expression[2]); + $_b = $expression[0]->processArrayExpression($this, $b, $expression[2]); + } else { + \assert($expression[2] instanceof EntityMetadata); + $a_ref = $this->getValueByTokens($a, $expression[0], $expression[2]); + $b_ref = $this->getValueByTokens($b, $expression[0], $expression[2]); + if ($a_ref === null || $b_ref === null) { + throw new InvalidStateException('Comparing entities that should not be included in the result. Possible missing filtering configuration for required entity type based on Single Table Inheritance.'); + } + $_a = $a_ref->value; + $_b = $b_ref->value; } - $_a = $a_ref->value; - $_b = $b_ref->value; - $direction = ($pair[1] === ICollection::ASC || $pair[1] === ICollection::ASC_NULLS_FIRST || $pair[1] === ICollection::ASC_NULLS_LAST) ? 1 : -1; + $ordering = $expression[1]; + $descReverse = ($ordering === ICollection::ASC || $ordering === ICollection::ASC_NULLS_FIRST || $ordering === ICollection::ASC_NULLS_LAST) ? 1 : -1; if ($_a === null || $_b === null) { // By default, <=> sorts nulls at the beginning. - $nullsDirection = $pair[1] === ICollection::ASC_NULLS_FIRST || $pair[1] === ICollection::DESC_NULLS_FIRST ? 1 : -1; - $result = ($_a <=> $_b) * $nullsDirection; + $nullsReverse = $ordering === ICollection::ASC_NULLS_FIRST || $ordering === ICollection::DESC_NULLS_FIRST ? 1 : -1; + $result = ($_a <=> $_b) * $nullsReverse; } elseif (is_int($_a) || is_float($_a) || is_int($_b) || is_float($_b)) { - $result = ($_a <=> $_b) * $direction; + $result = ($_a <=> $_b) * $descReverse; } else { - $result = ((string) $_a <=> (string) $_b) * $direction; + $result = ((string) $_a <=> (string) $_b) * $descReverse; } if ($result !== 0) { @@ -116,9 +120,20 @@ public function createSorter(array $conditions): Closure /** * Returns value reference, returns null when entity should not be evaluated at all because of STI condition. + * @param string|array $expr */ - public function getValue(IEntity $entity, string $expr): ?ValueReference + public function getValue(IEntity $entity, $expr): ?ArrayPropertyValueReference { + if (is_array($expr)) { + $function = array_shift($expr); + $collectionFunction = $this->repository->getCollectionFunction($function); + if (!$collectionFunction instanceof IArrayFunction) { + throw new InvalidStateException("Collection function $function has to implement " . IArrayFunction::class . ' interface.'); + } + $value = $collectionFunction->processArrayExpression($this, $entity, $expr); + return new ArrayPropertyValueReference($value, false, null); + } + [$tokens, $sourceEntityClassName] = ConditionParserHelper::parsePropertyExpr($expr); $sourceEntityMeta = $this->repository->getEntityMetadata($sourceEntityClassName); return $this->getValueByTokens($entity, $tokens, $sourceEntityMeta); @@ -173,7 +188,7 @@ public function normalizeValue($value, PropertyMetadata $propertyMetadata, bool /** * @param string[] $tokens */ - private function getValueByTokens(IEntity $entity, array $tokens, EntityMetadata $sourceEntityMeta): ?ValueReference + private function getValueByTokens(IEntity $entity, array $tokens, EntityMetadata $sourceEntityMeta): ?ArrayPropertyValueReference { if (!$entity instanceof $sourceEntityMeta->className) { return null; @@ -220,7 +235,7 @@ private function getValueByTokens(IEntity $entity, array $tokens, EntityMetadata $values[] = $this->normalizeValue($value, $propertyMeta, false); } while (!empty($stack)); - return new ValueReference( + return new ArrayPropertyValueReference( $isMultiValue ? $values : $values[0], $isMultiValue, $propertyMeta diff --git a/src/Collection/Helpers/ValueReference.php b/src/Collection/Helpers/ArrayPropertyValueReference.php similarity index 74% rename from src/Collection/Helpers/ValueReference.php rename to src/Collection/Helpers/ArrayPropertyValueReference.php index 8259285f..5544dd32 100644 --- a/src/Collection/Helpers/ValueReference.php +++ b/src/Collection/Helpers/ArrayPropertyValueReference.php @@ -11,7 +11,7 @@ use Nextras\Orm\Entity\Reflection\PropertyMetadata; -class ValueReference +class ArrayPropertyValueReference { /** @var mixed */ public $value; @@ -19,11 +19,11 @@ class ValueReference /** @var bool */ public $isMultiValue; - /** @var PropertyMetadata */ + /** @var PropertyMetadata|null */ public $propertyMetadata; - public function __construct($value, bool $isMultiValue, PropertyMetadata $propertyMetadata) + public function __construct($value, bool $isMultiValue, ?PropertyMetadata $propertyMetadata) { $this->value = $value; $this->isMultiValue = $isMultiValue; diff --git a/src/Collection/Helpers/DbalExpressionResult.php b/src/Collection/Helpers/DbalExpressionResult.php new file mode 100644 index 00000000..28e28efa --- /dev/null +++ b/src/Collection/Helpers/DbalExpressionResult.php @@ -0,0 +1,45 @@ + */ + public $args; + + /** @var bool */ + public $isHavingClause; + + /** @var (callable(mixed): mixed)|null */ + public $valueNormalizer; + + + /** + * @param array $args + */ + public function __construct( + array $args, + bool $isHavingClause = false, + ?callable $valueNormalizer = null + ) + { + $this->args = $args; + $this->isHavingClause = $isHavingClause; + $this->valueNormalizer = $valueNormalizer; + } + + + public function append(string $expression, ...$args): DbalExpressionResult + { + \array_unshift($args, $this->args); + \array_unshift($args, "%ex $expression"); + return new DbalExpressionResult($args, $this->isHavingClause); + } +} diff --git a/src/Mapper/Dbal/QueryBuilderHelper.php b/src/Collection/Helpers/DbalQueryBuilderHelper.php similarity index 72% rename from src/Mapper/Dbal/QueryBuilderHelper.php rename to src/Collection/Helpers/DbalQueryBuilderHelper.php index d0445d79..0dc2e60e 100644 --- a/src/Mapper/Dbal/QueryBuilderHelper.php +++ b/src/Collection/Helpers/DbalQueryBuilderHelper.php @@ -6,22 +6,19 @@ * @link https://github.com/nextras/orm */ -namespace Nextras\Orm\Mapper\Dbal; +namespace Nextras\Orm\Collection\Helpers; use Nette\Utils\Arrays; use Nextras\Dbal\QueryBuilder\QueryBuilder; -use Nextras\Orm\Collection\Helpers\ConditionParserHelper; +use Nextras\Orm\Collection\Functions\IQueryBuilderFunction; 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; use Nextras\Orm\InvalidArgumentException; -use Nextras\Orm\InvalidStateException; use Nextras\Orm\Mapper\Dbal\Conventions\IConventions; -use Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFilterFunction; -use Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFunction; -use Nextras\Orm\Mapper\Dbal\Helpers\ColumnReference; +use Nextras\Orm\Mapper\Dbal\DbalMapper; use Nextras\Orm\Model\IModel; use Nextras\Orm\NotSupportedException; use Nextras\Orm\Repository\IRepository; @@ -30,7 +27,7 @@ /** * QueryBuilder helper for Nextras Dbal. */ -class QueryBuilderHelper +class DbalQueryBuilderHelper { /** @var IModel */ private $model; @@ -65,82 +62,102 @@ public function __construct(IModel $model, IRepository $repository, DbalMapper $ } - public function processApplyFunction(QueryBuilder $builder, string $function, array $expr): QueryBuilder + /** + * @param string|array $expr + */ + public function processPropertyExpr(QueryBuilder $builder, $expr): DbalExpressionResult { - $customFunction = $this->repository->getCollectionFunction($function); - if (!$customFunction instanceof IQueryBuilderFunction) { - throw new InvalidStateException("Custom function $function has to implement " . IQueryBuilderFunction::class . ' interface.'); + if (\is_array($expr)) { + $function = \array_shift($expr); + $collectionFunction = $this->repository->getCollectionFunction($function); + if (!$collectionFunction instanceof IQueryBuilderFunction) { + throw new InvalidArgumentException("Collection function $function has to implement " . IQueryBuilderFunction::class . ' interface.'); + } + return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr); } - return $customFunction->processQueryBuilderFilter($this, $builder, $expr); + [$tokens, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($expr); + return $this->processTokens($tokens, $sourceEntity, $builder); } - public function processFilterFunction(QueryBuilder $builder, array $expr): array + /** + * @param array $expr + */ + public function processFilterFunction(QueryBuilder $builder, array $expr): DbalExpressionResult { - $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::class . ' interface.'); + $function = isset($expr[0]) ? \array_shift($expr) : ICollection::AND; + $collectionFunction = $this->repository->getCollectionFunction($function); + if (!$collectionFunction instanceof IQueryBuilderFunction) { + throw new InvalidArgumentException("Collection function $function has to implement " . IQueryBuilderFunction::class . ' interface.'); } - - return $customFunction->processQueryBuilderFilter($this, $builder, $expr); + return $collectionFunction->processQueryBuilderExpression($this, $builder, $expr); } - public function processPropertyExpr(QueryBuilder $builder, string $propertyExpr): ColumnReference + /** + * @param string|array $expr + * @return array + */ + public function processOrder(QueryBuilder $builder, $expr, string $direction): array { - [$tokens, $sourceEntity] = ConditionParserHelper::parsePropertyExpr($propertyExpr); - return $this->processTokens($tokens, $sourceEntity, $builder); + $columnReference = $this->processPropertyExpr($builder, $expr); + return $this->processOrderDirection($columnReference, $direction); } - public function processOrder(QueryBuilder $builder, string $propertyExpr, string $direction) + /** + * @return array + */ + private function processOrderDirection(DbalExpressionResult $expression, string $direction): array { - $property = $this->processPropertyExpr($builder, $propertyExpr)->column; if ($this->platformName === 'mysql') { if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) { - $builder->addOrderBy('%column ASC', $property); + return ['%ex ASC', $expression->args]; } elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) { - $builder->addOrderBy('%column DESC', $property); + return ['%ex DESC', $expression->args]; } elseif ($direction === ICollection::ASC_NULLS_LAST) { - $builder->addOrderBy('%column IS NULL, %column ASC', $property, $property); + return ['%ex IS NULL, %ex ASC', $expression->args, $expression->args]; } elseif ($direction === ICollection::DESC_NULLS_FIRST) { - $builder->addOrderBy('%column IS NOT NULL, %column DESC', $property, $property); + return ['%ex IS NOT NULL, %ex DESC', $expression->args, $expression->args]; } } elseif ($this->platformName === 'mssql') { if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_FIRST) { - $builder->addOrderBy('%column ASC', $property); + return ['%ex ASC', $expression->args]; } elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_LAST) { - $builder->addOrderBy('%column DESC', $property); + return ['%ex DESC', $expression->args]; } elseif ($direction === ICollection::ASC_NULLS_LAST) { - $builder->addOrderBy('CASE WHEN %column IS NULL THEN 1 ELSE 0 END, %column ASC', $property, $property); + return ['CASE WHEN %ex IS NULL THEN 1 ELSE 0 END, %ex ASC', $expression->args, $expression->args]; } elseif ($direction === ICollection::DESC_NULLS_FIRST) { - $builder->addOrderBy('CASE WHEN %column IS NOT NULL THEN 1 ELSE 0 END, %column DESC', $property, $property); + return ['CASE WHEN %ex IS NOT NULL THEN 1 ELSE 0 END, %ex DESC', $expression->args, $expression->args]; } } elseif ($this->platformName === 'pgsql') { if ($direction === ICollection::ASC || $direction === ICollection::ASC_NULLS_LAST) { - $builder->addOrderBy('%column ASC', $property); + return ['%ex ASC', $expression->args]; } elseif ($direction === ICollection::DESC || $direction === ICollection::DESC_NULLS_FIRST) { - $builder->addOrderBy('%column DESC', $property); + return ['%ex DESC', $expression->args]; } elseif ($direction === ICollection::ASC_NULLS_FIRST) { - $builder->addOrderBy('%column ASC NULLS FIRST', $property); + return ['%ex ASC NULLS FIRST', $expression->args]; } elseif ($direction === ICollection::DESC_NULLS_LAST) { - $builder->addOrderBy('%column DESC NULLS LAST', $property); + return ['%ex DESC NULLS LAST', $expression->args]; } - } else { - throw new NotSupportedException(); } + + throw new NotSupportedException(); } - public function normalizeValue($value, ColumnReference $columnReference) + /** + * @param mixed $value + * @return mixed + */ + public function normalizeValue($value, PropertyMetadata $propertyMetadata, IConventions $conventions) { - if (isset($columnReference->propertyMetadata->types['array'])) { - if (is_array($value) && !is_array(reset($value))) { + if (isset($propertyMetadata->types['array'])) { + if (\is_array($value) && !\is_array(reset($value))) { $value = [$value]; } - if ($columnReference->propertyMetadata->isPrimary) { + if ($propertyMetadata->isPrimary) { foreach ($value as $subValue) { if (!Arrays::isList($subValue)) { throw new InvalidArgumentException('Composite primary value has to be passed as a list, without array keys.'); @@ -149,10 +166,10 @@ public function normalizeValue($value, ColumnReference $columnReference) } } - if ($columnReference->propertyMetadata->wrapper) { - $property = $columnReference->propertyMetadata->getWrapperPrototype(); - if (is_array($value)) { - $value = array_map(function ($subValue) use ($property) { + if ($propertyMetadata->wrapper !== null) { + $property = $propertyMetadata->getWrapperPrototype(); + if (\is_array($value)) { + $value = \array_map(function ($subValue) use ($property) { return $property->convertToRawValue($subValue); }, $value); } else { @@ -160,8 +177,8 @@ public function normalizeValue($value, ColumnReference $columnReference) } } - $tmp = $columnReference->conventions->convertEntityToStorage([$columnReference->propertyMetadata->name => $value]); - $value = reset($tmp); + $tmp = $conventions->convertEntityToStorage([$propertyMetadata->name => $value]); + $value = \reset($tmp); return $value; } @@ -171,7 +188,7 @@ public function normalizeValue($value, ColumnReference $columnReference) * @param array $tokens * @param class-string<\Nextras\Orm\Entity\IEntity>|null $sourceEntity */ - private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): ColumnReference + private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilder $builder): DbalExpressionResult { $lastToken = \array_pop($tokens); \assert($lastToken !== null); @@ -218,7 +235,13 @@ private function processTokens(array $tokens, ?string $sourceEntity, QueryBuilde $propertyPrefixTokens ); - return new ColumnReference($column, $propertyMetadata, $currentEntityMetadata, $currentReflection); + return new DbalExpressionResult( + ['%column', $column], + false, + function ($value) use ($propertyMetadata, $currentReflection) { + return $this->normalizeValue($value, $propertyMetadata, $currentReflection); + } + ); } diff --git a/src/Collection/ICollection.php b/src/Collection/ICollection.php index 3cadbb30..e4886cf6 100644 --- a/src/Collection/ICollection.php +++ b/src/Collection/ICollection.php @@ -10,11 +10,11 @@ use Countable; use IteratorAggregate; +use Nextras\Orm\Collection\Functions\ConjunctionOperatorFunction; +use Nextras\Orm\Collection\Functions\DisjunctionOperatorFunction; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Mapper\IRelationshipMapper; use Nextras\Orm\NoResultException; -use Nextras\Orm\Repository\Functions\ConjunctionOperatorFunction; -use Nextras\Orm\Repository\Functions\DisjunctionOperatorFunction; /** @@ -79,18 +79,10 @@ public function findBy(array $where): ICollection; /** * Orders collection by column. * Returns new instance of collection. - * @param string $propertyPath property name or property path expression (property->property) - * @param string $direction sorting direction self::ASC or self::DESC + * @param string|array $expression property name or property path expression (property->property) or "expression function" array expression. + * @param string $direction the sorting direction self::ASC or self::DESC, etc. */ - public function orderBy(string $propertyPath, string $direction = self::ASC): ICollection; - - - /** - * Orders collection by multiple column orderings. - * @param array $properties (key - property name, value - property sorting direction) - * @return ICollection - */ - public function orderByMultiple(array $properties): ICollection; + public function orderBy($expression, string $direction = self::ASC): ICollection; /** @@ -105,13 +97,6 @@ public function resetOrderBy(): ICollection; public function limitBy(int $limit, int $offset = null): ICollection; - /** - * Applies custom function to the collection. - * Returns new instance of collection. - */ - public function applyFunction(string $functionName, ...$args): ICollection; - - /** * Fetches the first row. */ diff --git a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php b/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php deleted file mode 100644 index 68f23d9b..00000000 --- a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFilterFunction.php +++ /dev/null @@ -1,22 +0,0 @@ - $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 deleted file mode 100644 index 6dbac318..00000000 --- a/src/Mapper/Dbal/CustomFunctions/IQueryBuilderFunction.php +++ /dev/null @@ -1,21 +0,0 @@ - $args - */ - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): QueryBuilder; -} diff --git a/src/Mapper/Dbal/DbalMapper.php b/src/Mapper/Dbal/DbalMapper.php index 3f12bfd4..454404a4 100644 --- a/src/Mapper/Dbal/DbalMapper.php +++ b/src/Mapper/Dbal/DbalMapper.php @@ -16,6 +16,8 @@ use Nextras\Dbal\Result\Result; use Nextras\Dbal\Result\Row; use Nextras\Orm\Collection\ArrayCollection; +use Nextras\Orm\Collection\DbalCollection; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\PropertyMetadata; @@ -72,7 +74,7 @@ public function findAll(): ICollection public function builder(): QueryBuilder { $tableName = $this->getTableName(); - $alias = QueryBuilderHelper::getAlias($tableName); + $alias = DbalQueryBuilderHelper::getAlias($tableName); $builder = new QueryBuilder($this->connection->getDriver()); $builder->from("[$tableName]", $alias); $builder->select("[$alias.*]"); diff --git a/src/Mapper/Dbal/Helpers/ColumnReference.php b/src/Mapper/Dbal/Helpers/ColumnReference.php deleted file mode 100644 index 932f951e..00000000 --- a/src/Mapper/Dbal/Helpers/ColumnReference.php +++ /dev/null @@ -1,38 +0,0 @@ - */ - public $column; - - /** @var PropertyMetadata */ - public $propertyMetadata; - - /** @var EntityMetadata */ - public $entityMetadata; - - /** @var IConventions */ - public $conventions; - - - public function __construct($column, PropertyMetadata $propertyMetadata, EntityMetadata $entityMetadata, IConventions $conventions) - { - $this->column = $column; - $this->propertyMetadata = $propertyMetadata; - $this->entityMetadata = $entityMetadata; - $this->conventions = $conventions; - } -} diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php index f7f81928..4e4c1728 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasMany.php @@ -11,6 +11,8 @@ use Iterator; use Nextras\Dbal\IConnection; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\DbalCollection; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Collection\MultiEntityIterator; use Nextras\Orm\Entity\IEntity; @@ -119,7 +121,7 @@ protected function execute(DbalCollection $collection, IEntity $parent): MultiEn private function fetchByTwoPassStrategy(QueryBuilder $builder, array $values): MultiEntityIterator { $sourceTable = $builder->getFromAlias(); - $targetTable = QueryBuilderHelper::getAlias($this->joinTable); + $targetTable = DbalQueryBuilderHelper::getAlias($this->joinTable); $builder = clone $builder; $builder->leftJoin( @@ -198,7 +200,7 @@ protected function executeCounts(DbalCollection $collection, IEntity $parent) private function fetchCounts(QueryBuilder $builder, array $values) { $sourceTable = $builder->getFromAlias(); - $targetTable = QueryBuilderHelper::getAlias($this->joinTable); + $targetTable = DbalQueryBuilderHelper::getAlias($this->joinTable); $builder = clone $builder; $builder->leftJoin( diff --git a/src/Mapper/Dbal/RelationshipMapperManyHasOne.php b/src/Mapper/Dbal/RelationshipMapperManyHasOne.php index 4fae5fb6..7a9fe021 100644 --- a/src/Mapper/Dbal/RelationshipMapperManyHasOne.php +++ b/src/Mapper/Dbal/RelationshipMapperManyHasOne.php @@ -12,6 +12,7 @@ use Iterator; use Nextras\Dbal\IConnection; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\DbalCollection; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Collection\MultiEntityIterator; use Nextras\Orm\Entity\IEntity; diff --git a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php index 25e78f45..ffdc7454 100644 --- a/src/Mapper/Dbal/RelationshipMapperOneHasMany.php +++ b/src/Mapper/Dbal/RelationshipMapperOneHasMany.php @@ -11,6 +11,7 @@ use Iterator; use Nextras\Dbal\IConnection; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\DbalCollection; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Collection\MultiEntityIterator; use Nextras\Orm\Entity\IEntity; diff --git a/src/Mapper/Memory/CustomFunctions/IArrayFilterFunction.php b/src/Mapper/Memory/CustomFunctions/IArrayFilterFunction.php deleted file mode 100644 index 3eb0303f..00000000 --- a/src/Mapper/Memory/CustomFunctions/IArrayFilterFunction.php +++ /dev/null @@ -1,18 +0,0 @@ -metadataRelationship->order !== null) { - return $collection->orderByMultiple($this->metadataRelationship->order); + return $collection->orderBy($this->metadataRelationship->order); } else { return $collection; } diff --git a/src/Repository/Functions/ConjunctionOperatorFunction.php b/src/Repository/Functions/ConjunctionOperatorFunction.php deleted file mode 100644 index ef62b61e..00000000 --- a/src/Repository/Functions/ConjunctionOperatorFunction.php +++ /dev/null @@ -1,57 +0,0 @@ -normalizeFunctions($args) as $arg) { - $callback = $helper->createFilter($arg); - if (!$callback($entity)) { - return false; - } - } - return true; - } - - - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array - { - $processedArgs = []; - foreach ($this->normalizeFunctions($args) as $arg) { - $processedArgs[] = $helper->processFilterFunction($builder, $arg); - } - return ['%and', $processedArgs]; - } - - - private function normalizeFunctions(array $args): array - { - if (isset($args[0])) { - return $args; - } - - $processedArgs = []; - foreach ($args as $argName => $argValue) { - [$argName, $operator] = ConditionParserHelper::parsePropertyOperator($argName); - $processedArgs[] = [ValueOperatorFunction::class, $operator, $argName, $argValue]; - } - return $processedArgs; - } -} diff --git a/src/Repository/Functions/DisjunctionOperatorFunction.php b/src/Repository/Functions/DisjunctionOperatorFunction.php deleted file mode 100644 index 1cb7a115..00000000 --- a/src/Repository/Functions/DisjunctionOperatorFunction.php +++ /dev/null @@ -1,57 +0,0 @@ -normalizeFunctions($args) as $arg) { - $callback = $helper->createFilter($arg); - if ($callback($entity)) { - return true; - } - } - return false; - } - - - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array - { - $processedArgs = []; - foreach ($this->normalizeFunctions($args) as $arg) { - $processedArgs[] = $helper->processFilterFunction($builder, $arg); - } - return ['%or', $processedArgs]; - } - - - private function normalizeFunctions(array $args): array - { - if (isset($args[0])) { - return $args; - } - - $processedArgs = []; - foreach ($args as $argName => $argValue) { - [$argName, $operator] = ConditionParserHelper::parsePropertyOperator($argName); - $processedArgs[] = [ValueOperatorFunction::class, $operator, $argName, $argValue]; - } - return $processedArgs; - } -} diff --git a/src/Repository/Functions/ValueOperatorFunction.php b/src/Repository/Functions/ValueOperatorFunction.php deleted file mode 100644 index 6cecb098..00000000 --- a/src/Repository/Functions/ValueOperatorFunction.php +++ /dev/null @@ -1,143 +0,0 @@ -getValue($entity, $args[1]); - if ($valueReference === null) { - return false; - } - - $targetValue = $helper->normalizeValue($args[2], $valueReference->propertyMetadata, true); - - if ($valueReference->isMultiValue) { - foreach ($valueReference->value as $subValue) { - if ($this->arrayEvaluate($operator, $targetValue, $subValue)) { - return true; - } - } - return false; - } else { - return $this->arrayEvaluate($operator, $targetValue, $valueReference->value); - } - } - - - private function arrayEvaluate(string $operator, $targetValue, $sourceValue): bool - { - if ($operator === ConditionParserHelper::OPERATOR_EQUAL) { - if (is_array($targetValue)) { - return in_array($sourceValue, $targetValue, true); - } else { - return $sourceValue === $targetValue; - } - } elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) { - if (is_array($targetValue)) { - return !in_array($sourceValue, $targetValue, true); - } else { - return $sourceValue !== $targetValue; - } - } elseif ($operator === ConditionParserHelper::OPERATOR_GREATER) { - return $sourceValue > $targetValue; - } elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_GREATER) { - return $sourceValue >= $targetValue; - } elseif ($operator === ConditionParserHelper::OPERATOR_SMALLER) { - return $sourceValue < $targetValue; - } elseif ($operator === ConditionParserHelper::OPERATOR_EQUAL_OR_SMALLER) { - return $sourceValue <= $targetValue; - } else { - throw new InvalidArgumentException(); - } - } - - - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array - { - assert(count($args) === 3); - $operator = $args[0]; - $columnReference = $helper->processPropertyExpr($builder, $args[1]); - $column = $columnReference->column; - $value = $helper->normalizeValue($args[2], $columnReference); - - if ($operator === ConditionParserHelper::OPERATOR_EQUAL) { - return $this->qbEqualOperator($column, $value); - } elseif ($operator === ConditionParserHelper::OPERATOR_NOT_EQUAL) { - return $this->qbNotEqualOperator($column, $value); - } else { - return $this->qbOtherOperator($operator, $column, $value); - } - } - - - private function qbEqualOperator($column, $value) - { - if (is_array($value)) { - if ($value) { - if (is_array($column)) { - $value = array_map(function ($value) use ($column) { - return array_combine($column, $value); - }, $value); - return ['%multiOr', $value]; - } else { - return ['%column IN %any', $column, $value]; - } - } else { - return ['1=0']; - } - } elseif ($value === null) { - return ['%column IS NULL', $column]; - } else { - return ['%column = %any', $column, $value]; - } - } - - - private function qbNotEqualOperator($column, $value) - { - if (is_array($value)) { - if ($value) { - if (is_array($column)) { - $value = array_map(function ($value) use ($column) { - return array_combine($column, $value); - }, $value); - return ['NOT (%multiOr)', $value]; - } else { - return ['%column NOT IN %any', $column, $value]; - } - } else { - return ['1=1']; - } - } elseif ($value === null) { - return ['%column IS NOT NULL', $column]; - } else { - return ['%column != %any', $column, $value]; - } - } - - - private function qbOtherOperator($operator, $column, $value) - { - return ["%column $operator %any", $column, $value]; - } -} diff --git a/src/Repository/IRepository.php b/src/Repository/IRepository.php index 492901bc..683f0acb 100644 --- a/src/Repository/IRepository.php +++ b/src/Repository/IRepository.php @@ -9,6 +9,8 @@ namespace Nextras\Orm\Repository; +use Nextras\Orm\Collection\Functions\IArrayFunction; +use Nextras\Orm\Collection\Functions\IQueryBuilderFunction; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\EntityMetadata; @@ -117,7 +119,7 @@ public function findById($primaryValues): ICollection; /** * Returns collection functions instance. - * @return object + * @return IArrayFunction|IQueryBuilderFunction */ public function getCollectionFunction(string $name); diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php index e24a267f..6f78223f 100644 --- a/src/Repository/Repository.php +++ b/src/Repository/Repository.php @@ -9,6 +9,9 @@ namespace Nextras\Orm\Repository; +use Nextras\Orm\Collection\Functions\ConjunctionOperatorFunction; +use Nextras\Orm\Collection\Functions\DisjunctionOperatorFunction; +use Nextras\Orm\Collection\Functions\ValueOperatorFunction; use Nextras\Orm\Collection\ICollection; use Nextras\Orm\Entity\IEntity; use Nextras\Orm\Entity\Reflection\EntityMetadata; @@ -20,9 +23,6 @@ use Nextras\Orm\Model\MetadataStorage; use Nextras\Orm\NoResultException; use Nextras\Orm\NotImplementedException; -use Nextras\Orm\Repository\Functions\ConjunctionOperatorFunction; -use Nextras\Orm\Repository\Functions\DisjunctionOperatorFunction; -use Nextras\Orm\Repository\Functions\ValueOperatorFunction; use ReflectionClass; diff --git a/tests/cases/integration/Collection/collection.customFunctions.phpt b/tests/cases/integration/Collection/collection.customFunctions.phpt index 1ac807e2..b487eeab 100644 --- a/tests/cases/integration/Collection/collection.customFunctions.phpt +++ b/tests/cases/integration/Collection/collection.customFunctions.phpt @@ -12,7 +12,6 @@ use Nextras\Orm\Collection\ICollection; use NextrasTests\Orm\DataTestCase; use NextrasTests\Orm\Helper; use NextrasTests\Orm\LikeFunction; -use NextrasTests\Orm\LikeFilterFunction; use Tester\Assert; use Tester\Environment; @@ -28,35 +27,17 @@ class CollectionCustomFunctionsTest extends DataTestCase Environment::skip('Test only DbalMapper'); } - $count = $this->orm->books->findBy([LikeFilterFunction::class, 'title', 'Book'])->count(); + $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book'])->count(); Assert::same(4, $count); - $count = $this->orm->books->findBy([LikeFilterFunction::class, 'title', 'Book 1'])->count(); + $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book 1'])->count(); Assert::same(1, $count); - $count = $this->orm->books->findBy([LikeFilterFunction::class, 'title', 'Book X'])->count(); + $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book X'])->count(); Assert::same(0, $count); } - public function testLike() - { - if ($this->section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $count = $this->orm->books->findAll()->applyFunction(LikeFunction::class, 'title', 'Book')->count(); - Assert::same(4, $count); - - $count = $this->orm->books->findAll()->applyFunction(LikeFunction::class, 'title', 'Book 1')->count(); - Assert::same(1, $count); - - $count = $this->orm->books->findAll()->applyFunction(LikeFunction::class, 'title', 'Book X')->count(); - Assert::same(0, $count); - } - - - public function testFilterLikeCombined() { if ($this->section === Helper::SECTION_ARRAY) { @@ -65,7 +46,7 @@ class CollectionCustomFunctionsTest extends DataTestCase $count = $this->orm->books->findBy([ ICollection::AND, - [LikeFilterFunction::class, 'title', 'Book'], + [LikeFunction::class, 'title', 'Book'], ['translator!=' => null], ])->count(); Assert::same(3, $count); @@ -73,32 +54,13 @@ class CollectionCustomFunctionsTest extends DataTestCase $count = $this->orm->books->findBy([ ICollection::OR, - [LikeFilterFunction::class, 'title', 'Book 1'], + [LikeFunction::class, 'title', 'Book 1'], ['translator' => null], ])->count(); Assert::same(2, $count); } - public function testLikeArray() - { - if ($this->section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $collection = new ArrayCollection(iterator_to_array($this->orm->books->findAll()), $this->orm->books); - - $count = $collection->applyFunction(LikeFunction::class, 'title', 'Book')->count(); - Assert::same(4, $count); - - $count = $collection->applyFunction(LikeFunction::class, 'title', 'Book 1')->count(); - Assert::same(1, $count); - - $count = $collection->applyFunction(LikeFunction::class, 'title', 'Book X')->count(); - Assert::same(0, $count); - } - - public function testFilterLikeArray() { if ($this->section === Helper::SECTION_ARRAY) { @@ -107,13 +69,13 @@ class CollectionCustomFunctionsTest extends DataTestCase $collection = new ArrayCollection(iterator_to_array($this->orm->books->findAll()), $this->orm->books); - $count = $collection->findBy([LikeFilterFunction::class, 'title', 'Book'])->count(); + $count = $collection->findBy([LikeFunction::class, 'title', 'Book'])->count(); Assert::same(4, $count); - $count = $collection->findBy([LikeFilterFunction::class, 'title', 'Book 1'])->count(); + $count = $collection->findBy([LikeFunction::class, 'title', 'Book 1'])->count(); Assert::same(1, $count); - $count = $collection->findBy([LikeFilterFunction::class, 'title', 'Book X'])->count(); + $count = $collection->findBy([LikeFunction::class, 'title', 'Book X'])->count(); Assert::same(0, $count); } @@ -128,7 +90,7 @@ class CollectionCustomFunctionsTest extends DataTestCase $count = $collection->findBy([ ICollection::AND, - [LikeFilterFunction::class, 'title', 'Book'], + [LikeFunction::class, 'title', 'Book'], ['translator!=' => null], ])->count(); Assert::same(3, $count); @@ -136,7 +98,7 @@ class CollectionCustomFunctionsTest extends DataTestCase $count = $collection->findBy([ ICollection::OR, - [LikeFilterFunction::class, 'title', 'Book 1'], + [LikeFunction::class, 'title', 'Book 1'], ['translator' => null], ])->count(); Assert::same(2, $count); diff --git a/tests/cases/integration/Collection/collection.phpt b/tests/cases/integration/Collection/collection.phpt index d302efd8..a9a0c3fd 100644 --- a/tests/cases/integration/Collection/collection.phpt +++ b/tests/cases/integration/Collection/collection.phpt @@ -107,7 +107,7 @@ class CollectionTest extends DataTestCase public function testOrderingMultiple() { $ids = $this->orm->books->findAll() - ->orderByMultiple([ + ->orderBy([ 'author->id' => ICollection::DESC, 'title' => ICollection::ASC, ]) @@ -115,7 +115,7 @@ class CollectionTest extends DataTestCase Assert::same([3, 4, 1, 2], $ids); $ids = $this->orm->books->findAll() - ->orderByMultiple([ + ->orderBy([ 'author->id' => ICollection::DESC, 'title' => ICollection::DESC, ]) diff --git a/tests/cases/unit/Collection/ArrayCollectionTest.phpt b/tests/cases/unit/Collection/ArrayCollectionTest.phpt index 3530aa1d..c199048b 100644 --- a/tests/cases/unit/Collection/ArrayCollectionTest.phpt +++ b/tests/cases/unit/Collection/ArrayCollectionTest.phpt @@ -98,7 +98,7 @@ class ArrayCollectionTest extends TestCase ); Assert::same( [$authors[2], $authors[1], $authors[0]], - iterator_to_array($collection->orderByMultiple(['age' => ICollection::DESC, 'name' => ICollection::ASC])) + iterator_to_array($collection->orderBy(['age' => ICollection::DESC, 'name' => ICollection::ASC])) ); Assert::same( [$authors[1], $authors[2], $authors[0]], @@ -106,7 +106,7 @@ class ArrayCollectionTest extends TestCase ); Assert::same( [$authors[1], $authors[2], $authors[0]], - iterator_to_array($collection->orderByMultiple(['age' => ICollection::DESC, 'name' => ICollection::DESC])) + iterator_to_array($collection->orderBy(['age' => ICollection::DESC, 'name' => ICollection::DESC])) ); } diff --git a/tests/cases/unit/Mapper/Dbal/DbalCollectionTest.phpt b/tests/cases/unit/Mapper/Dbal/DbalCollectionTest.phpt index 5d4bbd35..5b85c2be 100644 --- a/tests/cases/unit/Mapper/Dbal/DbalCollectionTest.phpt +++ b/tests/cases/unit/Mapper/Dbal/DbalCollectionTest.phpt @@ -8,7 +8,7 @@ namespace NextrasTests\Orm\Mapper\Dbal; use ArrayIterator; use Mockery; -use Nextras\Orm\Mapper\Dbal\DbalCollection; +use Nextras\Orm\Collection\DbalCollection; use NextrasTests\Orm\Book; use NextrasTests\Orm\TestCase; use Tester\Assert; diff --git a/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt b/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt index 35399cfe..00b1f6fe 100644 --- a/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt +++ b/tests/cases/unit/Mapper/Dbal/DbalValueOperatorFunctionTest.phpt @@ -8,10 +8,10 @@ namespace NextrasTests\Orm\Mapper\Dbal; use Mockery; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Functions\ValueOperatorFunction; use Nextras\Orm\Collection\Helpers\ConditionParserHelper; -use Nextras\Orm\Mapper\Dbal\Helpers\ColumnReference; -use Nextras\Orm\Mapper\Dbal\QueryBuilderHelper; -use Nextras\Orm\Repository\Functions\ValueOperatorFunction; +use Nextras\Orm\Collection\Helpers\DbalExpressionResult; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use NextrasTests\Orm\TestCase; use Tester\Assert; @@ -28,19 +28,17 @@ class DbalValueOperatorFunctionTest extends TestCase { $valueOperatorFunction = new ValueOperatorFunction(); - $columnReference = Mockery::mock(ColumnReference::class); - $columnReference->column = 'books.id'; + $expressionResult = new DbalExpressionResult(['%column', 'books.id']); - $helper = Mockery::mock(QueryBuilderHelper::class); - $helper->shouldReceive('processPropertyExpr')->once()->andReturn($columnReference); - $helper->shouldReceive('normalizeValue')->once()->with($expr[2], Mockery::any())->andReturn($expr[2]); + $helper = Mockery::mock(DbalQueryBuilderHelper::class); + $helper->shouldReceive('processPropertyExpr')->once()->andReturn($expressionResult); $builder = Mockery::mock(QueryBuilder::class); $builder->shouldReceive('getFromAlias')->andReturn('books'); Assert::same( $expected, - $valueOperatorFunction->processQueryBuilderFilter($helper, $builder, $expr) + $valueOperatorFunction->processQueryBuilderExpression($helper, $builder, $expr)->args ); } @@ -48,11 +46,11 @@ class DbalValueOperatorFunctionTest extends TestCase protected function operatorTestProvider() { return [ - [['%column = %any', 'books.id', 1], [ConditionParserHelper::OPERATOR_EQUAL, 'id', 1]], - [['%column != %any', 'books.id', 1], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', 1]], - [['%column IN %any', 'books.id', [1, 2]], [ConditionParserHelper::OPERATOR_EQUAL, 'id', [1, 2]]], - [['%column NOT IN %any', 'books.id', [1, 2]], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', [1, 2]]], - [['%column IS NOT NULL', 'books.id'], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', null]], + [['%ex = %any', ['%column', 'books.id'], 1], [ConditionParserHelper::OPERATOR_EQUAL, 'id', 1]], + [['%ex != %any', ['%column', 'books.id'], 1], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', 1]], + [['%ex IN %any', ['%column', 'books.id'], [1, 2]], [ConditionParserHelper::OPERATOR_EQUAL, 'id', [1, 2]]], + [['%ex NOT IN %any', ['%column', 'books.id'], [1, 2]], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', [1, 2]]], + [['%ex IS NOT NULL', ['%column', 'books.id']], [ConditionParserHelper::OPERATOR_NOT_EQUAL, 'id', null]], ]; } } diff --git a/tests/cases/unit/Mapper/Dbal/QueryBuilderHelperTest.phpt b/tests/cases/unit/Mapper/Dbal/QueryBuilderHelperTest.phpt index a615dbc9..32cd824f 100644 --- a/tests/cases/unit/Mapper/Dbal/QueryBuilderHelperTest.phpt +++ b/tests/cases/unit/Mapper/Dbal/QueryBuilderHelperTest.phpt @@ -15,7 +15,7 @@ use Nextras\Orm\Entity\Reflection\PropertyMetadata; use Nextras\Orm\Entity\Reflection\PropertyRelationshipMetadata; use Nextras\Orm\InvalidArgumentException; use Nextras\Orm\Mapper\Dbal\DbalMapper; -use Nextras\Orm\Mapper\Dbal\QueryBuilderHelper; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Mapper\Dbal\Conventions\IConventions; use Nextras\Orm\Mapper\Dbal\Conventions\Conventions; use Nextras\Orm\Model\IModel; @@ -32,7 +32,7 @@ $dic = require_once __DIR__ . '/../../../../bootstrap.php'; class QueryBuilderHelperTest extends TestCase { - /** @var QueryBuilderHelper */ + /** @var DbalQueryBuilderHelper */ private $builderHelper; /** @var Conventions|MockInterface */ @@ -70,7 +70,7 @@ class QueryBuilderHelperTest extends TestCase $this->entityMetadata = Mockery::mock(EntityMetadata::class); $this->queryBuilder = Mockery::mock(QueryBuilder::class); - $this->builderHelper = new QueryBuilderHelper($this->model, $this->repository, $this->mapper); + $this->builderHelper = new DbalQueryBuilderHelper($this->model, $this->repository, $this->mapper); Environment::$checkAssertions = false; } @@ -106,7 +106,7 @@ class QueryBuilderHelperTest extends TestCase $this->conventions->shouldReceive('convertEntityToStorageKey')->once()->with('name')->andReturn('name'); $this->queryBuilder->shouldReceive('leftJoin')->once()->with('books', '[authors]', 'translator', '[books.translator_id] = [translator.id]'); - $columnExpr = $this->builderHelper->processPropertyExpr($this->queryBuilder, 'translator->name')->column; + $columnExpr = $this->builderHelper->processPropertyExpr($this->queryBuilder, 'translator->name')->args[1]; Assert::same('translator.name', $columnExpr); } @@ -168,7 +168,7 @@ class QueryBuilderHelperTest extends TestCase $this->queryBuilder->shouldReceive('groupBy')->twice()->with('%column[]', ['authors.id']); $columnReference = $this->builderHelper->processPropertyExpr($this->queryBuilder, 'translatedBooks->tags->name'); - Assert::same('translatedBooks_tags.name', $columnReference->column); + Assert::same('translatedBooks_tags.name', $columnReference->args[1]); } diff --git a/tests/inc/model/LikeFilterFunction.php b/tests/inc/model/LikeFilterFunction.php deleted file mode 100644 index 21cd8dcd..00000000 --- a/tests/inc/model/LikeFilterFunction.php +++ /dev/null @@ -1,30 +0,0 @@ -getValue($entity, $args[0])->value; - return Strings::startsWith($value, $args[1]); - } - - - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): array - { - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); - $column = $helper->processPropertyExpr($builder, $args[0])->column; - return ['%column LIKE %like_', $column, $args[1]]; - } -} diff --git a/tests/inc/model/LikeFunction.php b/tests/inc/model/LikeFunction.php index 945fec2d..56c128b5 100644 --- a/tests/inc/model/LikeFunction.php +++ b/tests/inc/model/LikeFunction.php @@ -4,29 +4,34 @@ use Nette\Utils\Strings; use Nextras\Dbal\QueryBuilder\QueryBuilder; +use Nextras\Orm\Collection\Functions\IArrayFunction; +use Nextras\Orm\Collection\Functions\IQueryBuilderFunction; use Nextras\Orm\Collection\Helpers\ArrayCollectionHelper; +use Nextras\Orm\Collection\Helpers\DbalExpressionResult; +use Nextras\Orm\Collection\Helpers\DbalQueryBuilderHelper; use Nextras\Orm\Entity\IEntity; -use Nextras\Orm\Mapper\Dbal\CustomFunctions\IQueryBuilderFunction; -use Nextras\Orm\Mapper\Dbal\QueryBuilderHelper; -use Nextras\Orm\Mapper\Memory\CustomFunctions\IArrayFunction; final class LikeFunction implements IArrayFunction, IQueryBuilderFunction { - public function processArrayFilter(ArrayCollectionHelper $helper, array $entities, array $args): array + public function processArrayExpression(ArrayCollectionHelper $helper, IEntity $entity, array $args) { - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); - return array_filter($entities, function (IEntity $entity) use ($helper, $args) { - return Strings::startsWith($helper->getValue($entity, $args[0])->value, $args[1]); - }); + \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1])); + + $value = $helper->getValue($entity, $args[0])->value; + return Strings::startsWith($value, $args[1]); } - public function processQueryBuilderFilter(QueryBuilderHelper $helper, QueryBuilder $builder, array $args): QueryBuilder + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult { - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); - $column = $helper->processPropertyExpr($builder, $args[0])->column; - $builder->andWhere('%column LIKE %like_', $column, $args[1]); - return $builder; + \assert(\count($args) === 2 && \is_string($args[0]) && \is_string($args[1])); + + $expression = $helper->processPropertyExpr($builder, $args[0]); + return $expression->append('LIKE %like_', $args[1]); } } diff --git a/tests/inc/model/book/BooksRepository.php b/tests/inc/model/book/BooksRepository.php index 168aa241..4f78258a 100644 --- a/tests/inc/model/book/BooksRepository.php +++ b/tests/inc/model/book/BooksRepository.php @@ -37,8 +37,6 @@ public function createCollectionFunction(string $name) { if ($name === LikeFunction::class) { return new LikeFunction(); - } elseif ($name === LikeFilterFunction::class) { - return new LikeFilterFunction(); } else { return parent::createCollectionFunction($name); }