Completed
Pull Request — master (#6)
by Joao
08:06
created

ServerRequestHandler::getDefaultHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace ByJG\RestServer;
4
5
use ByJG\Cache\Psr16\NoCacheEngine;
6
use ByJG\RestServer\Exception\ClassNotFoundException;
7
use ByJG\RestServer\Exception\Error404Exception;
8
use ByJG\RestServer\Exception\Error405Exception;
9
use ByJG\RestServer\Exception\Error520Exception;
10
use ByJG\RestServer\Exception\InvalidClassException;
11
use ByJG\RestServer\Exception\OperationIdInvalidException;
12
use ByJG\RestServer\Exception\SchemaInvalidException;
13
use ByJG\RestServer\Exception\SchemaNotFoundException;
14
use ByJG\RestServer\HandleOutput\HandleOutputInterface;
15
use ByJG\RestServer\HandleOutput\HtmlHandler;
16
use ByJG\RestServer\HandleOutput\JsonHandler;
17
use ByJG\RestServer\HandleOutput\XmlHandler;
18
use FastRoute\Dispatcher;
19
use FastRoute\RouteCollector;
20
use Psr\SimpleCache\CacheInterface;
21
22
class ServerRequestHandler
23
{
24
    const OK = "OK";
25
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
26
    const NOT_FOUND = "NOT FOUND";
27
28
    protected $routes = null;
29
30
    protected $defaultHandler = null;
31
32
    protected $mimeTypeHandler = [
33
        "text/xml" => XmlHandler::class,
34
        "application/xml" => XmlHandler::class,
35
        "text/html" => HtmlHandler::class,
36
        "application/json" => JsonHandler::class
37
    ];
38
39
    protected $pathHandler = [
40
41
    ];
42
43
    public function getRoutes()
44
    {
45
        return $this->routes;
46
    }
47
48
    /**
49
     * @param \ByJG\RestServer\RoutePattern[] $routes
50
     */
51
    public function setRoutes($routes)
52
    {
53
        foreach ((array)$routes as $route) {
54
            $this->addRoute($route);
55
        }
56
    }
57
58
    /**
59
     * @param \ByJG\RestServer\RoutePattern $route
60
     */
61
    public function addRoute(RoutePattern $route)
62
    {
63
        if (is_null($this->routes)) {
64
            $this->routes = [];
65
        }
66
        $this->routes[] = $route;
67
    }
68
69
    /**
70
     * @return HandleOutputInterface
71
     */
72
    public function getDefaultHandler()
73
    {
74
        if (empty($this->defaultHandler)) {
75
            $this->defaultHandler = new JsonHandler();
76
        }
77
        return $this->defaultHandler;
78
    }
79
80
    /**
81
     * @param HandleOutputInterface $defaultHandler
82
     */
83
    public function setDefaultHandler(HandleOutputInterface $defaultHandler)
84
    {
85
        $this->defaultHandler = $defaultHandler;
86
    }
87
88
    /**
89
     * @throws \ByJG\RestServer\Exception\ClassNotFoundException
90
     * @throws \ByJG\RestServer\Exception\Error404Exception
91
     * @throws \ByJG\RestServer\Exception\Error405Exception
92
     * @throws \ByJG\RestServer\Exception\Error520Exception
93
     * @throws \ByJG\RestServer\Exception\InvalidClassException
94
     */
95
    protected function process()
0 ignored issues
show
Coding Style introduced by
process 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...
96
    {
97
        // Initialize ErrorHandler with default error handler
98
        ErrorHandler::getInstance()->register();
99
100
        // Get the URL parameters
101
        $httpMethod = $_SERVER['REQUEST_METHOD'];
102
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
103
        parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr);
104
105
        // Generic Dispatcher for RestServer
106
        $dispatcher = \FastRoute\simpleDispatcher(function (RouteCollector $r) {
107
108
            foreach ($this->getRoutes() as $route) {
109
                $r->addRoute(
110
                    $route->properties('method'),
111
                    $route->properties('pattern'),
112
                    [
113
                        "handler" => $route->properties('handler'),
114
                        "class" => $route->properties('class'),
115
                        "function" => $route->properties('function')
116
                    ]
117
                );
118
            }
119
        });
120
121
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
0 ignored issues
show
Security Bug introduced by
It seems like $uri defined by parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) on line 102 can also be of type false; however, FastRoute\Dispatcher::dispatch() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
122
123
        // Default Handler for errors
124
        $this->getDefaultHandler()->writeHeader();
125
        ErrorHandler::getInstance()->setHandler($this->getDefaultHandler()->getErrorHandler());
126
127
        // Processing
128
        switch ($routeInfo[0]) {
129
            case Dispatcher::NOT_FOUND:
130
                throw new Error404Exception("404 Route '$uri' Not found");
131
132
            case Dispatcher::METHOD_NOT_ALLOWED:
133
                throw new Error405Exception('405 Method Not Allowed');
134
135
            case Dispatcher::FOUND:
136
                // ... 200 Process:
137
                $vars = array_merge($routeInfo[2], $queryStr);
138
139
                // Instantiate the Service Handler
140
                $handlerRequest = $routeInfo[1];
141
142
                // Execute the request
143
                $handler = !empty($handlerRequest['handler']) ? $handlerRequest['handler'] : $this->getDefaultHandler();
144
                $this->executeRequest(
145
                    new $handler(),
146
                    $handlerRequest['class'],
147
                    $handlerRequest['function'],
148
                    $vars
149
                );
150
151
                break;
152
153
            default:
154
                throw new Error520Exception('Unknown');
155
        }
156
    }
