Completed
Push — master ( 055f1b...f72a4b )
by Pavel
52s
created

BaseResolver::checkType()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 8.5139

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 24
ccs 10
cts 17
cp 0.5881
rs 8.5125
cc 6
eloc 17
nc 8
nop 3
crap 8.5139
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: batanov.pavel
5
 * Date: 11.02.2016
6
 * Time: 18:41
7
 */
8
9
namespace Bankiru\Api\Rpc\Routing\ControllerResolver;
10
11
use Bankiru\Api\Rpc\Exception\InvalidMethodParametersException;
12
use Bankiru\Api\Rpc\Http\RequestInterface;
13
use Psr\Log\LoggerInterface;
14
use Psr\Log\NullLogger;
15
16
class BaseResolver implements ControllerResolverInterface
17
{
18
    /** @var  LoggerInterface */
19
    private $logger;
20
21
    /**
22
     * Resolver constructor.
23
     *
24
     * @param LoggerInterface $logger
25
     */
26 7
    public function __construct(LoggerInterface $logger = null)
27
    {
28 7
        $this->logger = $logger;
29
30 7
        if (null === $this->logger) {
31
            $this->logger = new NullLogger();
32
        }
33 7
    }
34
35
    /** {@inheritdoc} */
36 7
    public function getController(RequestInterface $request)
37
    {
38 7
        if (!$controller = $request->getAttributes()->get('_controller')) {
39 1
            $this->logger->warning('Unable to look for the controller as the "_controller" parameter is missing.');
40
41 1
            return false;
42
        }
43
44 6
        if (is_array($controller)) {
45
            return $controller;
46
        }
47
48 6
        if (is_object($controller)) {
49
            if (method_exists($controller, '__invoke')) {
50
                return $controller;
51
            }
52
53
            throw new \InvalidArgumentException(
54
                sprintf(
55
                    'Controller "%s" for method "%s" is not callable.',
56
                    get_class($controller),
57
                    $request->getMethod()
58
                )
59
            );
60
        }
61
62 6
        if (false === strpos($controller, ':')) {
63
            if (method_exists($controller, '__invoke')) {
64
                return $this->instantiateController($controller);
65
            } elseif (function_exists($controller)) {
66
                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...
67
            }
68
        }
69
70 6
        $callable = $this->createController($controller);
71
72 6
        if (!is_callable($callable)) {
73
            throw new \InvalidArgumentException(
74
                sprintf(
75
                    'The controller for method "%s" is not callable. %s',
76
                    $request->getMethod(),
77
                    $this->getControllerError($callable)
78
                )
79
            );
80
        }
81
82 6
        return $callable;
83
    }
84
85
    /**
86
     * Returns an instantiated controller.
87
     *
88
     * @param string $class A class name
89
     *
90
     * @return object
91
     */
92 6
    protected function instantiateController($class)
93
    {
94 6
        return new $class();
95
    }
96
97
    /**
98
     * Returns a callable for the given controller.
99
     *
100
     * @param string $controller A Controller string
101
     *
102
     * @return callable A PHP callable
103
     *
104
     * @throws \InvalidArgumentException
105
     */
106 6
    protected function createController($controller)
107
    {
108 6
        if (false === strpos($controller, '::')) {
109
            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s".', $controller));
110
        }
111
112 6
        list($class, $method) = explode('::', $controller, 2);
113
114 6
        if (!class_exists($class)) {
115
            throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
116
        }
117
118 6
        return [$this->instantiateController($class), $method];
119
    }
120
121
    private function getControllerError($callable)
122
    {
123
        if (is_string($callable)) {
124
            if (false !== strpos($callable, '::')) {
125
                $callable = explode('::', $callable);
126
            }
127
128
            if (class_exists($callable) && !method_exists($callable, '__invoke')) {
129
                return sprintf('Class "%s" does not have a method "__invoke".', $callable);
130
            }
131
132
            if (!function_exists($callable)) {
133
                return sprintf('Function "%s" does not exist.', $callable);
134
            }
135
        }
136
137
        if (!is_array($callable)) {
138
            return sprintf(
139
                'Invalid type for controller given, expected string or array, got "%s".',
140
                gettype($callable)
141
            );
142
        }
143
144
        if (2 !== count($callable)) {
145
            return sprintf('Invalid format for controller, expected array(controller, method) or controller::method.');
146
        }
147
148
        list($controller, $method) = $callable;
149
150
        if (is_string($controller) && !class_exists($controller)) {
151
            return sprintf('Class "%s" does not exist.', $controller);
152
        }
153
154
        $className = is_object($controller) ? get_class($controller) : $controller;
155
156
        if (method_exists($controller, $method)) {
157
            return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className);
158
        }
159
160
        $collection = get_class_methods($controller);
161
162
        $alternatives = [];
163
164
        foreach ($collection as $item) {
165
            $lev = levenshtein($method, $item);
166
167
            if ($lev <= strlen($method) / 3 || false !== strpos($item, $method)) {
168
                $alternatives[] = $item;
169
            }
170
        }
171
172
        asort($alternatives);
173
174
        $message = sprintf('Expected method "%s" on class "%s"', $method, $className);
175
176
        if (count($alternatives) > 0) {
177
            $message .= sprintf(', did you mean "%s"?', implode('", "', $alternatives));
178
        } else {
179
            $message .= sprintf('. Available methods: "%s".', implode('", "', $collection));
180
        }
181
182
        return $message;
183
    }
