Skip to content

Commit bc0071e

Browse files
committed
Change how to stringify callables
Different stringifies stringify of callables but don't indicate they are callables. This commit ensures that when stringifying a callable, we can see its signature, including name, parameters, and return type. Signed-off-by: Henrique Moody <[email protected]>
1 parent 6a8d66c commit bc0071e

File tree

6 files changed

+392
-0
lines changed

6 files changed

+392
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <[email protected]>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Stringifier\Stringifiers;
12+
13+
use Closure;
14+
use ReflectionFunction;
15+
use ReflectionFunctionAbstract;
16+
use ReflectionIntersectionType;
17+
use ReflectionMethod;
18+
use ReflectionNamedType;
19+
use ReflectionParameter;
20+
use ReflectionType;
21+
use ReflectionUnionType;
22+
use Respect\Stringifier\Helpers\ObjectHelper;
23+
use Respect\Stringifier\Quoter;
24+
use Respect\Stringifier\Stringifier;
25+
26+
use function array_keys;
27+
use function array_map;
28+
use function count;
29+
use function implode;
30+
use function is_array;
31+
use function is_callable;
32+
use function is_object;
33+
use function is_string;
34+
use function sprintf;
35+
use function str_contains;
36+
use function strrchr;
37+
use function strstr;
38+
use function substr;
39+
40+
final class CallableStringifier implements Stringifier
41+
{
42+
use ObjectHelper;
43+
44+
public function __construct(
45+
private readonly Stringifier $stringifier,
46+
private readonly Quoter $quoter,
47+
) {
48+
}
49+
50+
public function stringify(mixed $raw, int $depth): ?string
51+
{
52+
if (!is_callable($raw)) {
53+
return null;
54+
}
55+
56+
if ($raw instanceof Closure) {
57+
return $this->buildFunction(new ReflectionFunction($raw), $depth);
58+
}
59+
60+
if (is_object($raw)) {
61+
return $this->buildMethod(new ReflectionMethod($raw, '__invoke'), $raw, $depth);
62+
}
63+
64+
if (is_array($raw) && is_object($raw[0])) {
65+
return $this->buildMethod(new ReflectionMethod($raw[0], $raw[1]), $raw[0], $depth);
66+
}
67+
68+
if (is_array($raw) && is_string($raw[0])) {
69+
return $this->buildStaticMethod(new ReflectionMethod($raw[0], $raw[1]), $depth);
70+
}
71+
72+
/** @var callable-string $raw */
73+
if (str_contains($raw, ':')) {
74+
/** @var class-string $class */
75+
$class = (string) strstr($raw, ':', true);
76+
$method = substr((string) strrchr($raw, ':'), 1);
77+
78+
return $this->buildStaticMethod(new ReflectionMethod($class, $method), $depth);
79+
}
80+
81+
return $this->buildFunction(new ReflectionFunction($raw), $depth);
82+
}
83+
84+
public function buildFunction(ReflectionFunction $raw, int $depth): ?string
85+
{
86+
return $this->quoter->quote($this->buildSignature($raw, $depth), $depth);
87+
}
88+
89+
private function buildMethod(ReflectionMethod $reflection, object $object, int $depth): string
90+
{
91+
return $this->quoter->quote(
92+
sprintf('%s->%s', $this->getName($object), $this->buildSignature($reflection, $depth)),
93+
$depth
94+
);
95+
}
96+
97+
private function buildStaticMethod(ReflectionMethod $reflection, int $depth): string
98+
{
99+
return $this->quoter->quote(
100+
sprintf('%s::%s', $reflection->class, $this->buildSignature($reflection, $depth)),
101+
$depth
102+
);
103+
}
104+
105+
private function buildSignature(ReflectionFunctionAbstract $function, int $depth): string
106+
{
107+
$signature = $function->isClosure() ? 'function ' : $function->getName();
108+
$signature .= sprintf(
109+
'(%s)',
110+
implode(
111+
', ',
112+
array_map(
113+
fn(ReflectionParameter $parameter): string => $this->buildParameter(
114+
$parameter,
115+
$depth + 1
116+
),
117+
$function->getParameters()
118+
)
119+
),
120+
);
121+
122+
$closureUsedVariables = $function->getClosureUsedVariables();
123+
if (count($closureUsedVariables)) {
124+
$signature .= sprintf(
125+
' use ($%s)',
126+
implode(
127+
', $',
128+
array_keys($closureUsedVariables)
129+
),
130+
);
131+
}
132+
133+
$returnType = $function->getReturnType();
134+
if ($returnType !== null) {
135+
$signature .= ': ' . $this->buildType($returnType, $depth);
136+
}
137+
138+
return $signature;
139+
}
140+
141+
private function buildParameter(ReflectionParameter $reflectionParameter, int $depth): string
142+
{
143+
$parameter = '';
144+
145+
$type = $reflectionParameter->getType();
146+
if ($type !== null) {
147+
$parameter .= $this->buildType($type, $depth);
148+
}
149+
150+
if ($reflectionParameter->isVariadic()) {
151+
return $parameter . ' ...$' . $reflectionParameter->getName();
152+
}
153+
154+
$parameter .= ' $' . $reflectionParameter->getName();
155+
if ($reflectionParameter->isOptional()) {
156+
$parameter .= ' = ' . $this->buildValue($reflectionParameter, $depth);
157+
}
158+
159+
return $parameter;
160+
}
161+
162+
private function buildValue(ReflectionParameter $reflectionParameter, int $depth): ?string
163+
{
164+
$value = $reflectionParameter->getDefaultValueConstantName();
165+
if ($value !== null) {
166+
return $value;
167+
}
168+
169+
return $this->stringifier->stringify($reflectionParameter->getDefaultValue(), $depth);
170+
}
171+
172+
private function buildType(ReflectionType $raw, int $depth): string
173+
{
174+
if ($raw instanceof ReflectionUnionType) {
175+
return implode(
176+
'|',
177+
array_map(fn(ReflectionType $type) => $this->buildType($type, $depth), $raw->getTypes())
178+
);
179+
}
180+
181+
if ($raw instanceof ReflectionIntersectionType) {
182+
return implode(
183+
'&',
184+
array_map(fn(ReflectionType $type) => $this->buildType($type, $depth), $raw->getTypes())
185+
);
186+
}
187+
188+
/** @var ReflectionNamedType $raw */
189+
190+
return ($raw->allowsNull() ? '?' : '') . $raw->getName();
191+
}
192+
}

