Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
- Enh #1010: Improve `Quoter::getTableNameParts()` method (@Tigrov)
- Enh #1011: Refactor `TableSchemaInterface` and `AbstractSchema` (@Tigrov)
- Enh #1011: Remove `AbstractTableSchema` and add `TableSchema` instead (@Tigrov)
- Chg #1014: Replace `getEscapingReplacements()`/`setEscapingReplacements()` methods with `escape` constructor parameter
in `Like` condition (@vjik)

## 1.3.0 March 21, 2024

Expand Down
2 changes: 2 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `AbstractDQLQueryBuilder::hasOffset()` - use `!empty($offset)` instead;
- `BatchQueryResultInterface::reset()` - use `BatchQueryResultInterface::rewind()` instead;
- `BatchQueryResult::reset()` - use `BatchQueryResult::rewind()` instead;
- `Like::getEscapingReplacements()`/`LikeCondition::setEscapingReplacements()` - use `escape` constructor parameter
instead;

### Remove deprecated parameters

Expand Down
17 changes: 8 additions & 9 deletions docs/guide/en/query/where.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,14 @@ When the value range is given as an array, many `LIKE` predicates will be genera

For example, `['like', 'name', ['test', 'sample']]` will generate `name LIKE '%test%' AND name LIKE '%sample%'`.

You may also give an optional third operand to specify how to escape special characters in the values.
The operand should be an array of mappings from the special characters to their escaped counterparts.

If this operand isn't provided, a default escape mapping will be used.

You may use false or an empty array to indicate the values are already escaped and no escape should be applied.

