Skip to content

Commit

Permalink
collection: implement LIKE operator support [closes #410]
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed May 24, 2020
1 parent 7aa81d0 commit f8e20cc
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 162 deletions.
81 changes: 81 additions & 0 deletions src/Collection/Expression/LikeExpression.php
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Expression;


/**
* Like expression wrapper for {@see \Nextras\Orm\Collection\Functions\CompareLikeFunction}.
*/
class LikeExpression
{
/**
* Wraps input as a final LIKE filter.
* Special LIKE characters are not sanitized.
* It is not recommended to use raw LIKE expression with user-entered input.
*/
public static function raw(string $input): LikeExpression
{
return new self($input, self::MODE_RAW);
}


/**
* Wraps input as find-by-prefix filter (i.e. string may end 0-n other characters).
* Special LIKE characters are sanitized.
*/
public static function startsWith(string $input)
{
return new self($input, self::MODE_STARTS_WITH);
}


/**
* Wraps input as find-by-suffix filter (i.e. string may start 0-n other characters).
* Special LIKE characters are sanitized.
*/
public static function endsWith(string $input)
{
return new self($input, self::MODE_ENDS_WITH);
}


/**
* Wraps input as contains filter (i.e. string may start and end 0-n other characters).
* Special LIKE characters are sanitized.
*/
public static function contains(string $input)
{
return new self($input, self::MODE_CONTAINS);
}


public const MODE_RAW = 0;
public const MODE_STARTS_WITH = 1;
public const MODE_ENDS_WITH = 2;
public const MODE_CONTAINS = 3;

/** @var string */
private $input;

/** @var int */
private $mode;


private function __construct(string $input, int $mode)
{
$this->input = $input;
$this->mode = $mode;
}


public function getInput(): string
{
return $this->input;
}


public function getMode(): int
{
return $this->mode;
}
}
118 changes: 118 additions & 0 deletions src/Collection/Functions/CompareLikeFunction.php
@@ -0,0 +1,118 @@
<?php declare(strict_types = 1);

namespace Nextras\Orm\Collection\Functions;


use Nette\Utils\Strings;
use Nextras\Dbal\QueryBuilder\QueryBuilder;
use Nextras\Orm\Collection\Expression\LikeExpression;
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\InvalidStateException;
use function preg_quote;
use function str_replace;


class CompareLikeFunction implements IArrayFunction, IQueryBuilderFunction
{
public function processArrayExpression(ArrayCollectionHelper $helper, IEntity $entity, array $args)
{
assert(count($args) === 2);

$valueReference = $helper->getValue($entity, $args[0]);

$likeExpression = $args[1];
assert($likeExpression instanceof LikeExpression);
$mode = $likeExpression->getMode();

if ($valueReference->propertyMetadata !== null) {
$targetValue = $helper->normalizeValue($likeExpression->getInput(), $valueReference->propertyMetadata, true);
} else {
$targetValue = $likeExpression->getInput();
}

if ($valueReference->isMultiValue) {
foreach ($valueReference->value as $subValue) {
if ($this->evaluateInPhp($mode, $subValue, $targetValue)) {
return true;
}
}
return false;
} else {
return $this->evaluateInPhp($mode, $valueReference->value, $targetValue);
}
}


public function processQueryBuilderExpression(
DbalQueryBuilderHelper $helper,
QueryBuilder $builder,
array $args
): DbalExpressionResult
{
assert(count($args) === 2);

$expression = $helper->processPropertyExpr($builder, $args[0]);

$likeExpression = $args[1];
assert($likeExpression instanceof LikeExpression);
$mode = $likeExpression->getMode();

if ($expression->valueNormalizer !== null) {
$cb = $expression->valueNormalizer;
$value = $cb($likeExpression->getInput());
} else {
$value = $likeExpression->getInput();
}

return $this->evaluateInDb($mode, $expression, $value);
}


/**
* @param mixed $sourceValue
* @param mixed $targetValue
*/
protected function evaluateInPhp(int $mode, $sourceValue, $targetValue): bool
{
if ($mode === LikeExpression::MODE_RAW) {
$regexp = '~^' . preg_quote($targetValue, '~') . '$~';
$regexp = str_replace(['_', '%'], ['.', '.*'], $regexp);
return Strings::match($sourceValue, $regexp) !== null;

} elseif ($mode === LikeExpression::MODE_STARTS_WITH) {
return Strings::startsWith($sourceValue, $targetValue);

} elseif ($mode === LikeExpression::MODE_ENDS_WITH) {
return Strings::endsWith($sourceValue, $targetValue);

} elseif ($mode === LikeExpression::MODE_CONTAINS) {
$regexp = '~^.*' . preg_quote($targetValue, '~') . '.*$~';
return Strings::match($sourceValue, $regexp) !== null;

} else {
throw new InvalidStateException();
}
}


/**
* @param mixed $value
*/
protected function evaluateInDb(int $mode, DbalExpressionResult $expression, $value): DbalExpressionResult
{
if ($mode === LikeExpression::MODE_RAW) {
return $expression->append('LIKE %s', $value);
} elseif ($mode === LikeExpression::MODE_STARTS_WITH) {
return $expression->append('LIKE %like_', $value);
} elseif ($mode === LikeExpression::MODE_ENDS_WITH) {
return $expression->append('LIKE %_like', $value);
} elseif ($mode === LikeExpression::MODE_CONTAINS) {
return $expression->append('LIKE %_like_', $value);
} else {
throw new InvalidStateException();
}
}
}
5 changes: 4 additions & 1 deletion src/Collection/Helpers/ConditionParserHelper.php
Expand Up @@ -6,6 +6,7 @@
use Nextras\Orm\Collection\Functions\CompareEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction;
use Nextras\Orm\Collection\Functions\CompareLikeFunction;
use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareSmallerThanEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareSmallerThanFunction;
Expand All @@ -27,7 +28,7 @@ class ConditionParserHelper
*/
public static function parsePropertyOperator(string $condition): array
{
if (!preg_match('#^(.+?)(!=|<=|>=|=|>|<)?$#', $condition, $matches)) {
if (!preg_match('#^(.+?)(!=|<=|>=|=|>|<|~)?$#', $condition, $matches)) {
return [CompareEqualsFunction::class, $condition];
}
$operator = $matches[2] ?? '=';
Expand All @@ -43,6 +44,8 @@ public static function parsePropertyOperator(string $condition): array
return [CompareSmallerThanEqualsFunction::class, $matches[1]];
} elseif ($operator === '<') {
return [CompareSmallerThanFunction::class, $matches[1]];
} elseif ($operator === '~') {
return [CompareLikeFunction::class, $matches[1]];
} else {
throw new InvalidStateException();
}
Expand Down
2 changes: 2 additions & 0 deletions src/Repository/Repository.php
Expand Up @@ -14,6 +14,7 @@
use Nextras\Orm\Collection\Functions\CompareEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction;
use Nextras\Orm\Collection\Functions\CompareLikeFunction;
use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareSmallerThanEqualsFunction;
use Nextras\Orm\Collection\Functions\CompareSmallerThanFunction;
Expand Down Expand Up @@ -280,6 +281,7 @@ protected function createCollectionFunction(string $name)
CompareNotEqualsFunction::class => true,
CompareSmallerThanEqualsFunction::class => true,
CompareSmallerThanFunction::class => true,
CompareLikeFunction::class => true,
ConjunctionOperatorFunction::class => true,
DisjunctionOperatorFunction::class => true,
AvgAggregateFunction::class => true,
Expand Down
110 changes: 0 additions & 110 deletions tests/cases/integration/Collection/collection.customFunctions.phpt

This file was deleted.

0 comments on commit f8e20cc

Please sign in to comment.