Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4643f2e
Initial plan
Copilot Jul 29, 2025
851de90
Add LikeMode enum and update Like filter to support different matchin…
Copilot Jul 29, 2025
5473bcd
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
b2c3f41
Fix empty string handling and add comprehensive tests for Like filter…
Copilot Jul 29, 2025
dbb695f
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
6cde9c6
Merge branch 'master' into copilot/fix-d2529f70-da98-4575-87b1-03af8c…
samdark Jul 29, 2025
9ebe43b
Add clarifying comments to test files for better readability
Copilot Jul 29, 2025
b207e40
Improve test readability by adding explicit comments about expected r…
Copilot Jul 29, 2025
74e8ab3
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
6edd506
Rename test method and use assertSame directly in tests
Copilot Jul 29, 2025
db04aeb
Add CHANGELOG entry for Like filter matching modes enhancement
Copilot Jul 29, 2025
49016e6
Revert test to use assertFixtures instead of assertSame as requested
Copilot Jul 29, 2025
900f2f6
Address review feedback: fix formatting and update attribution
Copilot Jul 29, 2025
7a7b5cc
Add Unicode test cases to catch escaped mutants in LikeHandler
Copilot Jul 29, 2025
6ec3d07
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
7bff5b9
Add better test cases to catch escaped mutants in LikeHandler
Copilot Jul 29, 2025
32e9676
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
d9e7cbd
Add targeted test cases to catch mb_strlen mutation in LikeHandler
Copilot Jul 29, 2025
6c77b27
Apply fixes from StyleCI
StyleCIBot Jul 29, 2025
91af548
improve
vjik Aug 6, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
- Chg #225: Rename classes: `All` to `AndX`, `Any` to `OrX`. Remove `Group` class (@vjik)
- Chg #226: Refactor filter classes to use readonly properties instead of getters (@vjik)
- New #213: Add `nextPage()` and `previousPage()` methods to `PaginatorInterface` (@samdark)
- New #200: Add matching mode parameter to `Like` filter (@samdark, @vjik)

## 1.0.1 January 25, 2023

Expand Down
2 changes: 2 additions & 0 deletions src/Reader/Filter/Like.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ final class Like implements FilterInterface
* - `null` - depends on implementation;
* - `true` - case-sensitive;
* - `false` - case-insensitive.
* @param LikeMode $mode Matching mode.
*/
public function __construct(
public readonly string $field,
public readonly string $value,
public readonly ?bool $caseSensitive = null,
public readonly LikeMode $mode = LikeMode::Contains,
) {
}
}
26 changes: 26 additions & 0 deletions src/Reader/Filter/LikeMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Data\Reader\Filter;

/**
* Like filter matching modes.
*/
enum LikeMode
{
/**
* Field value contains the search value.
*/
case Contains;

/**
* Field value starts with the search value.
*/
case StartsWith;

/**
* Field value ends with the search value.
*/
case EndsWith;
}
36 changes: 33 additions & 3 deletions src/Reader/Iterable/FilterHandler/LikeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Yiisoft\Data\Reader\Iterable\FilterHandler;

use Yiisoft\Data\Reader\Filter\Like;
use Yiisoft\Data\Reader\Filter\LikeMode;
use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Data\Reader\Iterable\Context;
use Yiisoft\Data\Reader\Iterable\IterableFilterHandlerInterface;
Expand All @@ -30,8 +31,37 @@ public function match(object|array $item, FilterInterface $filter, Context $cont
return false;
}

return $filter->caseSensitive === true
? str_contains($itemValue, $filter->value)
: mb_stripos($itemValue, $filter->value) !== false;
if ($filter->value === '') {
return true;
}

return match ($filter->mode) {
LikeMode::Contains => $this->matchContains($itemValue, $filter->value, $filter->caseSensitive),
LikeMode::StartsWith => $this->matchStartsWith($itemValue, $filter->value, $filter->caseSensitive),
LikeMode::EndsWith => $this->matchEndsWith($itemValue, $filter->value, $filter->caseSensitive),
};
}

private function matchContains(string $value, string $search, ?bool $caseSensitive): bool
{
return $caseSensitive === true
? str_contains($value, $search)
: mb_stripos($value, $search) !== false;
}

private function matchStartsWith(string $value, string $search, ?bool $caseSensitive): bool
{
return $caseSensitive === true
? str_starts_with($value, $search)
: mb_stripos($value, $search) === 0;
}

