From 4643f2eaa4ab1141fb40fc3d19b887d43b48286b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:44:23 +0000 Subject: [PATCH 01/19] Initial plan From 851de90342ac5d70254b035f17c950f5cbad0031 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:00:26 +0000 Subject: [PATCH 02/19] Add LikeMode enum and update Like filter to support different matching modes Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- src/Reader/Filter/Like.php | 6 ++ src/Reader/Filter/LikeMode.php | 26 +++++++ .../Iterable/FilterHandler/LikeHandler.php | 37 ++++++++- .../FilterHandler/LikeHandlerTest.php | 76 +++++++++++++++++++ 4 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/Reader/Filter/LikeMode.php diff --git a/src/Reader/Filter/Like.php b/src/Reader/Filter/Like.php index 8180bea..2bf41bb 100644 --- a/src/Reader/Filter/Like.php +++ b/src/Reader/Filter/Like.php @@ -19,11 +19,17 @@ final class Like implements FilterInterface * - `null` - depends on implementation; * - `true` - case-sensitive; * - `false` - case-insensitive. + * @param LikeMode $mode Matching mode: + * + * - `LikeMode::CONTAINS` - field value contains the search value (default); + * - `LikeMode::STARTS_WITH` - field value starts with the search value; + * - `LikeMode::ENDS_WITH` - field value ends with the search value. */ public function __construct( public readonly string $field, public readonly string $value, public readonly ?bool $caseSensitive = null, + public readonly LikeMode $mode = LikeMode::CONTAINS, ) { } } diff --git a/src/Reader/Filter/LikeMode.php b/src/Reader/Filter/LikeMode.php new file mode 100644 index 0000000..7b6841f --- /dev/null +++ b/src/Reader/Filter/LikeMode.php @@ -0,0 +1,26 @@ +caseSensitive === true - ? str_contains($itemValue, $filter->value) - : mb_stripos($itemValue, $filter->value) !== false; + return match ($filter->mode) { + LikeMode::CONTAINS => $this->matchContains($itemValue, $filter->value, $filter->caseSensitive), + LikeMode::STARTS_WITH => $this->matchStartsWith($itemValue, $filter->value, $filter->caseSensitive), + LikeMode::ENDS_WITH => $this->matchEndsWith($itemValue, $filter->value, $filter->caseSensitive), + }; + } + + private function matchContains(string $itemValue, string $searchValue, ?bool $caseSensitive): bool + { + return $caseSensitive === true + ? str_contains($itemValue, $searchValue) + : mb_stripos($itemValue, $searchValue) !== false; + } + + private function matchStartsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool + { + return $caseSensitive === true + ? str_starts_with($itemValue, $searchValue) + : mb_stripos($itemValue, $searchValue) === 0; + } + + private function matchEndsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool + { + if ($caseSensitive === true) { + return str_ends_with($itemValue, $searchValue); + } + + $searchLength = mb_strlen($searchValue); + if ($searchLength > mb_strlen($itemValue)) { + return false; + } + + return mb_strtolower(mb_substr($itemValue, -$searchLength)) === mb_strtolower($searchValue); } } diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 0e16b82..9b03323 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -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; @@ -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 tests + [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], + + // STARTS_WITH mode tests + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', false, LikeMode::STARTS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', true, LikeMode::STARTS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::STARTS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great Cat', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'Привет', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::STARTS_WITH], + + // ENDS_WITH mode tests + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', false, LikeMode::ENDS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', true, LikeMode::ENDS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::ENDS_WITH], + [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat Fighter', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'мир', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => 'das Öl'], 'value', 'öl', false, LikeMode::ENDS_WITH], + + // Edge cases + [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::CONTAINS], + [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::ENDS_WITH], + [true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::STARTS_WITH], + [true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::ENDS_WITH], + [false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::STARTS_WITH], + [false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::ENDS_WITH], + ]; + } + + #[DataProvider('matchWithModeDataProvider')] + public function testMatchWithMode( + bool $expected, + array $item, + string $field, + string $value, + ?bool $caseSensitive, + LikeMode $mode + ): void { + $filterHandler = new LikeHandler(); + $context = new Context([], new FlatValueReader()); + $this->assertSame( + $expected, + $filterHandler->match($item, new Like($field, $value, $caseSensitive, $mode), $context) + ); + } + + public function testBackwardCompatibility(): void + { + $filterHandler = new LikeHandler(); + $context = new Context([], new FlatValueReader()); + $item = ['id' => 1, 'value' => 'Great Cat Fighter']; + + // Test that old constructor still works (defaults to CONTAINS mode) + $oldFilter = new Like('value', 'Cat'); + $newFilter = new Like('value', 'Cat', null, LikeMode::CONTAINS); + + $this->assertSame( + $filterHandler->match($item, $oldFilter, $context), + $filterHandler->match($item, $newFilter, $context) + ); + } } From 5473bcddad745c96af1d8a2b55a9ce14abc765e3 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 20:00:35 +0000 Subject: [PATCH 03/19] Apply fixes from StyleCI --- src/Reader/Filter/LikeMode.php | 6 +++--- src/Reader/Iterable/FilterHandler/LikeHandler.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Reader/Filter/LikeMode.php b/src/Reader/Filter/LikeMode.php index 7b6841f..91c57e1 100644 --- a/src/Reader/Filter/LikeMode.php +++ b/src/Reader/Filter/LikeMode.php @@ -13,14 +13,14 @@ enum LikeMode * Field value contains the search value. */ case CONTAINS; - + /** * Field value starts with the search value. */ case STARTS_WITH; - + /** * Field value ends with the search value. */ case ENDS_WITH; -} \ No newline at end of file +} diff --git a/src/Reader/Iterable/FilterHandler/LikeHandler.php b/src/Reader/Iterable/FilterHandler/LikeHandler.php index 9fa4f3a..c001abb 100644 --- a/src/Reader/Iterable/FilterHandler/LikeHandler.php +++ b/src/Reader/Iterable/FilterHandler/LikeHandler.php @@ -57,12 +57,12 @@ private function matchEndsWith(string $itemValue, string $searchValue, ?bool $ca if ($caseSensitive === true) { return str_ends_with($itemValue, $searchValue); } - + $searchLength = mb_strlen($searchValue); if ($searchLength > mb_strlen($itemValue)) { return false; } - + return mb_strtolower(mb_substr($itemValue, -$searchLength)) === mb_strtolower($searchValue); } } From b2c3f4127de96f5d42f5f6c40cff598124c8dd73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:03:27 +0000 Subject: [PATCH 04/19] Fix empty string handling and add comprehensive tests for Like filter modes Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../Iterable/FilterHandler/LikeHandler.php | 12 +++++++ .../BaseReaderWithLikeTestCase.php | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Reader/Iterable/FilterHandler/LikeHandler.php b/src/Reader/Iterable/FilterHandler/LikeHandler.php index c001abb..2e304c4 100644 --- a/src/Reader/Iterable/FilterHandler/LikeHandler.php +++ b/src/Reader/Iterable/FilterHandler/LikeHandler.php @@ -40,6 +40,10 @@ public function match(object|array $item, FilterInterface $filter, Context $cont private function matchContains(string $itemValue, string $searchValue, ?bool $caseSensitive): bool { + if ($searchValue === '') { + return true; // Empty string is contained in any string + } + return $caseSensitive === true ? str_contains($itemValue, $searchValue) : mb_stripos($itemValue, $searchValue) !== false; @@ -47,6 +51,10 @@ private function matchContains(string $itemValue, string $searchValue, ?bool $ca private function matchStartsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool { + if ($searchValue === '') { + return true; // Empty string matches the start of any string + } + return $caseSensitive === true ? str_starts_with($itemValue, $searchValue) : mb_stripos($itemValue, $searchValue) === 0; @@ -54,6 +62,10 @@ private function matchStartsWith(string $itemValue, string $searchValue, ?bool $ private function matchEndsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool { + if ($searchValue === '') { + return true; // Empty string matches the end of any string + } + if ($caseSensitive === true) { return str_ends_with($itemValue, $searchValue); } diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 5e7dbe3..52c6610 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -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 @@ -34,4 +35,35 @@ 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 tests (should work like before) + 'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::CONTAINS, [2]], + 'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::CONTAINS, [2]], + + // STARTS_WITH mode tests + 'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::STARTS_WITH, [2]], + 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], + 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], + + // ENDS_WITH mode tests + 'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::ENDS_WITH, [2]], + 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], + 'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::ENDS_WITH, []], + ]; + } + + #[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)); + $this->assertFixtures($expectedFixtureIndexes, $reader->read()); + } } From dbb695f24ad337b6efc6505e85ef7962ce1c8260 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 20:03:38 +0000 Subject: [PATCH 05/19] Apply fixes from StyleCI --- src/Reader/Iterable/FilterHandler/LikeHandler.php | 6 +++--- .../Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Reader/Iterable/FilterHandler/LikeHandler.php b/src/Reader/Iterable/FilterHandler/LikeHandler.php index 2e304c4..b111a77 100644 --- a/src/Reader/Iterable/FilterHandler/LikeHandler.php +++ b/src/Reader/Iterable/FilterHandler/LikeHandler.php @@ -43,7 +43,7 @@ private function matchContains(string $itemValue, string $searchValue, ?bool $ca if ($searchValue === '') { return true; // Empty string is contained in any string } - + return $caseSensitive === true ? str_contains($itemValue, $searchValue) : mb_stripos($itemValue, $searchValue) !== false; @@ -54,7 +54,7 @@ private function matchStartsWith(string $itemValue, string $searchValue, ?bool $ if ($searchValue === '') { return true; // Empty string matches the start of any string } - + return $caseSensitive === true ? str_starts_with($itemValue, $searchValue) : mb_stripos($itemValue, $searchValue) === 0; @@ -65,7 +65,7 @@ private function matchEndsWith(string $itemValue, string $searchValue, ?bool $ca if ($searchValue === '') { return true; // Empty string matches the end of any string } - + if ($caseSensitive === true) { return str_ends_with($itemValue, $searchValue); } diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 52c6610..f3a9e6c 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -42,12 +42,12 @@ public static function dataWithReaderAndMode(): array // CONTAINS mode tests (should work like before) 'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::CONTAINS, [2]], 'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::CONTAINS, [2]], - + // STARTS_WITH mode tests 'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::STARTS_WITH, [2]], 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], - + // ENDS_WITH mode tests 'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::ENDS_WITH, [2]], 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], From 9ebe43bcbc1897f120ceb65948251eccfa759565 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:34:06 +0000 Subject: [PATCH 06/19] Add clarifying comments to test files for better readability Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php | 6 +++--- tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index f3a9e6c..cea5afe 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -39,16 +39,16 @@ public function testWithReader( public static function dataWithReaderAndMode(): array { return [ - // CONTAINS mode tests (should work like before) + // CONTAINS mode 'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::CONTAINS, [2]], 'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::CONTAINS, [2]], - // STARTS_WITH mode tests + // STARTS_WITH mode 'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::STARTS_WITH, [2]], 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], - // ENDS_WITH mode tests + // ENDS_WITH mode 'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::ENDS_WITH, [2]], 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], 'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::ENDS_WITH, []], diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 9b03323..1e711a5 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -53,7 +53,7 @@ public function testMatch(bool $expected, array $item, string $field, string $va public static function matchWithModeDataProvider(): array { return [ - // CONTAINS mode tests + // 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], @@ -70,7 +70,7 @@ public static function matchWithModeDataProvider(): array [true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'Привет', null, LikeMode::STARTS_WITH], [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::STARTS_WITH], - // ENDS_WITH mode tests + // ENDS_WITH mode [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::ENDS_WITH], [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', false, LikeMode::ENDS_WITH], [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', true, LikeMode::ENDS_WITH], @@ -115,7 +115,7 @@ public function testBackwardCompatibility(): void $context = new Context([], new FlatValueReader()); $item = ['id' => 1, 'value' => 'Great Cat Fighter']; - // Test that old constructor still works (defaults to CONTAINS mode) + // Test that constructor defaults to CONTAINS mode $oldFilter = new Like('value', 'Cat'); $newFilter = new Like('value', 'Cat', null, LikeMode::CONTAINS); From b207e40560ced06ef9e6b28fefea9e258df4ecfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:36:29 +0000 Subject: [PATCH 07/19] Improve test readability by adding explicit comments about expected results Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../BaseReaderWithLikeTestCase.php | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index cea5afe..a52c35c 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -40,18 +40,18 @@ public static function dataWithReaderAndMode(): array { return [ // CONTAINS mode - 'contains: same case, case sensitive: null' => ['email', 'ed@be', null, LikeMode::CONTAINS, [2]], - 'contains: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::CONTAINS, [2]], + '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::STARTS_WITH, [2]], - 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], - 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], + 'starts with: same case, case sensitive: null' => ['email', 'seed@', null, LikeMode::STARTS_WITH, [2]], // Expects: seed@beat + 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], // Expects: seed@beat + 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], // Expects: no matches // ENDS_WITH mode - 'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::ENDS_WITH, [2]], - 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], - 'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::ENDS_WITH, []], + 'ends with: same case, case sensitive: null' => ['email', '@beat', null, LikeMode::ENDS_WITH, [2]], // Expects: seed@beat + 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], // Expects: seed@beat + 'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::ENDS_WITH, []], // Expects: no matches ]; } @@ -64,6 +64,9 @@ public function testWithReaderAndMode( array $expectedFixtureIndexes, ): void { $reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive, $mode)); - $this->assertFixtures($expectedFixtureIndexes, $reader->read()); + $actualData = $reader->read(); + + // Assert that we get the expected fixtures based on the filter criteria + $this->assertFixtures($expectedFixtureIndexes, $actualData); } } From 74e8ab3bc125ff3082d1c1c68dd64fbe35e7ab37 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 21:36:36 +0000 Subject: [PATCH 08/19] Apply fixes from StyleCI --- .../Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index a52c35c..2941506 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -65,7 +65,7 @@ public function testWithReaderAndMode( ): 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); } From 6edd5066722732ab9d0f50d926c2377e1136038e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:51:09 +0000 Subject: [PATCH 09/19] Rename test method and use assertSame directly in tests Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../ReaderWithFilter/BaseReaderWithLikeTestCase.php | 9 +++++++-- tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 2941506..0bbef6b 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -66,7 +66,12 @@ public function testWithReaderAndMode( $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); + // Build expected fixtures array directly + $expectedFixtures = []; + foreach ($expectedFixtureIndexes as $index) { + $expectedFixtures[$index] = $this->getFixture($index); + } + + $this->assertSame($expectedFixtures, $actualData); } } diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 1e711a5..cbc4727 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -109,7 +109,7 @@ public function testMatchWithMode( ); } - public function testBackwardCompatibility(): void + public function testConstructorDefaultMode(): void { $filterHandler = new LikeHandler(); $context = new Context([], new FlatValueReader()); From db04aeb4503484bbed248543099cb4e4e6575c17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 21:56:41 +0000 Subject: [PATCH 10/19] Add CHANGELOG entry for Like filter matching modes enhancement Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f72aa..37bb04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `LikeMode` enum and `mode` parameter to `Like` filter for starts with, ends with, and contains matching modes (@copilot) ## 1.0.1 January 25, 2023 From 49016e68ad020687d2f8ef93a5a80c7d11138478 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:05:05 +0000 Subject: [PATCH 11/19] Revert test to use assertFixtures instead of assertSame as requested Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../ReaderWithFilter/BaseReaderWithLikeTestCase.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 0bbef6b..2941506 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -66,12 +66,7 @@ public function testWithReaderAndMode( $reader = $this->getReader()->withFilter(new Like($field, $value, $caseSensitive, $mode)); $actualData = $reader->read(); - // Build expected fixtures array directly - $expectedFixtures = []; - foreach ($expectedFixtureIndexes as $index) { - $expectedFixtures[$index] = $this->getFixture($index); - } - - $this->assertSame($expectedFixtures, $actualData); + // Assert that we get the expected fixtures based on the filter criteria + $this->assertFixtures($expectedFixtureIndexes, $actualData); } } From 900f2f69847eb5b193f6d92bfd22d73846d74181 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:25:37 +0000 Subject: [PATCH 12/19] Address review feedback: fix formatting and update attribution Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- CHANGELOG.md | 2 +- .../Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php | 1 - tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37bb04e..0b659d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +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 `LikeMode` enum and `mode` parameter to `Like` filter for starts with, ends with, and contains matching modes (@copilot) +- New #200: Add `LikeMode` enum and `mode` parameter to `Like` filter for starts with, ends with, and contains matching modes (@samdark) ## 1.0.1 January 25, 2023 diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 2941506..1d1e07c 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -65,7 +65,6 @@ public function testWithReaderAndMode( ): 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); } diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index cbc4727..3733dee 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -60,7 +60,7 @@ public static function matchWithModeDataProvider(): array [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::CONTAINS], [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::CONTAINS], - // STARTS_WITH mode tests + // STARTS_WITH mode [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::STARTS_WITH], [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', false, LikeMode::STARTS_WITH], [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', true, LikeMode::STARTS_WITH], From 7a7b5ccaf0a7583f45d2690cf5e7f58d67a7a0eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:58:46 +0000 Subject: [PATCH 13/19] Add Unicode test cases to catch escaped mutants in LikeHandler Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../Reader/Iterable/FilterHandler/LikeHandlerTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 3733dee..c4fd34a 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -81,6 +81,17 @@ public static function matchWithModeDataProvider(): array [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::ENDS_WITH], [true, ['id' => 1, 'value' => 'das Öl'], 'value', 'öl', false, LikeMode::ENDS_WITH], + // Unicode test cases to catch mutants that replace mb_* functions + // Test case for mb_stripos vs stripos in STARTS_WITH (catches mutant 1) + // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç + [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], + // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant 2) + // strlen('café') = 5 bytes, mb_strlen('café') = 4 characters + [false, ['id' => 1, 'value' => 'café'], 'value', 'toolong', false, LikeMode::ENDS_WITH], + // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant 3) + // strtolower doesn't properly convert Turkish Ü to ü + [true, ['id' => 1, 'value' => 'MÜDÜR'], 'value', 'üdür', false, LikeMode::ENDS_WITH], + // Edge cases [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::CONTAINS], [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::STARTS_WITH], From 6ec3d070b3ba8056be79c60adfbf88b94118841d Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 22:58:56 +0000 Subject: [PATCH 14/19] Apply fixes from StyleCI --- tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index c4fd34a..8fe7521 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -85,7 +85,7 @@ public static function matchWithModeDataProvider(): array // Test case for mb_stripos vs stripos in STARTS_WITH (catches mutant 1) // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], - // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant 2) + // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant 2) // strlen('café') = 5 bytes, mb_strlen('café') = 4 characters [false, ['id' => 1, 'value' => 'café'], 'value', 'toolong', false, LikeMode::ENDS_WITH], // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant 3) From 7bff5b90ed5d5056845f0f0ad2672a065a0c485a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:15:17 +0000 Subject: [PATCH 15/19] Add better test cases to catch escaped mutants in LikeHandler Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../FilterHandler/LikeHandlerTest.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 8fe7521..7903e51 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -85,12 +85,20 @@ public static function matchWithModeDataProvider(): array // Test case for mb_stripos vs stripos in STARTS_WITH (catches mutant 1) // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], - // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant 2) - // strlen('café') = 5 bytes, mb_strlen('café') = 4 characters - [false, ['id' => 1, 'value' => 'café'], 'value', 'toolong', false, LikeMode::ENDS_WITH], - // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant 3) - // strtolower doesn't properly convert Turkish Ü to ü - [true, ['id' => 1, 'value' => 'MÜDÜR'], 'value', 'üdür', false, LikeMode::ENDS_WITH], + + // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant on line 74) + // itemValue = 'café' (4 chars, 5 bytes), searchValue = 'abcde' (5 chars, 5 bytes) + // With mb_strlen: 5 > 4 = true (returns false - correct behavior) + // With strlen: 5 > 5 = false (proceeds to comparison - incorrect, should have returned false) + // The mutant will incorrectly proceed to compare when it should return false early + [false, ['id' => 1, 'value' => 'café'], 'value', 'abcde', false, LikeMode::ENDS_WITH], + + // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant on line 78) + // Use Turkish İ which strtolower doesn't handle properly + // itemValue ends with Turkish İ, searchValue is also Turkish İ + // mb_strtolower('İ') = 'i̇', strtolower('İ') = 'İ' (unchanged) + // Original: 'i̇' === 'i̇' = true, Mutant: 'i̇' === 'İ' = false + [true, ['id' => 1, 'value' => 'aliİ'], 'value', 'İ', false, LikeMode::ENDS_WITH], // Edge cases [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::CONTAINS], From 32e9676565f6af2260f0ef10bbe75cbcb3701f81 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 23:15:25 +0000 Subject: [PATCH 16/19] Apply fixes from StyleCI --- tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 7903e51..564da1e 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -85,14 +85,14 @@ public static function matchWithModeDataProvider(): array // Test case for mb_stripos vs stripos in STARTS_WITH (catches mutant 1) // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], - + // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant on line 74) // itemValue = 'café' (4 chars, 5 bytes), searchValue = 'abcde' (5 chars, 5 bytes) // With mb_strlen: 5 > 4 = true (returns false - correct behavior) // With strlen: 5 > 5 = false (proceeds to comparison - incorrect, should have returned false) // The mutant will incorrectly proceed to compare when it should return false early [false, ['id' => 1, 'value' => 'café'], 'value', 'abcde', false, LikeMode::ENDS_WITH], - + // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant on line 78) // Use Turkish İ which strtolower doesn't handle properly // itemValue ends with Turkish İ, searchValue is also Turkish İ From d9e7cbd0f0f8d733e7b08ff1185ca7d62a6ccac7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:33:49 +0000 Subject: [PATCH 17/19] Add targeted test cases to catch mb_strlen mutation in LikeHandler Co-authored-by: samdark <47294+samdark@users.noreply.github.com> --- .../Iterable/FilterHandler/LikeHandlerTest.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 564da1e..8d20b44 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -86,12 +86,18 @@ public static function matchWithModeDataProvider(): array // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], - // Test case for mb_strlen vs strlen in ENDS_WITH (catches mutant on line 74) - // itemValue = 'café' (4 chars, 5 bytes), searchValue = 'abcde' (5 chars, 5 bytes) - // With mb_strlen: 5 > 4 = true (returns false - correct behavior) - // With strlen: 5 > 5 = false (proceeds to comparison - incorrect, should have returned false) - // The mutant will incorrectly proceed to compare when it should return false early - [false, ['id' => 1, 'value' => 'café'], 'value', 'abcde', false, LikeMode::ENDS_WITH], + // Test case for mb_strlen vs strlen in ENDS_WITH (attempts to catch mutant on line 74) + // This tests the critical edge case where mb_strlen and strlen differ significantly + // itemValue = '🌟' (1 char, 4 bytes), searchValue = 'xyz🌟' (4 chars, 7 bytes) + // Original: mb_strlen('xyz🌟') > mb_strlen('🌟') → 4 > 1 → returns false (correct) + // Mutant: mb_strlen('xyz🌟') > strlen('🌟') → 4 > 4 → false, proceeds to comparison + // The mutant incorrectly proceeds when it should return false early + [false, ['id' => 1, 'value' => '🌟'], 'value', 'xyz🌟', false, LikeMode::ENDS_WITH], + + // Additional test case for the same mutant with different multi-byte scenario + // itemValue = 'é🎉' (2 chars, 6 bytes), searchValue = 'abcé🎉' (5 chars, 9 bytes) + // Original: 5 > 2 → returns false, Mutant: 5 > 6 → false, proceeds to comparison + [false, ['id' => 1, 'value' => 'é🎉'], 'value', 'abcé🎉', false, LikeMode::ENDS_WITH], // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant on line 78) // Use Turkish İ which strtolower doesn't handle properly From 6c77b27f9b16b837e856498b4efe90c9f49a8e0b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 29 Jul 2025 23:33:58 +0000 Subject: [PATCH 18/19] Apply fixes from StyleCI --- tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 8d20b44..4c75680 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -93,7 +93,7 @@ public static function matchWithModeDataProvider(): array // Mutant: mb_strlen('xyz🌟') > strlen('🌟') → 4 > 4 → false, proceeds to comparison // The mutant incorrectly proceeds when it should return false early [false, ['id' => 1, 'value' => '🌟'], 'value', 'xyz🌟', false, LikeMode::ENDS_WITH], - + // Additional test case for the same mutant with different multi-byte scenario // itemValue = 'é🎉' (2 chars, 6 bytes), searchValue = 'abcé🎉' (5 chars, 9 bytes) // Original: 5 > 2 → returns false, Mutant: 5 > 6 → false, proceeds to comparison From 91af5489d8ef45abad522f9eeea5e6d6276eb597 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 6 Aug 2025 08:34:55 +0300 Subject: [PATCH 19/19] improve --- CHANGELOG.md | 2 +- src/Reader/Filter/Like.php | 8 +- src/Reader/Filter/LikeMode.php | 6 +- .../Iterable/FilterHandler/LikeHandler.php | 45 +++---- .../BaseReaderWithLikeTestCase.php | 16 +-- .../FilterHandler/LikeHandlerTest.php | 117 +++++++----------- 6 files changed, 76 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b659d7..0f5cfb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +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 `LikeMode` enum and `mode` parameter to `Like` filter for starts with, ends with, and contains matching modes (@samdark) +- New #200: Add matching mode parameter to `Like` filter (@samdark, @vjik) ## 1.0.1 January 25, 2023 diff --git a/src/Reader/Filter/Like.php b/src/Reader/Filter/Like.php index 2bf41bb..91b3bc3 100644 --- a/src/Reader/Filter/Like.php +++ b/src/Reader/Filter/Like.php @@ -19,17 +19,13 @@ final class Like implements FilterInterface * - `null` - depends on implementation; * - `true` - case-sensitive; * - `false` - case-insensitive. - * @param LikeMode $mode Matching mode: - * - * - `LikeMode::CONTAINS` - field value contains the search value (default); - * - `LikeMode::STARTS_WITH` - field value starts with the search value; - * - `LikeMode::ENDS_WITH` - field value ends with the search value. + * @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, + public readonly LikeMode $mode = LikeMode::Contains, ) { } } diff --git a/src/Reader/Filter/LikeMode.php b/src/Reader/Filter/LikeMode.php index 91c57e1..fb62a21 100644 --- a/src/Reader/Filter/LikeMode.php +++ b/src/Reader/Filter/LikeMode.php @@ -12,15 +12,15 @@ enum LikeMode /** * Field value contains the search value. */ - case CONTAINS; + case Contains; /** * Field value starts with the search value. */ - case STARTS_WITH; + case StartsWith; /** * Field value ends with the search value. */ - case ENDS_WITH; + case EndsWith; } diff --git a/src/Reader/Iterable/FilterHandler/LikeHandler.php b/src/Reader/Iterable/FilterHandler/LikeHandler.php index b111a77..204b75c 100644 --- a/src/Reader/Iterable/FilterHandler/LikeHandler.php +++ b/src/Reader/Iterable/FilterHandler/LikeHandler.php @@ -31,50 +31,37 @@ public function match(object|array $item, FilterInterface $filter, Context $cont return false; } + if ($filter->value === '') { + return true; + } + return match ($filter->mode) { - LikeMode::CONTAINS => $this->matchContains($itemValue, $filter->value, $filter->caseSensitive), - LikeMode::STARTS_WITH => $this->matchStartsWith($itemValue, $filter->value, $filter->caseSensitive), - LikeMode::ENDS_WITH => $this->matchEndsWith($itemValue, $filter->value, $filter->caseSensitive), + 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 $itemValue, string $searchValue, ?bool $caseSensitive): bool + private function matchContains(string $value, string $search, ?bool $caseSensitive): bool { - if ($searchValue === '') { - return true; // Empty string is contained in any string - } - return $caseSensitive === true - ? str_contains($itemValue, $searchValue) - : mb_stripos($itemValue, $searchValue) !== false; + ? str_contains($value, $search) + : mb_stripos($value, $search) !== false; } - private function matchStartsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool + private function matchStartsWith(string $value, string $search, ?bool $caseSensitive): bool { - if ($searchValue === '') { - return true; // Empty string matches the start of any string - } - return $caseSensitive === true - ? str_starts_with($itemValue, $searchValue) - : mb_stripos($itemValue, $searchValue) === 0; + ? str_starts_with($value, $search) + : mb_stripos($value, $search) === 0; } - private function matchEndsWith(string $itemValue, string $searchValue, ?bool $caseSensitive): bool + private function matchEndsWith(string $value, string $search, ?bool $caseSensitive): bool { - if ($searchValue === '') { - return true; // Empty string matches the end of any string - } - if ($caseSensitive === true) { - return str_ends_with($itemValue, $searchValue); - } - - $searchLength = mb_strlen($searchValue); - if ($searchLength > mb_strlen($itemValue)) { - return false; + return str_ends_with($value, $search); } - return mb_strtolower(mb_substr($itemValue, -$searchLength)) === mb_strtolower($searchValue); + return mb_strtolower(mb_substr($value, -mb_strlen($search))) === mb_strtolower($search); } } diff --git a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php index 1d1e07c..cc9dbae 100644 --- a/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php +++ b/tests/Common/Reader/ReaderWithFilter/BaseReaderWithLikeTestCase.php @@ -40,18 +40,18 @@ 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 + '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::STARTS_WITH, [2]], // Expects: seed@beat - 'starts with: different case, case sensitive: false' => ['email', 'SEED@', false, LikeMode::STARTS_WITH, [2]], // Expects: seed@beat - 'starts with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::STARTS_WITH, []], // Expects: no matches + '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::ENDS_WITH, [2]], // Expects: seed@beat - 'ends with: different case, case sensitive: false' => ['email', '@BEAT', false, LikeMode::ENDS_WITH, [2]], // Expects: seed@beat - 'ends with: middle part (should fail)' => ['email', 'ed@be', null, LikeMode::ENDS_WITH, []], // Expects: no matches + '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 ]; } diff --git a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php index 4c75680..87beeda 100644 --- a/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php +++ b/tests/Reader/Iterable/FilterHandler/LikeHandlerTest.php @@ -53,67 +53,46 @@ public function testMatch(bool $expected, array $item, string $field, string $va 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], - - // STARTS_WITH mode - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::STARTS_WITH], - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', false, LikeMode::STARTS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'great', true, LikeMode::STARTS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::STARTS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::STARTS_WITH], - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great Cat', null, LikeMode::STARTS_WITH], - [true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'Привет', null, LikeMode::STARTS_WITH], - [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::STARTS_WITH], - - // ENDS_WITH mode - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Fighter', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', false, LikeMode::ENDS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'fighter', true, LikeMode::ENDS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Great', null, LikeMode::ENDS_WITH], - [false, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => 'Great Cat Fighter'], 'value', 'Cat Fighter', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => 'Привет мир'], 'value', 'мир', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => '🙁🙂🙁'], 'value', '🙁', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => 'das Öl'], 'value', 'öl', false, LikeMode::ENDS_WITH], - - // Unicode test cases to catch mutants that replace mb_* functions - // Test case for mb_stripos vs stripos in STARTS_WITH (catches mutant 1) - // stripos would return false, mb_stripos returns 0 for Turkish Ç/ç - [true, ['id' => 1, 'value' => 'Çağrı'], 'value', 'çağ', false, LikeMode::STARTS_WITH], - - // Test case for mb_strlen vs strlen in ENDS_WITH (attempts to catch mutant on line 74) - // This tests the critical edge case where mb_strlen and strlen differ significantly - // itemValue = '🌟' (1 char, 4 bytes), searchValue = 'xyz🌟' (4 chars, 7 bytes) - // Original: mb_strlen('xyz🌟') > mb_strlen('🌟') → 4 > 1 → returns false (correct) - // Mutant: mb_strlen('xyz🌟') > strlen('🌟') → 4 > 4 → false, proceeds to comparison - // The mutant incorrectly proceeds when it should return false early - [false, ['id' => 1, 'value' => '🌟'], 'value', 'xyz🌟', false, LikeMode::ENDS_WITH], - - // Additional test case for the same mutant with different multi-byte scenario - // itemValue = 'é🎉' (2 chars, 6 bytes), searchValue = 'abcé🎉' (5 chars, 9 bytes) - // Original: 5 > 2 → returns false, Mutant: 5 > 6 → false, proceeds to comparison - [false, ['id' => 1, 'value' => 'é🎉'], 'value', 'abcé🎉', false, LikeMode::ENDS_WITH], - - // Test case for mb_strtolower vs strtolower in ENDS_WITH (catches mutant on line 78) - // Use Turkish İ which strtolower doesn't handle properly - // itemValue ends with Turkish İ, searchValue is also Turkish İ - // mb_strtolower('İ') = 'i̇', strtolower('İ') = 'İ' (unchanged) - // Original: 'i̇' === 'i̇' = true, Mutant: 'i̇' === 'İ' = false - [true, ['id' => 1, 'value' => 'aliİ'], 'value', 'İ', false, LikeMode::ENDS_WITH], + // "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::STARTS_WITH], - [true, ['id' => 1, 'value' => 'test'], 'value', '', null, LikeMode::ENDS_WITH], - [true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::STARTS_WITH], - [true, ['id' => 1, 'value' => 'test'], 'value', 'test', null, LikeMode::ENDS_WITH], - [false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::STARTS_WITH], - [false, ['id' => 1, 'value' => 'test'], 'value', 'longer', null, LikeMode::ENDS_WITH], + [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], ]; } @@ -124,29 +103,25 @@ public function testMatchWithMode( string $field, string $value, ?bool $caseSensitive, - LikeMode $mode + LikeMode $mode, ): void { - $filterHandler = new LikeHandler(); + $handler = new LikeHandler(); $context = new Context([], new FlatValueReader()); + $filter = new Like($field, $value, $caseSensitive, $mode); + $this->assertSame( $expected, - $filterHandler->match($item, new Like($field, $value, $caseSensitive, $mode), $context) + $handler->match($item, $filter, $context) ); } public function testConstructorDefaultMode(): void { - $filterHandler = new LikeHandler(); + $handler = new LikeHandler(); $context = new Context([], new FlatValueReader()); $item = ['id' => 1, 'value' => 'Great Cat Fighter']; - // Test that constructor defaults to CONTAINS mode - $oldFilter = new Like('value', 'Cat'); - $newFilter = new Like('value', 'Cat', null, LikeMode::CONTAINS); - - $this->assertSame( - $filterHandler->match($item, $oldFilter, $context), - $filterHandler->match($item, $newFilter, $context) - ); + $this->assertTrue($handler->match($item, new Like('value', 'Cat'), $context)); + $this->assertFalse($handler->match($item, new Like('value', 'Hello'), $context)); } }