Passed
Pull Request — master (#9)
by Pavel
11:27
created

BaseResolver::getController()   C

Complexity

Conditions 9
Paths 10

Size

Total Lines 48
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 43.1718

Importance

Changes 0
Metric Value
dl 0
loc 48
c 0
b 0
f 0
ccs 8
cts 32
cp 0.25
rs 5.5102
cc 9
eloc 27
nc 10
nop 1
crap 43.1718
1
<?php
2
3
/*
4
 * Copyright (c) 2010-2017 Fabien Potencier
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is furnished
11
 * to do so, subject to t * *he following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in all
14
 * copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 *
24
 */
25
26
namespace Bankiru\Api\Rpc\Routing\ControllerResolver;
27
28
use Bankiru\Api\Rpc\Exception\InvalidMethodParametersException;
29
use Bankiru\Api\Rpc\RpcRequestInterface;
30
use Psr\Log\LoggerInterface;
31
use Psr\Log\NullLogger;
32
33
class BaseResolver implements ControllerResolverInterface
34
{
35
    /** @var  LoggerInterface */
36
    private $logger;
37
38
    /**
39
     * Resolver constructor.
40
     *
41
     * @param LoggerInterface $logger
42
     */
43 8
    public function __construct(LoggerInterface $logger = null)
44
    {
45 8
        $this->logger = $logger ?: new NullLogger();
46 8
    }
47
48
    /** {@inheritdoc} */
49 8
    public function getController(RpcRequestInterface $request)
50
    {
51 8
        if (!$controller = $request->getAttributes()->get('_controller')) {
52
            $this->logger->warning('Unable to look for the controller as the "_controller" parameter is missing.');
53
54
            return false;
55
        }
56
57 8
        if (is_array($controller)) {
58
            return $controller;
59
        }
60
61 8
        if (is_object($controller)) {
62
            if (method_exists($controller, '__invoke')) {
63
                return $controller;
64
            }
65
66
            throw new \InvalidArgumentException(
67
                sprintf(
68
                    'Controller "%s" for method "%s" is not callable.',
69
                    get_class($controller),
70
                    $request->getMethod()
71
                )
72
            );
73
        }
74
75 8
        if (false === strpos($controller, ':')) {
76
            if (method_exists($controller, '__invoke')) {
77
                return $this->instantiateController($controller);
78
            } elseif (function_exists($controller)) {
79
                return $controller;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $controller; (integer|double|string|null|boolean) is incompatible with the return type declared by the interface Bankiru\Api\Rpc\Routing\...nterface::getController of type callable|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
80
            }
81
        }
82
83 8
        $callable = $this->createController($controller);
84
85 8
        if (!is_callable($callable)) {
86
            throw new \InvalidArgumentException(
87
                sprintf(
88
                    'The controller for method "%s" is not callable. %s',
89
                    $request->getMethod(),
90
                    $this->getControllerError($callable)
91
                )
92
            );
93
        }
94
95 8
        return $callable;
96
    }
97
98
    /** {@inheritdoc} */
99 7
    public function getArguments(RpcRequestInterface $request, $controller)
100
    {
101 7
        if (is_array($controller)) {
102 7
            $r = new \ReflectionMethod($controller[0], $controller[1]);
103 7
        } elseif (is_object($controller) && !$controller instanceof \Closure) {
104
            $r = new \ReflectionObject($controller);
105
            $r = $r->getMethod('__invoke');
106
        } else {
107
            $r = new \ReflectionFunction($controller);
108
        }
109
110 7
        return $this->doGetArguments($request, $r->getParameters());
111
    }
112
113
    /**
114
     * Returns an instantiated controller.
115
     *
116
     * @param string $class A class name
117
     *
118
     * @return object
119
     */
120 8
    protected function instantiateController($class)
121
    {
122 8
        return new $class();
123
    }
124
125
    /**
126
     * Returns a callable for the given controller.
127
     *
128
     * @param string $controller A Controller string
129
     *
130
     * @return callable A PHP callable
131
     *
132
     * @throws \InvalidArgumentException
133
     */
134 8
    protected function createController($controller)
135
    {
136 8
        if (false === strpos($controller, '::')) {
137
            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s".', $controller));
138
        }
139
140 8
        list($class, $method) = explode('::', $controller, 2);
141
142 8
        if (!class_exists($class)) {
143
            throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
144
        }
145
146 8
        return [$this->instantiateController($class), $method];
147
    }
148
149
    /**
150
     * @param RpcRequestInterface    $request
151
     * @param \ReflectionParameter[] $parameters
152
     *
153
     * @return array
154
     * @throws \RuntimeException
155
     */
156 7
    protected function doGetArguments(RpcRequestInterface $request, array $parameters)
157
    {
158 7
        $attributes = $request->getAttributes()->all();
159 7
        $arguments  = [];
160 7
        $missing    = [];
161 7
        foreach ($parameters as $param) {
162 6
            if (is_array($request->getParameters()) && array_key_exists($param->name, $request->getParameters())) {
163 5
                $arguments[] = $this->checkType($request->getParameters()[$param->name], $param, $request);
164 6
            } elseif (array_key_exists($param->name, $attributes)) {
165
                $arguments[] = $this->checkType($attributes[$param->name], $param->name, $request);
166 6
            } elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
167 5
                $arguments[] = $request;
168 6
            } elseif ($param->isDefaultValueAvailable()) {
169 5
                $arguments[] = $param->getDefaultValue();
170 5
            } else {
171 2
                $missing[] = $param->name;
172
            }
173 7
        }
174
175 6
        if (count($missing) > 0) {
176 2
            throw InvalidMethodParametersException::missing($request->getMethod(), $missing);
177
        }
178
179 4
        return $arguments;
180
    }
181
182
    private function getControllerError($callable)
183
    {
184
        if (is_string($callable)) {
185
            if (false !== strpos($callable, '::')) {
186
                $callable = explode('::', $callable);
187
            }
188
189
            if (class_exists($callable) && !method_exists($callable, '__invoke')) {
190
                return sprintf('Class "%s" does not have a method "__invoke".', $callable);
191
            }
192
193
            if (!function_exists($callable)) {
194
                return sprintf('Function "%s" does not exist.', $callable);
195
            }
196
        }
197
198
        if (!is_array($callable)) {
199
            return sprintf(
200
                'Invalid type for controller given, expected string or array, got "%s".',
201
                gettype($callable)
202
            );
203
        }
204
205
        if (2 !== count($callable)) {
206
            return sprintf('Invalid format for controller, expected array(controller, method) or controller::method.');
207
        }
208
209
        list($controller, $method) = $callable;
210
211
        if (is_string($controller) && !class_exists($controller)) {
212
            return sprintf('Class "%s" does not exist.', $controller);
213
        }
214
215
        $className = is_object($controller) ? get_class($controller) : $controller;
216
217
        if (method_exists($controller, $method)) {
218
            return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className);
219
        }
220
221
        $collection = get_class_methods($controller);
222
223
        $alternatives = [];
224
225
        foreach ($collection as $item) {
226
            $lev = levenshtein($method, $item);
227
228
            if ($lev <= strlen($method) / 3 || false !== strpos($item, $method)) {
229
                $alternatives[] = $item;
230
            }
231
        }
232
233
        asort($alternatives);
234
235
        $message = sprintf('Expected method "%s" on class "%s"', $method, $className);
236
237
        if (count($alternatives) > 0) {
238
            $message .= sprintf(', did you mean "%s"?', implode('", "', $alternatives));
239
        } else {
240
            $message .= sprintf('. Available methods: "%s".', implode('", "', $collection));
241
        }
242
243
        return $message;
244
    }
245
246
    /**
247
     * Checks that argument matches parameter type
248
     *
249
     * @param mixed                $argument
250
     * @param \ReflectionParameter $param
251
     * @param RpcRequestInterface  $request
252
     *
253
     * @return mixed
254
     */
255 5
    private function checkType($argument, \ReflectionParameter $param, RpcRequestInterface $request)
256
    {
257 5
        $actual = is_object($argument) ? get_class($argument) : gettype($argument);
258 5
        if (null !== $param->getClass()) {
259
            $className = $param->getClass();
260
            if (!($argument instanceof $className)) {
261
                throw InvalidMethodParametersException::typeMismatch(
262
                    $request->getMethod(),
263
                    $param->name,
264
                    $className,
265
                    $actual
266
                );
267
            }
268 5
        } elseif ($param->isArray() && !is_array($argument)) {
269 1
            throw InvalidMethodParametersException::typeMismatch(
270 1
                $request->getMethod(),
271 1
                $param->name,
272 1
                'array',
273
                $actual
274 1
            );
275
        }
276
277 5
        return $argument;
278
    }
279
}
280