|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
declare(strict_types=1); |
|
4
|
|
|
|
|
5
|
|
|
namespace Jasny\SwitchRoute; |
|
6
|
|
|
|
|
7
|
|
|
use BadMethodCallException; |
|
8
|
|
|
use Closure; |
|
9
|
|
|
use Jasny\ReflectionFactory\ReflectionFactory; |
|
10
|
|
|
use Jasny\ReflectionFactory\ReflectionFactoryInterface; |
|
11
|
|
|
use LogicException; |
|
12
|
|
|
use ReflectionException; |
|
13
|
|
|
use ReflectionFunction; |
|
14
|
|
|
use ReflectionFunctionAbstract; |
|
15
|
|
|
use ReflectionMethod; |
|
16
|
|
|
use ReflectionNamedType; |
|
17
|
|
|
|
|
18
|
|
|
/** |
|
19
|
|
|
* Invoke the action or script specified by the route. |
|
20
|
|
|
*/ |
|
21
|
|
|
class Invoker implements InvokerInterface |
|
22
|
|
|
{ |
|
23
|
|
|
/** |
|
24
|
|
|
* Callback to turn a controller name and action name into a callable. |
|
25
|
|
|
* @var callable |
|
26
|
|
|
*/ |
|
27
|
|
|
protected $createInvokable; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* @var ReflectionFactoryInterface |
|
31
|
|
|
*/ |
|
32
|
|
|
protected $reflection; |
|
33
|
|
|
|
|
34
|
|
|
/** |
|
35
|
|
|
* Invoker constructor. |
|
36
|
|
|
* |
|
37
|
|
|
* @param callable|null $createInvokable |
|
38
|
|
|
* @param ReflectionFactoryInterface|null $reflection |
|
39
|
|
|
*/ |
|
40
|
28 |
|
public function __construct(?callable $createInvokable = null, ?ReflectionFactoryInterface $reflection = null) |
|
41
|
|
|
{ |
|
42
|
28 |
|
$this->createInvokable = $createInvokable ?? Closure::fromCallable([__CLASS__, 'createInvokable']); |
|
43
|
28 |
|
$this->reflection = $reflection ?? new ReflectionFactory(); |
|
44
|
28 |
|
} |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* Generate code for a function or method class for the route. |
|
48
|
|
|
* |
|
49
|
|
|
* The argument template should have two '%s' placeholders. The first is used for the argument name, the second for |
|
50
|
|
|
* the default value. |
|
51
|
|
|
* |
|
52
|
|
|
* @param array $route |
|
53
|
|
|
* @param callable $genArg Callback to generate code for arguments. |
|
54
|
|
|
* @param string $new PHP code to instantiate class. |
|
55
|
|
|
* @return string |
|
56
|
|
|
* @throws ReflectionException |
|
57
|
|
|
*/ |
|
58
|
25 |
|
public function generateInvocation(array $route, callable $genArg, string $new = '(new %s)'): string |
|
59
|
|
|
{ |
|
60
|
25 |
|
['controller' => $controller, 'action' => $action] = $route + ['controller' => null, 'action' => null]; |
|
61
|
|
|
|
|
62
|
25 |
|
$invokable = ($this->createInvokable)($controller, $action); |
|
63
|
24 |
|
$this->assertInvokable($invokable); |
|
64
|
|
|
|
|
65
|
17 |
|
if (is_string($invokable) && strpos($invokable, '::') !== false) { |
|
66
|
1 |
|
$invokable = explode('::', $invokable); |
|
67
|
|
|
} |
|
68
|
|
|
|
|
69
|
17 |
|
$reflection = $this->getReflection($invokable); |
|
70
|
|
|
|
|
71
|
15 |
|
return (is_string($invokable) ? $invokable : $this->generateInvocationMethod($invokable, $reflection, $new)) |
|
72
|
15 |
|
. '(' . $this->generateInvocationArgs($reflection, $genArg) . ')'; |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* Generate the code for a method call. |
|
77
|
|
|
* |
|
78
|
|
|
* @param callable&array $invokable |
|
79
|
|
|
* @param ReflectionMethod $reflection |
|
80
|
|
|
* @param string $new PHP code to instantiate class. |
|
81
|
|
|
* @return string |
|
82
|
|
|
*/ |
|
83
|
15 |
|
protected function generateInvocationMethod( |
|
84
|
|
|
array $invokable, |
|
85
|
|
|
ReflectionMethod $reflection, |
|
86
|
|
|
string $new = '(new \\%s)' |
|
87
|
|
|
): string { |
|
88
|
15 |
|
return $invokable[1] === '__invoke' |
|
89
|
4 |
|
? sprintf($new, $invokable[0]) |
|
90
|
15 |
|
: ($reflection->isStatic() ? "{$invokable[0]}::" : sprintf($new, $invokable[0]) . "->") . $invokable[1]; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* Generate code for the arguments when calling the action. |
|
95
|
|
|
* |
|
96
|
|
|
* @param ReflectionFunctionAbstract $reflection |
|
97
|
|
|
* @param callable $genArg |
|
98
|
|
|
* @return string |
|
99
|
|
|
* @throws ReflectionException |
|
100
|
|
|
*/ |
|
101
|
16 |
|
protected function generateInvocationArgs(ReflectionFunctionAbstract $reflection, callable $genArg): string |
|
102
|
|
|
{ |
|
103
|
16 |
|
$args = []; |
|
104
|
|
|
|
|
105
|
16 |
|
foreach ($reflection->getParameters() as $param) { |
|
106
|
6 |
|
$default = $param->isOptional() ? $param->getDefaultValue() : null; |
|
107
|
6 |
|
$type = $param->getType() instanceof ReflectionNamedType ? $param->getType()->getName() : null; |
|
108
|
6 |
|
$args[] = $genArg($param->getName(), $type, $default); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
16 |
|
return join(', ', $args); |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Assert that invokable is a function name or array with class and method. |
|
116
|
|
|
* |
|
117
|
|
|
* @param mixed $invokable |
|
118
|
|
|
*/ |
|
119
|
25 |
|
protected function assertInvokable($invokable): void |
|
120
|
|
|
{ |
|
121
|
25 |
|
$valid = is_callable($invokable, true) && ( |
|
122
|
|
|
( |
|
123
|
22 |
|
is_string($invokable) && preg_match('/^[a-z_]\w*(\\\\\w+)*(::[a-z_]\w*)?$/i', $invokable) |
|
124
|
|
|
) || ( |
|
125
|
20 |
|
is_array($invokable) && |
|
126
|
20 |
|
is_string($invokable[0]) && preg_match('/^[a-z_]\w*(\\\\\w+)*$/i', $invokable[0]) && |
|
127
|
25 |
|
preg_match('/^[a-z_]\w*$/i', $invokable[1]) |
|
128
|
|
|
) |
|
129
|
|
|
); |
|
130
|
|
|
|
|
131
|
25 |
|
if ($valid) { |
|
132
|
18 |
|
return; |
|
133
|
|
|
} |
|
134
|
|
|
|
|
135
|
7 |
|
if (is_array($invokable)) { |
|
136
|
|
|
$types = array_map(static function ($item) { |
|
137
|
4 |
|
return is_object($item) ? get_class($item) : gettype($item); |
|
138
|
4 |
|
}, $invokable); |
|
139
|
4 |
|
$type = '[' . join(', ', $types) . ']'; |
|
140
|
|
|
} else { |
|
141
|
3 |
|
$type = is_object($invokable) ? get_class($invokable) : gettype($invokable); |
|
142
|
|
|
} |
|
143
|
|
|
|
|
144
|
7 |
|
throw new LogicException("Invokable should be a function or array with class name and method, {$type} given"); |
|
145
|
|
|
} |
|
146
|
|
|
|
|
147
|
|
|
/** |
|
148
|
|
|
* Get reflection of invokable. |
|
149
|
|
|
* |
|
150
|
|
|
* @param array|string $invokable |
|
151
|
|
|
* @return ReflectionFunction|ReflectionMethod |
|
152
|
|
|
* @throws ReflectionException if function or method doesn't exist |
|
153
|
|
|
*/ |
|
154
|
18 |
|
protected function getReflection($invokable): ReflectionFunctionAbstract |
|
155
|
|
|
{ |
|
156
|
18 |
|
return is_array($invokable) |
|
157
|
17 |
|
? $this->reflection->reflectMethod($invokable[0], $invokable[1]) |
|
158
|
16 |
|
: $this->reflection->reflectFunction($invokable); |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
/** |
|
162
|
|
|
* Generate standard code for when no route matches. |
|
163
|
|
|
* |
|
164
|
|
|
* @return string |
|
165
|
|
|
*/ |
|
166
|
2 |
|
public function generateDefault(): string |
|
167
|
|
|
{ |
|
168
|
|
|
return <<<CODE |
|
169
|
2 |
|
if (\$allowedMethods === []) { |
|
170
|
|
|
http_response_code(404); |
|
171
|
|
|
echo "Not Found"; |
|
172
|
|
|
} else { |
|
173
|
|
|
http_response_code(405); |
|
174
|
|
|
header('Allow: ' . join(', ', \$allowedMethods)); |
|
175
|
|
|
echo "Method Not Allowed"; |
|
176
|
|
|
} |
|
177
|
|
|
CODE; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Default method to create invokable from controller and action name |
|
182
|
|
|
* |
|
183
|
|
|
* @param string|null $controller |
|
184
|
|
|
* @param string|null $action |
|
185
|
|
|
* @return array |
|
186
|
|
|
*/ |
|
187
|
14 |
|
final public static function createInvokable(?string $controller, ?string $action): array |
|
188
|
|
|
{ |
|
189
|
14 |
|
if ($controller === null && $action === null) { |
|
190
|
1 |
|
throw new BadMethodCallException("Neither controller or action is set"); |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
13 |
|
[$class, $method] = $controller !== null |
|
194
|
10 |
|
? [$controller . 'Controller', ($action ?? 'default') . 'Action'] |
|
195
|
13 |
|
: [$action . 'Action', '__invoke']; |
|
196
|
|
|
|
|
197
|
|
|
return [ |
|
198
|
13 |
|
strtr(ucwords($class, '-'), ['-' => '']), |
|
199
|
13 |
|
strtr(lcfirst(ucwords($method, '-')), ['-' => '']) |
|
200
|
|
|
]; |
|
201
|
|
|
} |
|
202
|
|
|
} |
|
203
|
|
|
|