diff --git a/src/Standard.php b/src/Standard.php index 2708cd91..3e5ad952 100644 --- a/src/Standard.php +++ b/src/Standard.php @@ -28,6 +28,7 @@ use Generator; use Iterator; use IteratorAggregate; +use ReflectionFunction; use Traversable; use Override; @@ -49,6 +50,7 @@ use function mt_getrandmax; use function mt_rand; use function array_keys; +use function array_walk; /** * Concrete pipeline with sensible default callbacks. @@ -1395,31 +1397,45 @@ public function each(callable $func, bool $discard = true): void } } - /** - * @param callable(TValue, TKey=): void $func - */ private function eachInternal(callable $func): void { if ($this->empty()) { return; } + $func = self::wrapInternalCallable($func); + + if (is_array($this->pipeline)) { + // 5% faster + array_walk($this->pipeline, $func); + return; + } + foreach ($this->pipeline as $key => $value) { - try { - $func($value, $key); - } catch (ArgumentCountError) { - // Optimization to reduce the number of argument count errors when calling internal callables. - // This error is thrown when too many arguments are passed to a built-in function (that are sensitive - // to extra arguments), so we can wrap it to prevent the errors later. On the other hand, if there - // are too little arguments passed, it will blow up just a line later. - $func = self::wrapInternalCallable($func); - $func($value, $key); - } + $func($value, $key); } } + /** + * Wraps internal functions with strict arity with a callable to prevent ArgumentCountError. + */ private static function wrapInternalCallable(callable $func): callable { - return static fn($value) => $func($value); + $ref = new ReflectionFunction($func(...)); + + if ($ref->isUserDefined()) { + return $func; + } + + if ($ref->isVariadic()) { + return $func; + } + + if (1 !== $ref->getNumberOfParameters()) { + return $func; + } + + // User-defined functions silently ignore extra args + return fn($a) => $func($a); } } diff --git a/tests/EachTest.php b/tests/EachTest.php index fd46a5b8..65b244b1 100644 --- a/tests/EachTest.php +++ b/tests/EachTest.php @@ -26,7 +26,6 @@ use Pipeline\Standard; use ArrayIterator; use SplQueue; -use Tests\Pipeline\Fixtures\CallableThrower; use function Pipeline\map; use function Pipeline\take; @@ -195,24 +194,4 @@ public function testArgumentCountError(): void $this->expectExceptionMessage('Too few arguments'); $pipeline->each(static function ($a, $b, $c): void {}); } - - /** - * Test that the reassignment of the callable inside the loop will affect all iterations. - */ - public function testCallableReassigned(): void - { - $callback = new CallableThrower(); - - $pipeline = fromArray(['1', '2', '3']); - $pipeline->each($callback); - - $this->assertSame(4, $callback->callCount, 'Expected 1 initial call that throws + 3 successful calls after wrapping'); - - $this->assertSame([ - ['1', 0], - ['1'], - ['2'], - ['3'], - ], $callback->args); - } } diff --git a/tests/Fixtures/CallableThrower.php b/tests/Fixtures/CallableThrower.php deleted file mode 100644 index 83a23f66..00000000 --- a/tests/Fixtures/CallableThrower.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -namespace Tests\Pipeline\Fixtures; - -use ArgumentCountError; - -class CallableThrower -{ - public array $args = []; - public int $callCount = 0; - - public function __invoke(...$args): void - { - $this->args[] = $args; - $this->callCount++; - - // @phpstan-ignore-next-line - if (1 === $this->callCount) { - throw new ArgumentCountError(); - } - } -}