From d0adef61a4f78f931fe9ae5baa7518bcf15ffc5c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:09:39 +0700 Subject: [PATCH 1/9] =?UTF-8?q?Version=202:=20=F0=9F=9A=80=20Faster=20proc?= =?UTF-8?q?ess=20with=20early=20validate=20filter=20before=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rector.php | 9 ++++++--- src/AtLeast.php | 6 +++--- src/Collector.php | 8 ++++++-- src/Finder.php | 19 ++++++++++--------- src/Only.php | 6 +++--- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/rector.php b/rector.php index b145f79..3231b06 100644 --- a/rector.php +++ b/rector.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; -use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector; return RectorConfig::configure() ->withPhpSets(php81: true) @@ -15,8 +15,11 @@ privatization: true, typeDeclarations: true ) - ->withSets([ - PHPUnitSetList::PHPUNIT_100, + ->withComposerBased(phpunit: true) + ->withSkip([ + BoolReturnTypeFromBooleanStrictReturnsRector::class => [ + __DIR__ . '/tests/FilterTest.php', + ], ]) ->withParallel() ->withRootFiles() diff --git a/src/AtLeast.php b/src/AtLeast.php index dada2db..14c76de 100644 --- a/src/AtLeast.php +++ b/src/AtLeast.php @@ -4,6 +4,7 @@ namespace ArrayLookup; +use ArrayLookup\Assert\Filter; use Traversable; use Webmozart\Assert\Assert; @@ -47,14 +48,13 @@ private static function atLeastFoundTimes( ): bool { // usage must be higher than 0 Assert::greaterThan($maxCount, 0); + // filter must be a callable with bool return type + Filter::boolean($filter); $totalFound = 0; foreach ($data as $key => $datum) { $isFound = $filter($datum, $key); - // returns of callable must be bool - Assert::boolean($isFound); - if (! $isFound) { continue; } diff --git a/src/Collector.php b/src/Collector.php index 23eb475..0449419 100644 --- a/src/Collector.php +++ b/src/Collector.php @@ -4,6 +4,7 @@ namespace ArrayLookup; +use ArrayLookup\Assert\Filter; use Traversable; use Webmozart\Assert\Assert; @@ -71,6 +72,11 @@ public function getResults(): array $collectedData = []; $isCallableWhen = is_callable($this->when); + if (is_callable($this->when)) { + // filter must be a callable with bool return type + Filter::boolean($this->when); + } + foreach ($this->data as $key => $datum) { if ($isCallableWhen) { /** @@ -79,8 +85,6 @@ public function getResults(): array $when = $this->when; $isFound = ($when)($datum, $key); - Assert::boolean($isFound); - if (! $isFound) { continue; } diff --git a/src/Finder.php b/src/Finder.php index c13c8a6..7fbeb37 100644 --- a/src/Finder.php +++ b/src/Finder.php @@ -5,6 +5,7 @@ namespace ArrayLookup; use ArrayIterator; +use ArrayLookup\Assert\Filter; use ArrayObject; use Traversable; use Webmozart\Assert\Assert; @@ -24,12 +25,12 @@ final class Finder */ public static function first(iterable $data, callable $filter, bool $returnKey = false): mixed { + // filter must be a callable with bool return type + Filter::boolean($filter); + foreach ($data as $key => $datum) { $isFound = $filter($datum, $key); - // returns of callable must be bool - Assert::boolean($isFound); - if (! $isFound) { continue; } @@ -71,6 +72,9 @@ public static function last( // ensure data is array for end(), key(), current(), prev() usage Assert::isArray($data); + // filter must be a callable with bool return type + Filter::boolean($filter); + // Use end(), key(), current(), prev() usage instead of array_reverse() // to avoid immediatelly got "Out of memory" on many data // see https://3v4l.org/IHo2H vs https://3v4l.org/Wqejc @@ -91,9 +95,6 @@ public static function last( $current = current($data); $isFound = $filter($current, $key); - // returns of callable must be bool - Assert::boolean($isFound); - if (! $isFound) { // go to previous row prev($data); @@ -133,12 +134,12 @@ public static function rows( $newKey = 0; $totalFound = 0; + // filter must be a callable with bool return type + Filter::boolean($filter); + foreach ($data as $key => $datum) { $isFound = $filter($datum, $key); - // returns of callable must be bool - Assert::boolean($isFound); - if (! $isFound) { continue; } diff --git a/src/Only.php b/src/Only.php index 83978fc..e296abd 100644 --- a/src/Only.php +++ b/src/Only.php @@ -4,6 +4,7 @@ namespace ArrayLookup; +use ArrayLookup\Assert\Filter; use Traversable; use Webmozart\Assert\Assert; @@ -47,14 +48,13 @@ private static function onlyFoundTimes( ): bool { // usage must be higher than 0 Assert::greaterThan($maxCount, 0); + // filter must be a callable with bool return type + Filter::boolean($filter); $totalFound = 0; foreach ($data as $key => $datum) { $isFound = $filter($datum, $key); - // returns of callable must be bool - Assert::boolean($isFound); - if (! $isFound) { continue; } From 8d50f6d90f51253750d36776fdaba8047aa9a501 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:09:48 +0700 Subject: [PATCH 2/9] add filter validation --- src/Assert/Filter.php | 39 ++++++++++++++++++++++++++++ tests/FilterTest.php | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/Assert/Filter.php create mode 100644 tests/FilterTest.php diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php new file mode 100644 index 0000000..f9b56e5 --- /dev/null +++ b/src/Assert/Filter.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace ArrayLookup\Assert; + +use InvalidArgumentException; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionNamedType; +use TypeError; + +use function sprintf; + +final class Filter +{ + public static function boolean(callable $filter): void + { + try { + $reflection = new ReflectionFunction($filter); + } catch (TypeError) { + $reflection = new ReflectionMethod($filter, '__invoke'); + } + + $returnType = $reflection->getReturnType(); + + if (! $returnType instanceof ReflectionNamedType) { + throw new InvalidArgumentException('Expected a bool return type on callable filter, null given'); + } + + $returnTypeName = $returnType->getName(); + if ($returnTypeName !== 'bool') { + throw new InvalidArgumentException(sprintf( + 'Expected a bool return type on callable filter, %s given', + $returnTypeName + )); + } + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php new file mode 100644 index 0000000..b559e0b --- /dev/null +++ b/tests/FilterTest.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace ArrayLookup\Tests; + +use ArrayLookup\AtLeast; +use InvalidArgumentException; +use PHPUnit\Framework\TestCase; + +final class FilterTest extends TestCase +{ + public function testOnceWithFilterInvokableClass(): void + { + $data = [1, 2, 3]; + $filter = new class { + public function __invoke(int $datum): bool + { + return $datum === 1; + } + }; + + $this->assertTrue(AtLeast::once($data, $filter)); + } + + public function testWithoutReturnTypeCallable(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a bool return type on callable filter, null given'); + + $data = [1, 2, 3]; + + // phpcs:disable + $filter = new class { + public function __invoke(int $datum) + { + return $datum === 1; + } + }; + // phpcs:enable + + $this->assertTrue(AtLeast::once($data, $filter)); + } + + public function testWithNonBoolReturnTypeCallable(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected a bool return type on callable filter, string given'); + + $data = [1, 2, 3]; + $filter = new class { + public function __invoke(int $datum): string + { + return 'test'; + } + }; + + $this->assertTrue(AtLeast::once($data, $filter)); + } +} From 87743dab2dbb74d91649f58ee480548207226f0f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:17:28 +0700 Subject: [PATCH 3/9] phpstan fix --- src/Assert/Filter.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php index f9b56e5..8dae94f 100644 --- a/src/Assert/Filter.php +++ b/src/Assert/Filter.php @@ -4,11 +4,12 @@ namespace ArrayLookup\Assert; +use Closure; use InvalidArgumentException; use ReflectionFunction; use ReflectionMethod; use ReflectionNamedType; -use TypeError; +use Webmozart\Assert\Assert; use function sprintf; @@ -16,10 +17,19 @@ final class Filter { public static function boolean(callable $filter): void { - try { + if ($filter instanceof Closure) { $reflection = new ReflectionFunction($filter); - } catch (TypeError) { + } elseif (is_object($filter)) { $reflection = new ReflectionMethod($filter, '__invoke'); + } else { + Assert::string($filter); + + if (! str_contains($filter, '::')) { + $reflection = new ReflectionFunction($filter); + } else { + [, $method] = explode('::', $filter); + $reflection = new ReflectionMethod($filter, $method); + } } $returnType = $reflection->getReturnType(); From 29b6d2b3fcc9adc9f93e697ef32d8473cd198327 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:17:51 +0700 Subject: [PATCH 4/9] cs fix --- src/Assert/Filter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php index 8dae94f..8d727bf 100644 --- a/src/Assert/Filter.php +++ b/src/Assert/Filter.php @@ -11,7 +11,10 @@ use ReflectionNamedType; use Webmozart\Assert\Assert; +use function explode; +use function is_object; use function sprintf; +use function str_contains; final class Filter { From 3e18640f1309853f106781345eedd905cdbed7fd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:19:00 +0700 Subject: [PATCH 5/9] cs fix --- src/Collector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Collector.php b/src/Collector.php index 0449419..5fce33c 100644 --- a/src/Collector.php +++ b/src/Collector.php @@ -72,7 +72,7 @@ public function getResults(): array $collectedData = []; $isCallableWhen = is_callable($this->when); - if (is_callable($this->when)) { + if ($isCallableWhen) { // filter must be a callable with bool return type Filter::boolean($this->when); } From 94f1a87d2d2e80ec74d9642103074b2ed0c357dd Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:27:47 +0700 Subject: [PATCH 6/9] fix phpstan --- src/Collector.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Collector.php b/src/Collector.php index 5fce33c..8b536b5 100644 --- a/src/Collector.php +++ b/src/Collector.php @@ -70,15 +70,14 @@ public function getResults(): array $count = 0; $collectedData = []; - $isCallableWhen = is_callable($this->when); - if ($isCallableWhen) { + if (is_callable($this->when)) { // filter must be a callable with bool return type Filter::boolean($this->when); } foreach ($this->data as $key => $datum) { - if ($isCallableWhen) { + if ($this->when !== null) { /** * @var callable(mixed $datum, int|string|null $key): bool $when */ From 358ef1596efd526c9fe4bb966a63d81f7a466f7e Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:39:28 +0700 Subject: [PATCH 7/9] Fix --- src/Assert/Filter.php | 12 +----------- src/Collector.php | 4 ++-- tests/FilterTest.php | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php index 8d727bf..44fd51f 100644 --- a/src/Assert/Filter.php +++ b/src/Assert/Filter.php @@ -9,12 +9,9 @@ use ReflectionFunction; use ReflectionMethod; use ReflectionNamedType; -use Webmozart\Assert\Assert; -use function explode; use function is_object; use function sprintf; -use function str_contains; final class Filter { @@ -25,14 +22,7 @@ public static function boolean(callable $filter): void } elseif (is_object($filter)) { $reflection = new ReflectionMethod($filter, '__invoke'); } else { - Assert::string($filter); - - if (! str_contains($filter, '::')) { - $reflection = new ReflectionFunction($filter); - } else { - [, $method] = explode('::', $filter); - $reflection = new ReflectionMethod($filter, $method); - } + throw new InvalidArgumentException('Expected Closure or invokable object, string given'); } $returnType = $reflection->getReturnType(); diff --git a/src/Collector.php b/src/Collector.php index 8b536b5..6ad5ae9 100644 --- a/src/Collector.php +++ b/src/Collector.php @@ -68,8 +68,8 @@ public function getResults(): array // ensure transform property is set early ->withTransform() method Assert::isCallable($this->transform); - $count = 0; - $collectedData = []; + $count = 0; + $collectedData = []; if (is_callable($this->when)) { // filter must be a callable with bool return type diff --git a/tests/FilterTest.php b/tests/FilterTest.php index b559e0b..ccebcd7 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -23,6 +23,17 @@ public function __invoke(int $datum): bool $this->assertTrue(AtLeast::once($data, $filter)); } + public function testOnceWithStringFilter(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected Closure or invokable object, string given'); + + $data = [1, 'f']; + $filter = 'is_string'; + + AtLeast::once($data, $filter); + } + public function testWithoutReturnTypeCallable(): void { $this->expectException(InvalidArgumentException::class); @@ -39,7 +50,7 @@ public function __invoke(int $datum) }; // phpcs:enable - $this->assertTrue(AtLeast::once($data, $filter)); + AtLeast::once($data, $filter); } public function testWithNonBoolReturnTypeCallable(): void @@ -55,6 +66,6 @@ public function __invoke(int $datum): string } }; - $this->assertTrue(AtLeast::once($data, $filter)); + AtLeast::once($data, $filter); } } From bc932e1b3147a0ba1264e479ef0072168125ef99 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:41:15 +0700 Subject: [PATCH 8/9] modernize message --- src/Assert/Filter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php index 44fd51f..41bd2a1 100644 --- a/src/Assert/Filter.php +++ b/src/Assert/Filter.php @@ -22,7 +22,9 @@ public static function boolean(callable $filter): void } elseif (is_object($filter)) { $reflection = new ReflectionMethod($filter, '__invoke'); } else { - throw new InvalidArgumentException('Expected Closure or invokable object, string given'); + throw new InvalidArgumentException( + sprintf('Expected Closure or invokable object, %s given', gettype($filter)) + ); } $returnType = $reflection->getReturnType(); From 29ea948261657626182bde6ea674db91fe989707 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan <samsonasik@gmail.com> Date: Sat, 28 Dec 2024 18:43:07 +0700 Subject: [PATCH 9/9] cs fix --- src/Assert/Filter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Assert/Filter.php b/src/Assert/Filter.php index 41bd2a1..1a46930 100644 --- a/src/Assert/Filter.php +++ b/src/Assert/Filter.php @@ -10,6 +10,7 @@ use ReflectionMethod; use ReflectionNamedType; +use function gettype; use function is_object; use function sprintf;