> Note: That when using an escape mapping (or the third operand isn't provided),
> the values will be automatically inside within a pair of percentage characters.
You may also provide an optional `escape` parameter to control whether special characters in the values should be
escaped. If `escape` is `true` (default), special characters like `%`, `_`, and `\` will be escaped and the values will
be automatically wrapped with `%`. If `escape` is `false`, the values will be used as-is without any escaping or
wrapping.

For example:
- `['like', 'name', 'test']` will generate `name LIKE '%test%'` (default escaping)
- `['like', 'name', 'test%', 'escape' => false]` will generate `name LIKE 'test%'` (no escaping or wrapping)

Optionally, you can specify the case sensitivity of the `LIKE` condition by passing boolean value with `caseSensitive`
key, e.g. `['like', 'name', 'Ivan', 'caseSensitive' => true]`.
Expand Down
25 changes: 12 additions & 13 deletions src/QueryBuilder/Condition/Builder/LikeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,6 @@ public function __construct(
public function build(ExpressionInterface $expression, array &$params = []): string
{
$values = $expression->value;
$escape = $expression->getEscapingReplacements();

if ($escape === []) {
$escape = $this->escapingReplacements;
}

[$andor, $not, $operator] = $this->parseOperator($expression);

Expand All @@ -80,7 +75,7 @@ public function build(ExpressionInterface $expression, array &$params = []): str

/** @psalm-var list<string|ExpressionInterface> $values */
foreach ($values as $value) {
$placeholderName = $this->preparePlaceholderName($value, $expression, $escape, $params);
$placeholderName = $this->preparePlaceholderName($value, $expression, $params);
$parts[] = "$column $operator $placeholderName$this->escapeSql";
}

Expand All @@ -95,9 +90,9 @@ public function build(ExpressionInterface $expression, array &$params = []): str
* @throws InvalidConfigException
* @throws NotSupportedException
*/
protected function prepareColumn(Like $expression, array &$params): string
protected function prepareColumn(Like $condition, array &$params): string
{
$column = $expression->column;
$column = $condition->column;

if ($column instanceof ExpressionInterface) {
return $this->queryBuilder->buildExpression($column, $params);
Expand All @@ -121,15 +116,19 @@ protected function prepareColumn(Like $expression, array &$params): string
*/
protected function preparePlaceholderName(
string|ExpressionInterface $value,
Like $expression,
array|null $escape,
Like $condition,
array &$params,
): string {
if ($value instanceof ExpressionInterface) {
return $this->queryBuilder->buildExpression($value, $params);
}
return $this->queryBuilder->bindParam(
new Param($escape === null ? $value : ('%' . strtr($value, $escape) . '%'), DataType::STRING),
new Param(
$condition->escape
? ('%' . strtr($value, $this->escapingReplacements) . '%')
: $value,
DataType::STRING,
),
$params
);
}
Expand All @@ -141,9 +140,9 @@ protected function preparePlaceholderName(
*
* @psalm-return array{0: string, 1: bool, 2: string}
*/
protected function parseOperator(Like $expression): array
protected function parseOperator(Like $condition): array
{
$operator = strtoupper($expression->operator);
$operator = strtoupper($condition->operator);
if (!preg_match('/^(AND |OR |)((NOT |)I?LIKE)/', $operator, $matches)) {
throw new InvalidArgumentException("Invalid operator in like condition: \"$operator\"");
}
Expand Down
38 changes: 6 additions & 32 deletions src/QueryBuilder/Condition/Like.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use InvalidArgumentException;
use Yiisoft\Db\Expression\ExpressionInterface;

use function array_key_exists;
use function is_array;
use function is_int;
use function is_string;
Expand All @@ -18,46 +17,26 @@
*/
final class Like implements ConditionInterface
{
protected array|null $escapingReplacements = [];
private const DEFAULT_ESCAPE = true;

/**
* @param ExpressionInterface|string $column The column name.
* @param string $operator The operator to use such as `>` or `<=`.
* @param array|ExpressionInterface|int|Iterator|string|null $value The value to the right of {@see operator}.
* @param bool|null $caseSensitive Whether the comparison is case-sensitive. `null` means using the default
* behavior.
* @param bool $escape Whether to escape the value. Defaults to `true`. If `false`, the value will be used as is
* without escaping.
*/
public function __construct(
public readonly string|ExpressionInterface $column,
public readonly string $operator,
public readonly array|int|string|Iterator|ExpressionInterface|null $value,
public readonly ?bool $caseSensitive = null,
public readonly bool $escape = self::DEFAULT_ESCAPE,
) {
}

/**
* @see setEscapingReplacements()
*/
public function getEscapingReplacements(): ?array
{
return $this->escapingReplacements;
}

/**
* This method allows specifying how to escape special characters in the value(s).
*
* @param array|null $escapingReplacements An array of mappings from the special characters to their escaped
* counterparts.
*
* You may use an empty array to indicate the values are already escaped and no escape should be applied.
* Note that when using an escape mapping (or the third operand isn't provided), the values will be automatically
* inside within a pair of percentage characters.
*/
public function setEscapingReplacements(array|null $escapingReplacements): void
{
$this->escapingReplacements = $escapingReplacements;
}

/**
* Creates a condition based on the given operator and operands.
*
Expand All @@ -69,18 +48,13 @@ public static function fromArrayDefinition(string $operator, array $operands): s
throw new InvalidArgumentException("Operator '$operator' requires two operands.");
}

$condition = new self(
return new self(
self::validateColumn($operator, $operands[0]),
$operator,
self::validateValue($operator, $operands[1]),
isset($operands['caseSensitive']) ? (bool) $operands['caseSensitive'] : null,
isset($operands['escape']) ? (bool) $operands['escape'] : self::DEFAULT_ESCAPE,
);

if (array_key_exists(2, $operands) && (is_array($operands[2]) || $operands[2] === null)) {
$condition->setEscapingReplacements($operands[2]);
}

return $condition;
}

/**
Expand Down
16 changes: 5 additions & 11 deletions tests/Db/QueryBuilder/Condition/LikeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function testConstructor(): void
$this->assertSame('LIKE', $likeCondition->operator);
$this->assertSame('test', $likeCondition->value);
$this->assertNull($likeCondition->caseSensitive);
$this->assertTrue($likeCondition->escape);
}

public function testFromArrayDefinition(): void
Expand All @@ -32,6 +33,7 @@ public function testFromArrayDefinition(): void
$this->assertSame('LIKE', $likeCondition->operator);
$this->assertSame('test', $likeCondition->value);
$this->assertNull($likeCondition->caseSensitive);
$this->assertTrue($likeCondition->escape);
}

public function testFromArrayDefinitionException(): void
Expand Down Expand Up @@ -60,22 +62,14 @@ public function testFromArrayDefinitionExceptionValue(): void
Like::fromArrayDefinition('LIKE', ['id', false]);
}

public function testFromArrayDefinitionSetEscapingReplacements(): void
public function testFromArrayDefinitionWithEscape(): void
{
$likeCondition = Like::fromArrayDefinition('LIKE', ['id', 'test', ['%' => '\%', '_' => '\_']]);
$likeCondition = Like::fromArrayDefinition('LIKE', ['id', 'test', 'escape' => false]);

$this->assertSame('id', $likeCondition->column);
$this->assertSame('LIKE', $likeCondition->operator);
$this->assertSame('test', $likeCondition->value);
$this->assertSame(['%' => '\%', '_' => '\_'], $likeCondition->getEscapingReplacements());
}

public function testSetEscapingReplacements(): void
{
$likeCondition = new Like('id', 'LIKE', 'test');
$likeCondition->setEscapingReplacements(['%' => '\%', '_' => '\_']);

$this->assertSame(['%' => '\%', '_' => '\_'], $likeCondition->getEscapingReplacements());
$this->assertFalse($likeCondition->escape);
}

#[TestWith([null])]
Expand Down
2 changes: 1 addition & 1 deletion tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ public static function buildLikeCondition(): array
/**
* {@see https://github.com/yiisoft/yii2/issues/15630}
*/
[['like', 'location.title_ru', 'vi%', null], '[[location]].[[title_ru]] LIKE :qp0', [':qp0' => new Param('vi%', DataType::STRING)]],
[['like', 'location.title_ru', 'vi%', 'escape' => false], '[[location]].[[title_ru]] LIKE :qp0', [':qp0' => new Param('vi%', DataType::STRING)]],

/* like object conditions */
[
Expand Down
Loading