Skip to content

Commit 1113566

Browse files
vjiksamdark
andauthored
Introduce like matching mode (#231)
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
1 parent 07e9146 commit 1113566

File tree

6 files changed

+172
-3
lines changed

6 files changed

+172
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
- Chg #225: Rename classes: `All` to `AndX`, `Any` to `OrX`. Remove `Group` class (@vjik)
5959
- Chg #226: Refactor filter classes to use readonly properties instead of getters (@vjik)
6060
- New #213: Add `nextPage()` and `previousPage()` methods to `PaginatorInterface` (@samdark)
61+
- New #200: Add matching mode parameter to `Like` filter (@samdark, @vjik)
6162

6263
## 1.0.1 January 25, 2023
6364

src/Reader/Filter/Like.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ final class Like implements FilterInterface
1919
* - `null` - depends on implementation;
2020
* - `true` - case-sensitive;
2121
* - `false` - case-insensitive.
22+
* @param LikeMode $mode Matching mode.
2223
*/
2324
public function __construct(
2425
public readonly string $field,
2526
public readonly string $value,
2627
public readonly ?bool $caseSensitive = null,
28+
public readonly LikeMode $mode = LikeMode::Contains,
2729
) {
2830
}
2931
}

src/Reader/Filter/LikeMode.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Data\Reader\Filter;
6+
7+
/**
8+
* Like filter matching modes.
9+
*/
10+
enum LikeMode
11+
{
12+
/**
13+
* Field value contains the search value.
14+
*/
15+
case Contains;
16+
17+
/**
18+
* Field value starts with the search value.
19+
*/
20+
case StartsWith;
21+
22+
/**
23+
* Field value ends with the search value.
24+
*/
25+
case EndsWith;
26+
}

src/Reader/Iterable/FilterHandler/LikeHandler.php

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Yiisoft\Data\Reader\Iterable\FilterHandler;
66

77
use Yiisoft\Data\Reader\Filter\Like;
8+
use Yiisoft\Data\Reader\Filter\LikeMode;
89
use Yiisoft\Data\Reader\FilterInterface;
910
use Yiisoft\Data\Reader\Iterable\Context;
1011
use Yiisoft\Data\Reader\Iterable\IterableFilterHandlerInterface;
@@ -30,8 +31,37 @@ public function match(object|array $item, FilterInterface $filter, Context $cont
3031
return false;
3132
}
3233

33-
return $filter->caseSensitive === true
34-
? str_contains($itemValue, $filter->value)
35-
: mb_stripos($itemValue, $filter->value) !== false;
34+
if ($filter->value === '') {
35+
return true;
36+
}
37+
38+
return match ($filter->mode) {
39+
LikeMode::Contains => $this->matchContains($itemValue, $filter->value, $filter->caseSensitive),
40+
LikeMode::StartsWith => $this->matchStartsWith($itemValue, $filter->value, $filter->caseSensitive),
41+
LikeMode::EndsWith => $this->matchEndsWith($itemValue, $filter->value, $filter->caseSensitive),
42+
};
43+
}
44+
45+
private function matchContains(string $value, string $search, ?bool $caseSensitive): bool
46+
{
47+
return $caseSensitive === true
48+
? str_contains($value, $search)
49+
: mb_stripos($value, $search) !== false;
50+
}
51+
52+
private function matchStartsWith(string $value, string $search, ?bool $caseSensitive): bool
53+
{
54+
return $caseSensitive === true
55+
? str_starts_with($value, $search)
56+
: mb_stripos($value, $search) === 0;
57+
}
58+
59+
private function matchEndsWith(string $value, string $search, ?bool $caseSensitive): bool
60+
{
61+
if ($caseSensitive === true) {
62+
return str_ends_with($value, $search);
63+
}
64+
65+
return mb_strtolower(mb_substr($value, -mb_strlen($search))) === mb_strtolower($search);
3666
}
3767
}

tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use Yiisoft\Data\Reader\Filter\Like;
9+
use Yiisoft\Data\Reader\Filter\LikeMode;
910
use Yiisoft\Data\Tests\Common\Reader\BaseReaderTestCase;
1011

