Invoker::generateInvocationArgs()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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