src/Stringifiers/ClusterStringifier.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static function createDefault(): self
5555
new JsonSerializableObjectStringifier($jsonEncodableStringifier, $quoter),
5656
new EnumerationStringifier($quoter),
5757
new ObjectWithDebugInfoStringifier($arrayStringifier, $quoter),
58+
new CallableStringifier($stringifier, $quoter),
5859
new ObjectStringifier($stringifier, $quoter, self::MAXIMUM_DEPTH, self::MAXIMUM_NUMBER_OF_PROPERTIES),
5960
$arrayStringifier,
6061
new InfiniteNumberStringifier($quoter),

tests/fixtures/WithInvoke.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <[email protected]>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
final class WithInvoke
12+
{
13+
public function __invoke(int $parameter = 0): never
14+
{
15+
exit($parameter);
16+
}
17+
}

tests/fixtures/WithMethods.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Respect/Stringifier.
5+
* Copyright (c) Henrique Moody <[email protected]>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
final class WithMethods
12+
{
13+
public function publicMethod(Iterator&Countable $parameter): ?static
14+
{
15+
return new static();
16+
}
17+
18+
public static function publicStaticMethod(int|float $parameter): void
19+
{
20+
}
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--FILE--
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
require 'vendor/autoload.php';
7+
8+
$variable = new WithInvoke();
9+
10+
outputMultiple(
11+
'chr',
12+
$variable,
13+
[new WithMethods(), 'publicMethod'],
14+
'WithMethods::publicStaticMethod',
15+
['WithMethods', 'publicStaticMethod'],
16+
static fn(int $foo): bool => (bool) $foo,
17+
static function (int $foo) use ($variable): string {
18+
return $variable::class;
19+
}
20+
);
21+
?>
22+
--EXPECT--
23+
`chr(int $codepoint): string`
24+
`WithInvoke->__invoke(int $parameter = 0): never`
25+
`WithMethods->publicMethod(Iterator&Countable $parameter): ?static`
26+
`WithMethods::publicStaticMethod(int|float $parameter): void`
27+
`WithMethods::publicStaticMethod(int|float $parameter): void`
28+
`function (int $foo): bool`
29+
`function (int $foo) use ($variable): string`

0 commit comments

Comments
 (0)