1112
abstract class BaseReaderWithLikeTestCase extends BaseReaderTestCase
@@ -34,4 +35,37 @@ public function testWithReader(
3435
$reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive));
3536
$this->assertFixtures($expectedFixtureIndexes, $reader->read());
3637
}
38+
39+
public static function dataWithReaderAndMode(): array
40+
{
41+
return [
42+
// CONTAINS mode
43+
'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::Contains, [2]], // Expects: seed@beat
44+
'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::Contains, [2]], // Expects: seed@beat
45+
46+
// STARTS_WITH mode
47+
'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::StartsWith, [2]], // Expects: seed@beat
48+
'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::StartsWith, [2]], // Expects: seed@beat
49+
'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::StartsWith, []], // Expects: no matches
50+
51+
// ENDS_WITH mode
52+
'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::EndsWith, [2]], // Expects: seed@beat
53+
'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::EndsWith, [2]], // Expects: seed@beat
54+
'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::EndsWith, []], // Expects: no matches
55+
];
56+
}
57+
58+
#[DataProvider('dataWithReaderAndMode')]
59+
public function testWithReaderAndMode(
60+
string $field,
61+
string $value,
62+
bool|null $caseSensitive,
63+
LikeMode $mode,
64+
array $expectedFixtureIndexes,
65+
): void {
66+
$reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive, $mode));
67+
$actualData = $reader->read();
68+
// Assert that we get the expected fixtures based on the filter criteria
69+
$this->assertFixtures($expectedFixtureIndexes, $actualData);
70+
}
3771
}

tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use Yiisoft\Data\Reader\Filter\Like;
9+
use Yiisoft\Data\Reader\Filter\LikeMode;
910
use Yiisoft\Data\Reader\Iterable\Context;
1011
use Yiisoft\Data\Reader\Iterable\FilterHandler\LikeHandler;
1112
use Yiisoft\Data\Reader\Iterable\ValueReader\FlatValueReader;
@@ -48,4 +49,79 @@ public function testMatch(bool $expected, array $item, string $field, string $va
4849
$context = new Context([], new FlatValueReader());
4950
$this->assertSame($expected, $filterHandler->match($item, new Like($field, $value, $caseSensitive), $context));
5051
}
52+
53+
public static function matchWithModeDataProvider(): array
54+
{
55+
return [
56+
// "Contains" mode
57+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::Contains],
58+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'cat', false, LikeMode::Contains],
59+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'cat', true, LikeMode::Contains],
60+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::Contains],
61+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::Contains],
62+
63+
// "StartsWith" mode
64+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::StartsWith],
65+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', false, LikeMode::StartsWith],
66+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', true, LikeMode::StartsWith],
67+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::StartsWith],
68+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::StartsWith],
69+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great Cat', null, LikeMode::StartsWith],
70+
[true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'Привет', null, LikeMode::StartsWith],
71+
[true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::StartsWith],
72+
73+
// "EndsWith" mode
74+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::EndsWith],
75+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', false, LikeMode::EndsWith],
76+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', true, LikeMode::EndsWith],
77+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::EndsWith],
78+
[false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::EndsWith],
79+
[true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat Fighter', null, LikeMode::EndsWith],
80+
[true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'мир', null, LikeMode::EndsWith],
81+
[true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::EndsWith],
82+
[true, ['id' => 1, 'value' => 'das Öl'], 'value', 'öl', false, LikeMode::EndsWith],
83+
84+
// Edge cases
85+
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::Contains],
86+
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::StartsWith],
87+
[true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::EndsWith],
88+
[true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::StartsWith],
89+
[true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::EndsWith],
90+
[false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::StartsWith],
91+
[false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::EndsWith],
92+
[true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::StartsWith],
93+
[false, ['id' => 1, 'value' => '🌟'], 'value', 'xyz🌟', false, LikeMode::EndsWith],
94+
[false, ['id' => 1, 'value' => 'é🎉'], 'value', 'abcé🎉', false, LikeMode::EndsWith],
95+
[true, ['id' => 1, 'value' => 'aliİ'], 'value', 'İ', false, LikeMode::EndsWith],
96+
];
97+
}
98+
99+
#[DataProvider('matchWithModeDataProvider')]
100+
public function testMatchWithMode(
101+
bool $expected,
102+
array $item,
103+
string $field,
104+
string $value,
105+
?bool $caseSensitive,
106+
LikeMode $mode,
107+
): void {
108+
$handler = new LikeHandler();
109+
$context = new Context([], new FlatValueReader());
110+
$filter = new Like($field, $value, $caseSensitive, $mode);
111+
112+
$this->assertSame(
113+
$expected,
114+
$handler->match($item, $filter, $context)
115+
);
116+
}
117+
118+
public function testConstructorDefaultMode(): void
119+
{
120+
$handler = new LikeHandler();
121+
$context = new Context([], new FlatValueReader());
122+
$item = ['id' => 1, 'value' => 'Great Cat Fighter'];
123+
124+
$this->assertTrue($handler->match($item, new Like('value', 'Cat'), $context));
125+
$this->assertFalse($handler->match($item, new Like('value', 'Hello'), $context));
126+
}
51127
}

0 commit comments

Comments
 (0)