Completed
Push — master ( d34cec...faecd8 )
by Augusto
02:22
created

Request::response()   C

Complexity

Conditions 7
Paths 21

Size

Total Lines 40
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.0572

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 40
ccs 17
cts 19
cp 0.8947
rs 6.7272
cc 7
eloc 20
nc 21
nop 0
crap 7.0572
1
<?php
2
/*
3
 * This file is part of the Respect\Rest package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace Respect\Rest;
10
11
use ReflectionFunctionAbstract;
12
use ReflectionParameter;
13
use Respect\Rest\Routes\AbstractRoute;
14
use Respect\Rest\Routines\Routinable;
15
use Respect\Rest\Routines\ProxyableBy;
16
use Respect\Rest\Routines\ProxyableThrough;
17
use Respect\Rest\Routines\ParamSynced;
18
19
/** A routed HTTP Request */
20
class Request
21
{
22
    /** @var string The HTTP method (commonly GET, POST, PUT, DELETE, HEAD) */
23
    public $method = '';
24
25
    /**
26
     * @var array A numeric array containing valid URL parameters. For a route
27
     * path like /users/*, a Request for /users/alganet should have an array
28
     * equivalent to ['alganet']
29
     */
30
    public $params = array();
31
32
    /** @var AbstractRoute A route matched for this request */
33
    public $route;
34
35
    /** @var string The called URI */
36
    public $uri = '';
37
38
    /**
39
     * @param string $method The HTTP method
40
     * @param string $uri    The called URI
41
     */
42 130
    public function __construct($method = null, $uri = null)
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
43
    {
44
        //Tries to infer request variables only if null
45 130
        if (is_null($method)) {
46 5
            $method = isset($_SERVER['REQUEST_METHOD'])
47 5
                ? $_SERVER['REQUEST_METHOD']
48 5
                : 'GET';
49 5
        }
50
51 130
        if (is_null($uri)) {
52 6
            $uri = isset($_SERVER['REQUEST_URI'])
53 6
                ? $_SERVER['REQUEST_URI']
54 6
                : '/';
55 6
        }
56
57 130
        $uri = parse_url($uri, PHP_URL_PATH);
58 130
        $this->uri = rtrim($uri, ' /');       //We always ignore the last /
59 130
        $this->method = strtoupper($method);  //normalizing the HTTP method
60 130
    }
61
62
    /**
63
     * Converting this request to string dispatch
64
     */
65 7
    public function __toString()
66
    {
67 7
        return $this->response();
68
    }
69
70
    /**
71
     * Declares an error handler for a single Router::errorRoute instance on the
72
     * fly before dispatching the request, so the application can capture the
73
     * errors. These are cleaned after dispatching by forwardErrors()
74
     *
75
     * @see    Respect\Rest\Request::forwardErrors
76
     * @see    http://php.net/set_error_handler
77
     *
78
     * @return mixed The previous error handler
79
     */
80 111
    protected function prepareForErrorForwards()
81
    {
82 111
       foreach ($this->route->sideRoutes as $sideRoute) {
83
            if ($sideRoute instanceof Routes\Error) {
84
                return set_error_handler(
85
                    function () use ($sideRoute) {
86
                        $sideRoute->errors[] = func_get_args();
87
                    }
88
                );
89
            }
90 111
        }
91 111
    }
92
93
    /**
94
     * Iterates over routines to find instances of
95
     * Respect\Rest\Routines\ProxyableBy and call them, forwarding if
96
     * necessary
97
     *
98
     * @see    Respect\Rest\Routines\ProxyableBy
99
     * @see    Respect\Rest\Request::routineCall
100
     *
101
     * @return mixed A route forwarding or false
102
     */
103 111
    protected function processPreRoutines()
104
    {
105 111
        foreach ($this->route->routines as $routine) {
106 48
            if (!$routine instanceof ProxyableBy) {
107 9
                continue;
108
            } else {
109 40
                $result = $this->routineCall(
110 40
                    'by',
111 40
                    $this->method,
112 40
                    $routine,
0 ignored issues
show
Documentation introduced by
$routine is of type object<Respect\Rest\Routines\ProxyableBy>, but the function expects a object<Respect\Rest\Routines\Routinable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
113 40
                    $this->params
114 40
                );
115
116
                //Routine returned an instance, let's forward it
117 40
                if ($result instanceof AbstractRoute) {
118 25
                    return $this->forward($result);
119 38
                } elseif (false === $result) {
120 4
                    return false;
121
                }
122
            }
123 107
        }
124 107
    }
125
126
    /**
127
     * Iterates over routines to find instances of
128
     * Respect\Rest\Routines\ProxyableThrough and call them, forwarding if
129
     * necessary
130
     *
131
     * @see    Respect\Rest\Routines\ProxyableThrough
132
     * @see    Respect\Rest\Request::routineCall
133
     *
134
     * @return mixed A route forwarding or false
135
     */
136 105
    protected function processPosRoutines($response)
137
    {
138 105
        $proxyResults = array();
139
140 105
        foreach ($this->route->routines as $routine) {
141 42
            if ($routine instanceof ProxyableThrough) {
142 30
                $proxyResults[] = $this->routineCall(
143 30
                    'through',
144 30
                    $this->method,
145 30
                    $routine,
0 ignored issues
show
Documentation introduced by
$routine is of type object<Respect\Rest\Routines\ProxyableThrough>, but the function expects a object<Respect\Rest\Routines\Routinable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
146 30
                    $this->params
147 30
                );
148 30
            }
149 105
        }
150
151
        //Some routine returned a callback as its result. Let's run
152
        //these callbacks on our actual response.
153
        //This is mainly used by accept() and similar ones.
154 105
        foreach ($proxyResults as $proxyCallback) {
155 30
            if (is_callable($proxyCallback)) {
156 28
                $response = call_user_func_array(
157 28
                    $proxyCallback,
158 28
                    array($response)
159 28
                );
160 28
            }
161 105
        }
162
163 105
        return $response;
164
    }
165
166
    /**
167
     * Restores the previous error handler if present then check error routes
168
     * for logged errors, forwarding them or returning null silently
169
     *
170
     * @param mixed $errorHandler Some error handler (internal or external to
171
     *                            Respect)
172
     *
173
     * @return mixed A route forwarding or a silent null
174
     */
175 105
    protected function forwardErrors($errorHandler)
176
    {
177 105
        if (isset($errorHandler)) {
178
            if (!$errorHandler) {
179
                restore_error_handler();
180
            } else {
181
                set_error_handler($errorHandler);
182
            }
183
        }
184
185 105
        foreach ($this->route->sideRoutes as $sideRoute) {
186
            if ($sideRoute instanceof Routes\Error && $sideRoute->errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sideRoute->errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
187
                return $this->forward($sideRoute);
188
            }
189 105
        }
190 105
    }
191
192
    /**
193
     * Does a catch-like operation on an exception based on previously
194
     * declared instances from Router::exceptionRoute
195
     *
196
     * @param Exception $e Any exception
197
     *
198
     * @return mixed A route forwarding or a silent null
199
     */
200 2
    protected function catchExceptions($e)
201
    {
202 2
        foreach ($this->route->sideRoutes as $sideRoute) {
203
            $exceptionClass = get_class($e);
204
            if (
205
                $exceptionClass      === $sideRoute->class
206
                || $sideRoute->class === 'Exception'
207
                || $sideRoute->class === '\Exception'
208
            ) {
209
                $sideRoute->exception = $e;
210
211
                return $this->forward($sideRoute);
212
            }
213 2
        }
214 2
    }
215
216
    /**
217
     * Generates and returns the response from the current route
218
     *
219
     * @return string A response!
220
     */
221 115
    public function response()
222
    {
223
        try {
224
            //No routes, get out
225 115
            if (!$this->route instanceof AbstractRoute) {
226 4
                return;
227
            }
228
229 111
            $errorHandler = $this->prepareForErrorForwards();
230 111
            $preRoutineResult = $this->processPreRoutines();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->processPreRoutines(); of type string|false|null adds false to the return on line 233 which is incompatible with the return type documented by Respect\Rest\Request::response of type string. It seems like you forgot to handle an error condition.
Loading history...
231
232 111
            if (!is_null($preRoutineResult)) {
233 6
                return $preRoutineResult;
234
            }
235
236 107
            $response = $this->route->runTarget($this->method, $this->params);
237
238
            //The code returned another route, this is a forward
239 105
            if ($response instanceof AbstractRoute) {
240 2
                return $this->forward($response);
241
            }
242
243 105
            $possiblyModifiedResponse = $this->processPosRoutines($response);
244 105
            $errorResponse = $this->forwardErrors($errorHandler);
245
246 105
            if (!is_null($errorResponse)) {
247
                return $errorResponse;
248
            }
249
250 105
            return $possiblyModifiedResponse;
251 2
        } catch (\Exception $e) {
252
            //Tries to catch it using catchExceptions()
253 2
            if (!$exceptionResponse = $this->catchExceptions($e)) {
0 ignored issues
show
Documentation introduced by
$e is of type object<Exception>, but the function expects a object<Respect\Rest\Exception>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
254 2
                throw $e;
255
            }
256
257
            //Returns whatever the exception routes returned
258
            return (string) $exceptionResponse;
259
        }
260
    }
261
262
    /**
263
     * Calls a routine on the current route and returns its result
264
     *
265
     * @param string     $type    The name of the routine (accept, when, etc)
266
     * @param string     $method  The method name (GET, HEAD, POST, etc)
267
     * @param Routinable $routine Some routine instance
268
     * @param array      $params  Params from the routine
269
     *
270
     * @return mixed Whatever the routine returns
271
     */
272 54
    public function routineCall($type, $method, Routinable $routine, &$params)
273
    {
274 54
        $reflection = $this->route->getReflection(
275
            //GET and HEAD are the same for routines
276 54
            $method == 'HEAD' ? 'GET' : $method
277 54
        );
278
279 54
        $callbackParameters = array();
280
281 54
        if (!$routine instanceof ParamSynced) {
282 36
            $callbackParameters = $params;
283 36
        } else {
284 18
            foreach ($routine->getParameters() as $parameter) {
285 10
                $callbackParameters[] = $this->extractRouteParam(
286 10
                    $reflection,
287 10
                    $parameter,
288
                    $params
289 10
                );
290 18
            }
291
        }
292
293 54
        return $routine->{$type}($this, $callbackParameters);
294
    }
295
296
    /**
297
     * Extracts a parameter value from the current route
298
     *
299
     * @param ReflectionFunctionAbstract $callback   Any function reflection
300
     * @param ReflectionParameter        $routeParam Any parameter reflection
301
     * @param array                      $params     Request URI params
302
     *
303
     * @return mixed a value from the reflected param
304
     */
305 10
    protected function extractRouteParam(
306
        ReflectionFunctionAbstract $callback,
307
        ReflectionParameter $routeParam,
308
        &$params
309
    ) {
310 10
        foreach ($callback->getParameters() as $callbackParamReflection) {
311
            //Check if parameters have same name and present (not filtered)
312
            if (
313 9
                $callbackParamReflection->getName() === $routeParam->getName()
314 9
                && isset($params[$callbackParamReflection->getPosition()])
315 9
            ) {
316 7
                return $params[$callbackParamReflection->getPosition()];
317
            }
318 8
        }
319
320 6
        if ($routeParam->isDefaultValueAvailable()) {
321 3
            return $routeParam->getDefaultValue();
322
        }
323
324 4
        return;
325
    }
326
327
    /**
328
     * Forwards a route
329
     *
330
     * @param AbstractRoute $route Any route
331
     *
332
     * @return Response from the forwarded route
333
     */
334 5
    public function forward(AbstractRoute $route)
335
    {
336 5
        $this->route = $route;
337
338 5
        return $this->response();
339
    }
340
}
341