Completed
Push — master ( 650383...e200ac )
by Arnold
06:31 queued 04:41
created

Invoker::generateInvocation()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 4
rs 10
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