157
158
    /**
159
     * @param HandleOutputInterface $handler
160
     * @param string $class
161
     * @param string $function
162
     * @param array $vars
163
     * @throws \ByJG\RestServer\Exception\ClassNotFoundException
164
     * @throws \ByJG\RestServer\Exception\InvalidClassException
165
     */
166
    protected function executeRequest($handler, $class, $function, $vars)
0 ignored issues
show
Coding Style introduced by
executeRequest uses the super-global variable $_REQUEST 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...
Coding Style introduced by
executeRequest uses the super-global variable $_GET 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...
Coding Style introduced by
executeRequest uses the super-global variable $_POST 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...
Coding Style introduced by
executeRequest 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...
Coding Style introduced by
executeRequest uses the super-global variable $_SESSION 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...
Coding Style introduced by
executeRequest uses the super-global variable $_COOKIE 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...
167
    {
168
        // Setting Default Headers and Error Handler
169
        $handler->writeHeader();
170
        ErrorHandler::getInstance()->setHandler($handler->getErrorHandler());
171
172
        // Set all default values
173
        foreach (array_keys($vars) as $key) {
174
            $_REQUEST[$key] = $_GET[$key] = $vars[$key];
175
        }
176
177
        // Create the Request and Response methods
178
        $request = new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE);
179
        $response = new HttpResponse();
180
181
        // Process Closure
182
        if ($function instanceof \Closure) {
183
            $function($response, $request);
184
            echo $handler->processResponse($response);
185
            return;
186
        }
187
188
        // Process Class::Method()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
189
        if (!class_exists($class)) {
190
            throw new ClassNotFoundException("Class '$class' defined in the route is not found");
191
        }
192
        $instance = new $class();
193
        if (!method_exists($instance, $function)) {
194
            throw new InvalidClassException("There is no method '$class::$function''");
195
        }
196
        $instance->$function($response, $request);
197
        $handler->processResponse($response);
198
    }
199
200
    /**
201
     * Handle the ROUTE (see web/app-dist.php)
202
     *
203
     * @param \ByJG\RestServer\RoutePattern[]|null $routePattern
204
     * @param bool $outputBuffer
205
     * @param bool $session
206
     * @throws \ByJG\RestServer\Exception\ClassNotFoundException
207
     * @throws \ByJG\RestServer\Exception\Error404Exception
208
     * @throws \ByJG\RestServer\Exception\Error405Exception
209
     * @throws \ByJG\RestServer\Exception\Error520Exception
210
     * @throws \ByJG\RestServer\Exception\InvalidClassException
211
     */
212
    public function handle($routePattern = null, $outputBuffer = true, $session = true)
