Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
collection: implement LIKE operator support [closes #410]
- Loading branch information
Showing
8 changed files
with
293 additions
and
162 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 0 additions & 110 deletions
110
tests/cases/integration/Collection/collection.customFunctions.phpt
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.