private function matchEndsWith(string $value, string $search, ?bool $caseSensitive): bool
{
if ($caseSensitive === true) {
return str_ends_with($value, $search);
}

return mb_strtolower(mb_substr($value, -mb_strlen($search))) === mb_strtolower($search);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\Attributes\DataProvider;
use Yiisoft\Data\Reader\Filter\Like;
use Yiisoft\Data\Reader\Filter\LikeMode;
use Yiisoft\Data\Tests\Common\Reader\BaseReaderTestCase;

abstract class BaseReaderWithLikeTestCase extends BaseReaderTestCase
Expand Down Expand Up @@ -34,4 +35,37 @@ public function testWithReader(
$reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive));
$this->assertFixtures($expectedFixtureIndexes, $reader->read());
}

public static function dataWithReaderAndMode(): array
{
return [
// CONTAINS mode
'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::Contains, [2]], // Expects: seed@beat
'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::Contains, [2]], // Expects: seed@beat

// STARTS_WITH mode
'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::StartsWith, [2]], // Expects: seed@beat
'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::StartsWith, [2]], // Expects: seed@beat
'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::StartsWith, []], // Expects: no matches

// ENDS_WITH mode
'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::EndsWith, [2]], // Expects: seed@beat
'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::EndsWith, [2]], // Expects: seed@beat
'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::EndsWith, []], // Expects: no matches
];
}

#[DataProvider('dataWithReaderAndMode')]
public function testWithReaderAndMode(
string $field,
string $value,
bool|null $caseSensitive,
LikeMode $mode,
array $expectedFixtureIndexes,
): void {
$reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive, $mode));
$actualData = $reader->read();
// Assert that we get the expected fixtures based on the filter criteria
$this->assertFixtures($expectedFixtureIndexes, $actualData);
}
}
76 changes: 76 additions & 0 deletions tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\Attributes\DataProvider;
use Yiisoft\Data\Reader\Filter\Like;
use Yiisoft\Data\Reader\Filter\LikeMode;
use Yiisoft\Data\Reader\Iterable\Context;
use Yiisoft\Data\Reader\Iterable\FilterHandler\LikeHandler;
use Yiisoft\Data\Reader\Iterable\ValueReader\FlatValueReader;
Expand Down Expand Up @@ -48,4 +49,79 @@ public function testMatch(bool $expected, array $item, string $field, string $va
$context = new Context([], new FlatValueReader());
$this->assertSame($expected, $filterHandler->match($item, new Like($field, $value, $caseSensitive), $context));
}

public static function matchWithModeDataProvider(): array
{
return [
// "Contains" mode
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::Contains],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'cat', false, LikeMode::Contains],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'cat', true, LikeMode::Contains],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::Contains],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::Contains],

// "StartsWith" mode
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', false, LikeMode::StartsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', true, LikeMode::StartsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::StartsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great Cat', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'Привет', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::StartsWith],

// "EndsWith" mode
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', false, LikeMode::EndsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', true, LikeMode::EndsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::EndsWith],
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat Fighter', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'мир', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'das Öl'], 'value', 'öl', false, LikeMode::EndsWith],

// Edge cases
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::Contains],
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::StartsWith],
[true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::EndsWith],
[false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::StartsWith],
[false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::StartsWith],
[false, ['id' => 1, 'value' => '🌟'], 'value', 'xyz🌟', false, LikeMode::EndsWith],
[false, ['id' => 1, 'value' => 'é🎉'], 'value', 'abcé🎉', false, LikeMode::EndsWith],
[true, ['id' => 1, 'value' => 'aliİ'], 'value', 'İ', false, LikeMode::EndsWith],
];
}

#[DataProvider('matchWithModeDataProvider')]
public function testMatchWithMode(
bool $expected,
array $item,
string $field,
string $value,
?bool $caseSensitive,
LikeMode $mode,
): void {
$handler = new LikeHandler();
$context = new Context([], new FlatValueReader());
$filter = new Like($field, $value, $caseSensitive, $mode);

$this->assertSame(
$expected,
$handler->match($item, $filter, $context)
);
}

public function testConstructorDefaultMode(): void
{
$handler = new LikeHandler();
$context = new Context([], new FlatValueReader());
$item = ['id' => 1, 'value' => 'Great Cat Fighter'];

$this->assertTrue($handler->match($item, new Like('value', 'Cat'), $context));
$this->assertFalse($handler->match($item, new Like('value', 'Hello'), $context));
}
}
Loading