0 ignored issues
show
Coding Style introduced by
handle 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...
213
    {
214
        if ($outputBuffer) {
215
            ob_start();
216
        }
217
        if ($session) {
218
            session_start();
219
        }
220
221
        /**
222
         * @var ServerRequestHandler
223
         */
224
        $this->setRoutes($routePattern);
0 ignored issues
show
Bug introduced by
It seems like $routePattern defined by parameter $routePattern on line 212 can also be of type null; however, ByJG\RestServer\ServerRequestHandler::setRoutes() does only seem to accept array<integer,object<ByJ...stServer\RoutePattern>>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
225
226
        // --------------------------------------------------------------------------
227
        // Check if script exists or if is itself
228
        // --------------------------------------------------------------------------
229
230
        $debugBacktrace =  debug_backtrace();
231
        if (!empty($_SERVER['SCRIPT_FILENAME'])
232
            && file_exists($_SERVER['SCRIPT_FILENAME'])
233
            && basename($_SERVER['SCRIPT_FILENAME']) !== basename($debugBacktrace[0]['file'])
234
        ) {
235
            $file = $_SERVER['SCRIPT_FILENAME'];
236
            if (strpos($file, '.php') !== false) {
237
                require_once($file);
238
            } else {
239
                header("Content-Type: " . $this->mimeContentType($file));
240
241
                echo file_get_contents($file);
242
            }
243
            return;
244
        }
245
246
        $this->process();
247
    }
248
249
    /**
250
     * Get the Mime Type based on the filename
251
     *
252
     * @param string $filename
253
     * @return string
254
     */
255
    protected function mimeContentType($filename)
256
    {
257
258
        $mimeTypes = array(
259
            'txt' => 'text/plain',
260
            'htm' => 'text/html',
261
            'html' => 'text/html',
262
            'php' => 'text/html',
263
            'css' => 'text/css',
264
            'js' => 'application/javascript',
265
            'json' => 'application/json',
266
            'xml' => 'application/xml',
267
            'swf' => 'application/x-shockwave-flash',
268
            'flv' => 'video/x-flv',
269
            // images
270
            'png' => 'image/png',
271
            'jpe' => 'image/jpeg',
272
            'jpeg' => 'image/jpeg',
273
            'jpg' => 'image/jpeg',
274
            'gif' => 'image/gif',
275
            'bmp' => 'image/bmp',
276
            'ico' => 'image/vnd.microsoft.icon',
277
            'tiff' => 'image/tiff',
278
            'tif' => 'image/tiff',
279
            'svg' => 'image/svg+xml',
280
            'svgz' => 'image/svg+xml',
281
            // archives
282
            'zip' => 'application/zip',
283
            'rar' => 'application/x-rar-compressed',
284
            'exe' => 'application/x-msdownload',
285
            'msi' => 'application/x-msdownload',
286
            'cab' => 'application/vnd.ms-cab-compressed',
287
            // audio/video
288
            'mp3' => 'audio/mpeg',
289
            'qt' => 'video/quicktime',
290
            'mov' => 'video/quicktime',
291
            // adobe
292
            'pdf' => 'application/pdf',
293
            'psd' => 'image/vnd.adobe.photoshop',
294
            'ai' => 'application/postscript',
295
            'eps' => 'application/postscript',
296
            'ps' => 'application/postscript',
297
            // ms office
298
            'doc' => 'application/msword',
299
            'rtf' => 'application/rtf',
300
            'xls' => 'application/vnd.ms-excel',
301
            'ppt' => 'application/vnd.ms-powerpoint',
302
            // open office
303
            'odt' => 'application/vnd.oasis.opendocument.text',
304
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
305
        );
306
307
        $ext = strtolower(array_pop(explode('.', $filename)));
0 ignored issues
show
Bug introduced by
explode('.', $filename) cannot be passed to array_pop() as the parameter $array expects a reference.
Loading history...
308
        if (array_key_exists($ext, $mimeTypes)) {
309
            return $mimeTypes[$ext];
310
        } elseif (function_exists('finfo_open')) {
311
            $finfo = finfo_open(FILEINFO_MIME);
312
            $mimetype = finfo_file($finfo, $filename);
313
            finfo_close($finfo);
314
            return $mimetype;
315
        } else {
316
            return 'application/octet-stream';
317
        }
318
    }
