Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`
Expand Down
4 changes: 4 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@
<rule ref="Generic.PHP.CharacterBeforePHPOpeningTag.Found">
<exclude-pattern>tests/integration/</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Functions.StaticClosure.ClosureNotStatic">
<exclude-pattern>tests/integration</exclude-pattern>
<exclude-pattern>tests/unit/Stringifiers/CallableStringifierTest.php</exclude-pattern>
</rule>
</ruleset>
32 changes: 18 additions & 14 deletions src/Stringifiers/CallableStringifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,43 +44,44 @@ 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);

return $this->buildStaticMethod(new ReflectionMethod($class, $method), $depth);
}

if (!is_string($raw)) {
return null;
}

return $this->buildFunction(new ReflectionFunction($raw), $depth);
}

Expand All @@ -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(
', ',
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/Stringifiers/CompositeStringifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
18 changes: 4 additions & 14 deletions tests/integration/stringify-callable.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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`
`Closure { fn(int $foo): bool }`
`Closure { static fn(int $foo) use ($variable): string }`
131 changes: 103 additions & 28 deletions tests/unit/Stringifiers/CallableStringifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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
{
Expand All @@ -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,
);

Expand All @@ -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(
Expand All @@ -88,40 +113,87 @@ public function itShouldStringifyWhenRawValueIsCallableThatDoesNotHaveAnAccessib
self::assertEquals($expected, $actual);
}

/** @return array<int, array{0: callable, 1: string}> */
public static function callableRawValuesProvider(): array
/** @return array<string, array{0: callable, 1: string}> */
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<string, array{0: callable, 1: string}> */
public static function nonClosureCallableRawValuesProvider(): array
{
return [
'invokable object' => [
new class {
public function __invoke(int $parameter): never
{
Expand All @@ -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',
],
];
}
}