184
185
    /** {@inheritdoc} */
186 6
    public function getArguments(RequestInterface $request, $controller)
187
    {
188 6
        if (is_array($controller)) {
189 6
            $r = new \ReflectionMethod($controller[0], $controller[1]);
190 6
        } elseif (is_object($controller) && !$controller instanceof \Closure) {
191
            $r = new \ReflectionObject($controller);
192
            $r = $r->getMethod('__invoke');
193
        } else {
194
            $r = new \ReflectionFunction($controller);
195
        }
196
197 6
        return $this->doGetArguments($request, $r->getParameters());
198
    }
199
200
    /**
201
     * @param RequestInterface       $request
202
     * @param \ReflectionParameter[] $parameters
203
     *
204
     * @return array
205
     * @throws \RuntimeException
206
     */
207 6
    protected function doGetArguments(RequestInterface $request, array $parameters)
208
    {
209 6
        $attributes = $request->getAttributes()->all();
210 6
        $arguments  = [];
211 6
        $missing    = [];
212 6
        foreach ($parameters as $param) {
213 6
            if (is_array($request->getParameters()) && array_key_exists($param->name, $request->getParameters())) {
214 5
                $arguments[] = $this->checkType($request->getParameters()[$param->name], $param, $request);
215 6
            } elseif (array_key_exists($param->name, $attributes)) {
216
                $arguments[] = $this->checkType($attributes[$param->name], $param->name, $request);
217 6
            } elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
218 5
                $arguments[] = $request;
219 6
            } elseif ($param->isDefaultValueAvailable()) {
220 5
                $arguments[] = $param->getDefaultValue();
221 5
            } else {
222 2
                $missing[] = $param->name;
223
            }
224 6
        }
225
226 5
        if (count($missing) > 0) {
227 2
            throw InvalidMethodParametersException::missing($request->getMethod(), $missing);
228
        }
229
230 3
        return $arguments;
231
    }
232
233
    /**
234
     * Checks that argument matches parameter type
235
     *
236
     * @param mixed                $argument
237
     * @param \ReflectionParameter $param
238
     * @param RequestInterface     $request
239
     *
240
     * @return mixed
241
     */
242 5
    private function checkType($argument, \ReflectionParameter $param, RequestInterface $request)
243
    {
244 5
        $actual = is_object($argument) ? get_class($argument) : gettype($argument);
245 5
        if (null !== $param->getClass()) {
246
            $className = $param->getClass();
247
            if (!($argument instanceof $className)) {
248
                throw InvalidMethodParametersException::typeMismatch(
249
                    $request->getMethod(),
250
                    $param->name,
251
                    $className,
252
                    $actual
253
                );
254
            }
255 5
        } elseif ($param->isArray() && !is_array($argument)) {
256 1
            throw InvalidMethodParametersException::typeMismatch(
257 1
                $request->getMethod(),
258 1
                $param->name,
259 1
                'array',
260
                $actual
261 1
            );
262
        }
263
264 5
        return $argument;
265
    }
266
}
267