319
320
    /**
321
     * @param $swaggerJson
322
     * @param \Psr\SimpleCache\CacheInterface|null $cache
323
     * @throws \ByJG\RestServer\Exception\SchemaInvalidException
324
     * @throws \ByJG\RestServer\Exception\SchemaNotFoundException
325
     * @throws \ByJG\RestServer\Exception\OperationIdInvalidException
326
     */
327
    public function setRoutesSwagger($swaggerJson, CacheInterface $cache = null)
328
    {
329
        if (!file_exists($swaggerJson)) {
330
            throw new SchemaNotFoundException("Schema '$swaggerJson' not found");
331
        }
332
333
        $schema = json_decode(file_get_contents($swaggerJson), true);
334
        if (!isset($schema['paths'])) {
335
            throw new SchemaInvalidException("Schema '$swaggerJson' is invalid");
336
        }
337
338
        if (is_null($cache)) {
339
            $cache = new NoCacheEngine();
340
        }
341
342
        $routePattern = $cache->get('SERVERHANDLERROUTES', false);
343
        if ($routePattern === false) {
344
            $routePattern = $this->generateRoutes($schema);
345
            $cache->set('SERVERHANDLERROUTES', $routePattern);
346
        }
347
        $this->setRoutes($routePattern);
348
    }
349
350
    /**
351
     * @param $schema
352
     * @return array
353
     * @throws \ByJG\RestServer\Exception\OperationIdInvalidException
354
     */
355
    protected function generateRoutes($schema)
356
    {
357
        $pathList = $this->sortPaths(array_keys($schema['paths']));
358
359
        $routes = [];
360
        foreach ($pathList as $path) {
361
            foreach ($schema['paths'][$path] as $method => $properties) {
362
                $handler = $this->getMethodHandler($method, $path, $properties);
363
                if (!isset($properties['operationId'])) {
364
                    throw new OperationIdInvalidException('OperationId was not found');
365
                }
366
367
                $parts = explode('::', $properties['operationId']);
368
                if (count($parts) !== 2) {
369
                    throw new OperationIdInvalidException(
370
                        'OperationId needs to be in the format Namespace\\class::method'
371
                    );
372
                }
373
374
                $routes[] = new RoutePattern(
375
                    strtoupper($method),
376
                    $path,
377
                    $handler,
378
                    $parts[1],
379
                    $parts[0]
380
                );
381
            }
382
        }
383
384
        return $routes;
385
    }
386
387
    protected function sortPaths($pathList)
388
    {
389
        usort($pathList, function ($left, $right) {
390 View Code Duplication
            if (strpos($left, '{') === false && strpos($right, '{') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
                return -16384;
392
            }
393 View Code Duplication
            if (strpos($left, '{') !== false && strpos($right, '{') === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
                return 16384;
395
            }
396
            if (strpos($left, $right) !== false) {
397
                return -16384;
398
            }
399
            if (strpos($right, $left) !== false) {
400
                return 16384;
401
            }
402
            return strcmp($left, $right);
403
        });
404
405
        return $pathList;
406
    }
407
408
    /**
409
     * @param $method
410
     * @param $path
411
     * @param $properties
412
     * @return string
413
     * @throws \ByJG\RestServer\Exception\OperationIdInvalidException
414
     */
415
    protected function getMethodHandler($method, $path, $properties)
416
    {
417
        $method = strtoupper($method);
418
        if (isset($this->pathHandler["$method::$path"])) {
419
            return $this->pathHandler["$method::$path"];
420
        }
421
        if (!isset($properties['produces'])) {
422
            return get_class($this->getDefaultHandler());
423
        }
424
425
        $produces = $properties['produces'];
426
        if (is_array($produces)) {
427
            $produces = $produces[0];
428
        }
429
430
        if (!isset($this->mimeTypeHandler[$produces])) {
431
            throw new OperationIdInvalidException("There is no handler for $produces");
432
        }
433
434
        return $this->mimeTypeHandler[$produces];
435
    }
436
437
    public function setMimeTypeHandler($mimetype, $handler)
438
    {
439
        $this->mimeTypeHandler[$mimetype] = $handler;
440
    }
441
442
    public function setPathHandler($method, $path, $handler)
443
    {
444
        $method = strtoupper($method);
445
        $this->pathHandler["$method::$path"] = $handler;
446
    }
447
}
448