diff --git a/README.md b/README.md index f455d23..7c78457 100644 --- a/README.md +++ b/README.md @@ -103,23 +103,8 @@ echo stringify(new class { public int $property = 42; }) . PHP_EOL; echo stringify(new class extends WithProperties { }) . PHP_EOL; // `WithProperties@anonymous { +$publicProperty=true #$protectedProperty=42 }` -echo stringify('chr') . PHP_EOL; -// `chr(int $codepoint): string` - -echo stringify([new WithMethods(), 'publicMethod']) . PHP_EOL; -// `WithMethods->publicMethod(Iterator&Countable $parameter): ?static` - -echo stringify('WithMethods::publicStaticMethod') . PHP_EOL; -// `WithMethods::publicStaticMethod(int|float $parameter): void` - -echo stringify(['WithMethods', 'publicStaticMethod']) . PHP_EOL; -// `WithMethods::publicStaticMethod(int|float $parameter): void` - -echo stringify(new WithInvoke()) . PHP_EOL; -// `WithInvoke->__invoke(int $parameter = 0): never` - -echo stringify(static fn(int $foo): string => '') . PHP_EOL; -// `function (int $foo): string` +echo stringify(fn(int $foo): string => '') . PHP_EOL; +// `Closure { fn(int $foo): string }` echo stringify(new DateTime()) . PHP_EOL; // `DateTime { 2023-04-21T11:29:03+00:00 }` diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 1e228c6..0ead2f2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -32,4 +32,8 @@ tests/integration/ + + tests/integration + tests/unit/Stringifiers/CallableStringifierTest.php + diff --git a/src/Stringifiers/CallableStringifier.php b/src/Stringifiers/CallableStringifier.php index 6158199..2725abc 100644 --- a/src/Stringifiers/CallableStringifier.php +++ b/src/Stringifiers/CallableStringifier.php @@ -44,32 +44,37 @@ final class CallableStringifier implements Stringifier public function __construct( private readonly Stringifier $stringifier, private readonly Quoter $quoter, + private readonly bool $closureOnly = true, ) { } public function stringify(mixed $raw, int $depth): string|null { - if (!is_callable($raw)) { - return null; - } - if ($raw instanceof Closure) { return $this->buildFunction(new ReflectionFunction($raw), $depth); } + if ($this->closureOnly || !is_callable($raw)) { + return null; + } + if (is_object($raw)) { return $this->buildMethod(new ReflectionMethod($raw, '__invoke'), $raw, $depth); } - if (is_array($raw) && is_object($raw[0]) && is_string($raw[1])) { + if (is_array($raw) && isset($raw[0], $raw[1]) && is_object($raw[0]) && is_string($raw[1])) { return $this->buildMethod(new ReflectionMethod($raw[0], $raw[1]), $raw[0], $depth); } - if (is_array($raw) && is_string($raw[0]) && is_string($raw[1])) { + if (is_array($raw) && isset($raw[0], $raw[1]) && is_string($raw[0]) && is_string($raw[1])) { return $this->buildStaticMethod(new ReflectionMethod($raw[0], $raw[1]), $depth); } - if (is_string($raw) && str_contains($raw, ':')) { + if (!is_string($raw)) { + return null; + } + + if (str_contains($raw, ':')) { /** @var class-string $class */ $class = (string) strstr($raw, ':', true); $method = substr((string) strrchr($raw, ':'), 1); @@ -77,10 +82,6 @@ public function stringify(mixed $raw, int $depth): string|null return $this->buildStaticMethod(new ReflectionMethod($class, $method), $depth); } - if (!is_string($raw)) { - return null; - } - return $this->buildFunction(new ReflectionFunction($raw), $depth); } @@ -107,8 +108,7 @@ private function buildStaticMethod(ReflectionMethod $reflection, int $depth): st private function buildSignature(ReflectionFunctionAbstract $function, int $depth): string { - $signature = $function->isClosure() ? 'function ' : $function->getName(); - $signature .= sprintf( + $signature = sprintf( '(%s)', implode( ', ', @@ -138,7 +138,11 @@ private function buildSignature(ReflectionFunctionAbstract $function, int $depth $signature .= ': ' . $this->buildType($returnType, $depth); } - return $signature; + if ($function->isClosure()) { + return sprintf('Closure { %sfn%s }', $function->isStatic() ? 'static ' : '', $signature); + } + + return $function->getName() . $signature; } private function buildParameter(ReflectionParameter $reflectionParameter, int $depth): string diff --git a/src/Stringifiers/CompositeStringifier.php b/src/Stringifiers/CompositeStringifier.php index 21926f9..b659073 100644 --- a/src/Stringifiers/CompositeStringifier.php +++ b/src/Stringifiers/CompositeStringifier.php @@ -60,8 +60,10 @@ public static function createDefault(): self self::MAXIMUM_NUMBER_OF_PROPERTIES, ), ); - $stringifier->prependStringifier($callableStringifier = new CallableStringifier($stringifier, $quoter)); - $stringifier->prependStringifier(new FiberObjectStringifier($callableStringifier, $quoter)); + $stringifier->prependStringifier(new CallableStringifier($stringifier, $quoter)); + $stringifier->prependStringifier( + new FiberObjectStringifier(new CallableStringifier($stringifier, $quoter, closureOnly: false), $quoter), + ); $stringifier->prependStringifier(new EnumerationStringifier($quoter)); $stringifier->prependStringifier(new ObjectWithDebugInfoStringifier($arrayStringifier, $quoter)); $stringifier->prependStringifier(new ArrayObjectStringifier($arrayStringifier, $quoter)); diff --git a/tests/integration/stringify-callable.phpt b/tests/integration/stringify-callable.phpt index 4ad861d..a1e8905 100644 --- a/tests/integration/stringify-callable.phpt +++ b/tests/integration/stringify-callable.phpt @@ -5,25 +5,15 @@ declare(strict_types=1); require 'vendor/autoload.php'; -$variable = new WithInvoke(); +$variable = true; outputMultiple( - 'chr', - $variable, - [new WithMethods(), 'publicMethod'], - 'WithMethods::publicStaticMethod', - ['WithMethods', 'publicStaticMethod'], - static fn(int $foo): bool => (bool) $foo, + fn(int $foo): bool => (bool) $foo, static function (int $foo) use ($variable): string { return $variable::class; }, ); ?> --EXPECT-- -`chr(int $codepoint): string` -`WithInvoke->__invoke(int $parameter = 0): never` -`WithMethods->publicMethod(Iterator&Countable $parameter): ?static` -`WithMethods::publicStaticMethod(int|float $parameter): void` -`WithMethods::publicStaticMethod(int|float $parameter): void` -`function (int $foo): bool` -`function (int $foo) use ($variable): string` \ No newline at end of file +`Closure { fn(int $foo): bool }` +`Closure { static fn(int $foo) use ($variable): string }` diff --git a/tests/unit/Stringifiers/CallableStringifierTest.php b/tests/unit/Stringifiers/CallableStringifierTest.php index fb298bb..7c607ff 100644 --- a/tests/unit/Stringifiers/CallableStringifierTest.php +++ b/tests/unit/Stringifiers/CallableStringifierTest.php @@ -32,14 +32,14 @@ final class CallableStringifierTest extends TestCase #[Test] public function itShouldNotStringifyWhenRawValueIsNotCallable(): void { - $sut = new CallableStringifier(new FakeStringifier(), new FakeQuoter()); + $sut = new CallableStringifier(new FakeStringifier(), new FakeQuoter(), closureOnly: false); self::assertNull($sut->stringify(1, self::DEPTH)); } #[Test] - #[DataProvider('callableRawValuesProvider')] - public function itShouldStringifyWhenRawValueIsCallable(callable $raw, string $expectedWithoutQuotes): void + #[DataProvider('closureRawValuesProvider')] + public function itShouldStringifyWhenRawValueIsClosure(callable $raw, string $expectedWithoutQuotes): void { $quoter = new FakeQuoter(); @@ -51,6 +51,31 @@ public function itShouldStringifyWhenRawValueIsCallable(callable $raw, string $e self::assertEquals($expected, $actual); } + #[Test] + #[DataProvider('nonClosureCallableRawValuesProvider')] + public function itShouldNotStringifyNonClosureCallableByDefault(callable $raw, string $useless): void + { + $sut = new CallableStringifier(new FakeStringifier(), new FakeQuoter()); + + self::assertNull($sut->stringify($raw, self::DEPTH)); + } + + #[Test] + #[DataProvider('nonClosureCallableRawValuesProvider')] + public function itShouldStringifyNonClosureCallableWhenClosureOnlyIsFalse( + callable $raw, + string $expectedWithoutQuotes, + ): void { + $quoter = new FakeQuoter(); + + $sut = new CallableStringifier(new FakeStringifier(), $quoter, closureOnly: false); + + $actual = $sut->stringify($raw, self::DEPTH); + $expected = $quoter->quote($expectedWithoutQuotes, self::DEPTH); + + self::assertEquals($expected, $actual); + } + #[Test] public function itShouldStringifyWhenRawValueIsCallableWithDefaultValues(): void { @@ -63,7 +88,7 @@ public function itShouldStringifyWhenRawValueIsCallableWithDefaultValues(): void $actual = $sut->stringify($raw, self::DEPTH); $expected = $quoter->quote( - sprintf('function (int $value = %s): int', $stringifier->stringify(1, self::DEPTH + 1)), + sprintf('Closure { static fn(int $value = %s): int }', $stringifier->stringify(1, self::DEPTH + 1)), self::DEPTH, ); @@ -77,7 +102,7 @@ public function itShouldStringifyWhenRawValueIsCallableThatDoesNotHaveAnAccessib $quoter = new FakeQuoter(); - $sut = new CallableStringifier(new FakeStringifier(), $quoter); + $sut = new CallableStringifier(new FakeStringifier(), $quoter, closureOnly: false); $actual = $sut->stringify($raw, self::DEPTH); $expected = $quoter->quote( @@ -88,40 +113,87 @@ public function itShouldStringifyWhenRawValueIsCallableThatDoesNotHaveAnAccessib self::assertEquals($expected, $actual); } - /** @return array */ - public static function callableRawValuesProvider(): array + /** @return array */ + public static function closureRawValuesProvider(): array { $var1 = 1; $var2 = 2; return [ - [static fn() => 1, 'function ()'], - [static fn(): int => 1, 'function (): int'], - [static fn(float $value): int => (int) $value, 'function (float $value): int'], - [static fn(float &$value): int => (int) $value, 'function (float &$value): int'], - // phpcs:ignore SlevomatCodingStandard.TypeHints.DNFTypeHintFormat - [static fn(?float $value): int => (int) $value, 'function (?float $value): int'], - [static fn(int $value = self::DEPTH): int => $value, 'function (int $value = self::DEPTH): int'], - [static fn(int|float $value): int => (int) $value, 'function (int|float $value): int'], - [static fn(Countable&Iterator $value): int => $value->count(), 'function (Countable&Iterator $value): int'], - [static fn(int ...$value): int => array_sum($value), 'function (int ...$value): int'], - [ + 'static closure without parameters' => [ + static fn() => 1, + 'Closure { static fn() }', + ], + 'non-static closure without parameters' => [ + fn() => 1, + 'Closure { fn() }', + ], + 'static closure with return type' => [ + static fn(): int => 1, + 'Closure { static fn(): int }', + ], + 'non-static closure with return type' => [ + fn(): int => 1, + 'Closure { fn(): int }', + ], + 'static closure with typed parameter' => [ + static fn(float $value): int => (int) $value, + 'Closure { static fn(float $value): int }', + ], + 'static closure with reference parameter' => [ + static fn(float &$value): int => (int) $value, + 'Closure { static fn(float &$value): int }', + ], + 'static closure with nullable parameter' => [ + static fn(float|null $value): int => (int) $value, + 'Closure { static fn(?float $value): int }', + ], + 'static closure with constant default value' => [ + static fn(int $value = self::DEPTH): int => $value, + 'Closure { static fn(int $value = self::DEPTH): int }', + ], + 'static closure with union type parameter' => [ + static fn(int|float $value): int => (int) $value, + 'Closure { static fn(int|float $value): int }', + ], + 'static closure with intersection type parameter' => [ + static fn(Countable&Iterator $value): int => $value->count(), + 'Closure { static fn(Countable&Iterator $value): int }', + ], + 'static closure with variadic parameter' => [ + static fn(int ...$value): int => array_sum($value), + 'Closure { static fn(int ...$value): int }', + ], + 'static closure with multiple parameters' => [ static fn(float $value1, float $value2): float => $value1 + $value2, - 'function (float $value1, float $value2): float', + 'Closure { static fn(float $value1, float $value2): float }', ], - [ + 'static closure with single use variable' => [ static function (int $value) use ($var1): int { return $value + $var1; }, - 'function (int $value) use ($var1): int', + 'Closure { static fn(int $value) use ($var1): int }', ], - [ + 'static closure with multiple use variables' => [ static function (int $value) use ($var1, $var2): int { return $value + $var1 + $var2; }, - 'function (int $value) use ($var1, $var2): int', + 'Closure { static fn(int $value) use ($var1, $var2): int }', ], - [ + 'non-static closure with use variable' => [ + function (int $value) use ($var1): int { + return $value + $var1; + }, + 'Closure { fn(int $value) use ($var1): int }', + ], + ]; + } + + /** @return array */ + public static function nonClosureCallableRawValuesProvider(): array + { + return [ + 'invokable object' => [ new class { public function __invoke(int $parameter): never { @@ -130,19 +202,22 @@ public function __invoke(int $parameter): never }, 'class->__invoke(int $parameter): never', ], - [ + 'object method as array' => [ [new DateTime(), 'format'], 'DateTime->format(string $format)', ], - [ + 'static method as array' => [ ['DateTime', 'createFromImmutable'], 'DateTime::createFromImmutable(DateTimeImmutable $object)', ], - [ + 'static method as string' => [ 'DateTimeImmutable::getLastErrors', 'DateTimeImmutable::getLastErrors()', ], - ['chr', 'chr(int $codepoint): string'], + 'function name as string' => [ + 'chr', + 'chr(int $codepoint): string', + ], ]; } }