Completed
Pull Request — master (#8)
by Joao
01:20
created

ServerRequestHandler   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 16
dl 0
loc 376
rs 8.8798
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getRoutes() 0 4 1
A setRoutes() 0 6 2
A addRoute() 0 7 2
A getDefaultHandler() 0 7 2
A setDefaultHandler() 0 4 1
B process() 0 62 6
B executeRequest() 0 33 6
B handle() 0 38 8
B mimeContentType() 0 68 4
A setRoutesSwagger() 0 15 3
A setMimeTypeHandler() 0 4 1
A setPathHandler() 0 5 1
B getMethodHandler() 0 27 7

How to fix   Complexity   

Complex Class

Complex classes like ServerRequestHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ServerRequestHandler, and based on these observations, apply Extract Interface, too.

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 Closure;
19
use FastRoute\Dispatcher;
20
use FastRoute\RouteCollector;
21
use Psr\SimpleCache\CacheInterface;
22
use Psr\SimpleCache\InvalidArgumentException;
23
use function FastRoute\simpleDispatcher;
24
25
class ServerRequestHandler
26
{
27
    const OK = "OK";
28
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
29
    const NOT_FOUND = "NOT FOUND";
30
31
    protected $routes = null;
32
33
    protected $defaultHandler = null;
34
35
    protected $mimeTypeHandler = [
36
        "text/xml" => XmlHandler::class,
37
        "application/xml" => XmlHandler::class,
38
        "text/html" => HtmlHandler::class,
39
        "application/json" => JsonHandler::class
40
    ];
41
42
    protected $pathHandler = [
43
44
    ];
45
46
    public function getRoutes()
47
    {
48
        return $this->routes;
49
    }
50
51
    /**
52
     * @param RoutePattern[] $routes
53
     */
54
    public function setRoutes($routes)
55
    {
56
        foreach ((array)$routes as $route) {
57
            $this->addRoute($route);
58
        }
59
    }
60
61
    /**
62
     * @param RoutePattern $route
63
     */
64
    public function addRoute(RoutePattern $route)
65
    {
66
        if (is_null($this->routes)) {
67
            $this->routes = [];
68
        }
69
        $this->routes[] = $route;
70
    }
71
72
    /**
73
     * @return HandleOutputInterface
74
     */
75
    public function getDefaultHandler()
76
    {
77
        if (empty($this->defaultHandler)) {
78
            $this->defaultHandler = new JsonHandler();
79
        }
80
        return $this->defaultHandler;
81
    }
82
83
    /**
84
     * @param HandleOutputInterface $defaultHandler
85
     */
86
    public function setDefaultHandler(HandleOutputInterface $defaultHandler)
87
    {
88
        $this->defaultHandler = $defaultHandler;
89
    }
90
91
    /**
92
     * @throws ClassNotFoundException
93
     * @throws Error404Exception
94
     * @throws Error405Exception
95
     * @throws Error520Exception
96
     * @throws InvalidClassException
97
     */
98
    protected function process()
99
    {
100
        // Initialize ErrorHandler with default error handler
101
        ErrorHandler::getInstance()->register();
102
103
        // Get the URL parameters
104
        $httpMethod = $_SERVER['REQUEST_METHOD'];
105
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
106
        parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr);
107
108
        // Generic Dispatcher for RestServer
109
        $dispatcher = simpleDispatcher(function (RouteCollector $r) {
110
111
            foreach ($this->getRoutes() as $route) {
112
                $r->addRoute(
113
                    $route->properties('method'),
114
                    $route->properties('pattern'),
115
                    [
116
                        "handler" => $route->properties('handler'),
117
                        "class" => $route->properties('class'),
118
                        "function" => $route->properties('function')
119
                    ]
120
                );
121
            }
122
        });
123
124
        $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 105 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...
125
126
        // Default Handler for errors
127
        $this->getDefaultHandler()->writeHeader();
128
        ErrorHandler::getInstance()->setHandler($this->getDefaultHandler()->getErrorHandler());
129
130
        // Processing
131
        switch ($routeInfo[0]) {
132
            case Dispatcher::NOT_FOUND:
133
                throw new Error404Exception("404 Route '$uri' Not found");
134
135
            case Dispatcher::METHOD_NOT_ALLOWED:
136
                throw new Error405Exception('405 Method Not Allowed');
137
138
            case Dispatcher::FOUND:
139
                // ... 200 Process:
140
                $vars = array_merge($routeInfo[2], $queryStr);
141
142
                // Instantiate the Service Handler
143
                $handlerRequest = $routeInfo[1];
144
145
                // Execute the request
146
                $handler = !empty($handlerRequest['handler']) ? $handlerRequest['handler'] : $this->getDefaultHandler();
147
                $this->executeRequest(
148
                    new $handler(),
149
                    $handlerRequest['class'],
150
                    $handlerRequest['function'],
151
                    $vars
152
                );
153
154
                break;
155
156
            default:
157
                throw new Error520Exception('Unknown');
158
        }
159
    }
160
161
    /**
162
     * @param HandleOutputInterface $handler
163
     * @param string $class
164
     * @param string $function
165
     * @param array $vars
166
     * @throws ClassNotFoundException
167
     * @throws InvalidClassException
168
     */
169
    protected function executeRequest($handler, $class, $function, $vars)
170
    {
171
        // Setting Default Headers and Error Handler
172
        $handler->writeHeader();
173
        ErrorHandler::getInstance()->setHandler($handler->getErrorHandler());
174
175
        // Set all default values
176
        foreach (array_keys($vars) as $key) {
177
            $_REQUEST[$key] = $_GET[$key] = $vars[$key];
178
        }
179
180
        // Create the Request and Response methods
181
        $request = new HttpRequest($_GET, $_POST, $_SERVER, isset($_SESSION) ? $_SESSION : [], $_COOKIE);
182
        $response = new HttpResponse();
183
184
        // Process Closure
185
        if ($function instanceof Closure) {
186
            $function($response, $request);
187
            echo $handler->processResponse($response);
188
            return;
189
        }
190
191
        // Process Class::Method()
192
        if (!class_exists($class)) {
193
            throw new ClassNotFoundException("Class '$class' defined in the route is not found");
194
        }
195
        $instance = new $class();
196
        if (!method_exists($instance, $function)) {
197
            throw new InvalidClassException("There is no method '$class::$function''");
198
        }
199
        $instance->$function($response, $request);
200
        $handler->processResponse($response);
201
    }
202
203
    /**
204
     * Handle the ROUTE (see web/app-dist.php)
205
     *
206
     * @param RoutePattern[]|null $routePattern
207
     * @param bool $outputBuffer
208
     * @param bool $session
209
     * @return bool|void
210
     * @throws ClassNotFoundException
211
     * @throws Error404Exception
212
     * @throws Error405Exception
213
     * @throws Error520Exception
214
     * @throws InvalidClassException
215
     */
216
    public function handle($routePattern = null, $outputBuffer = true, $session = true)
217
    {
218
        if ($outputBuffer) {
219
            ob_start();
220
        }
221
        if ($session) {
222
            session_start();
223
        }
224
225
        /**
226
         * @var ServerRequestHandler
227
         */
228
        $this->setRoutes($routePattern);
0 ignored issues
show
Bug introduced by
It seems like $routePattern defined by parameter $routePattern on line 216 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...
229
230
        // --------------------------------------------------------------------------
231
        // Check if script exists or if is itself
232
        // --------------------------------------------------------------------------
233
234
        $debugBacktrace =  debug_backtrace();
235
        if (!empty($_SERVER['SCRIPT_FILENAME'])
236
            && file_exists($_SERVER['SCRIPT_FILENAME'])
237
            && basename($_SERVER['SCRIPT_FILENAME']) !== basename($debugBacktrace[0]['file'])
238
        ) {
239
            $file = $_SERVER['SCRIPT_FILENAME'];
240
            if (strrchr($file, '.') === ".php") {
241
                require_once($file);
242
            } else {
243
                if (!defined("RESTSERVER_TEST")) {
244
                    header("Content-Type: " . $this->mimeContentType($file));
245
                }
246
247
                echo file_get_contents($file);
248
            }
249
            return true;
250
        }
251
252
        return $this->process();
253
    }
254
255
    /**
256
     * Get the Mime Type based on the filename
257
     *
258
     * @param string $filename
259
     * @return string
260
     * @throws Error404Exception
261
     */
262
    public function mimeContentType($filename)
263
    {
264
265
        $mimeTypes = array(
266
            'txt' => 'text/plain',
267
            'htm' => 'text/html',
268
            'html' => 'text/html',
269
            'php' => 'text/html',
270
            'css' => 'text/css',
271
            'js' => 'application/javascript',
272
            'json' => 'application/json',
273
            'xml' => 'application/xml',
274
            'swf' => 'application/x-shockwave-flash',
275
            'flv' => 'video/x-flv',
276
            // images
277
            'png' => 'image/png',
278
            'jpe' => 'image/jpeg',
279
            'jpeg' => 'image/jpeg',
280
            'jpg' => 'image/jpeg',
281
            'gif' => 'image/gif',
282
            'bmp' => 'image/bmp',
283
            'ico' => 'image/vnd.microsoft.icon',
284
            'tiff' => 'image/tiff',
285
            'tif' => 'image/tiff',
286
            'svg' => 'image/svg+xml',
287
            'svgz' => 'image/svg+xml',
288
            // archives
289
            'zip' => 'application/zip',
290
            'rar' => 'application/x-rar-compressed',
291
            'exe' => 'application/x-msdownload',
292
            'msi' => 'application/x-msdownload',
293
            'cab' => 'application/vnd.ms-cab-compressed',
294
            // audio/video
295
            'mp3' => 'audio/mpeg',
296
            'qt' => 'video/quicktime',
297
            'mov' => 'video/quicktime',
298
            // adobe
299
            'pdf' => 'application/pdf',
300
            'psd' => 'image/vnd.adobe.photoshop',
301
            'ai' => 'application/postscript',
302
            'eps' => 'application/postscript',
303
            'ps' => 'application/postscript',
304
            // ms office
305
            'doc' => 'application/msword',
306
            'rtf' => 'application/rtf',
307
            'xls' => 'application/vnd.ms-excel',
308
            'ppt' => 'application/vnd.ms-powerpoint',
309
            // open office
310
            'odt' => 'application/vnd.oasis.opendocument.text',
311
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
312
        );
313
314
        if (!file_exists($filename)) {
315
            throw new Error404Exception();
316
        }
317
318
        $ext = substr(strrchr($filename, "."), 1);
319
        if (array_key_exists($ext, $mimeTypes)) {
320
            return $mimeTypes[$ext];
321
        } elseif (function_exists('finfo_open')) {
322
            $finfo = finfo_open(FILEINFO_MIME);
323
            $mimetype = finfo_file($finfo, $filename);
324
            finfo_close($finfo);
325
            return $mimetype;
326
        } else {
327
            return 'application/octet-stream';
328
        }
329
    }
330
331
    /**
332
     * @param $swaggerJson
333
     * @param CacheInterface|null $cache
334
     * @throws SchemaInvalidException
335
     * @throws SchemaNotFoundException
336
     * @throws OperationIdInvalidException
337
     * @throws InvalidArgumentException
338
     */
339
    public function setRoutesSwagger($swaggerJson, CacheInterface $cache = null)
340
    {
341
        $swaggerWrapper = new SwaggerWrapper($swaggerJson, $this);
342
343
        if (is_null($cache)) {
344
            $cache = new NoCacheEngine();
345
        }
346
347
        $routePattern = $cache->get('SERVERHANDLERROUTES', false);
348
        if ($routePattern === false) {
349
            $routePattern = $swaggerWrapper->generateRoutes();
350
            $cache->set('SERVERHANDLERROUTES', $routePattern);
351
        }
352
        $this->setRoutes($routePattern);
353
    }
354
355
    public function setMimeTypeHandler($mimetype, $handler)
356
    {
357
        $this->mimeTypeHandler[$mimetype] = $handler;
358
    }
359
360
    public function setPathHandler($method, $path, $handler)
361
    {
362
        $method = strtoupper($method);
363
        $this->pathHandler["$method::$path"] = $handler;
364
    }
365
366
    /**
367
     * @param $method
368
     * @param $path
369
     * @param $properties
370
     * @return string
371
     * @throws OperationIdInvalidException
372
     */
373
    public function getMethodHandler($method, $path, $properties)
374
    {
375
        $method = strtoupper($method);
376
        if (isset($this->pathHandler["$method::$path"])) {
377
            return $this->pathHandler["$method::$path"];
378
        }
379
380
        $produces = null;
381
        if (isset($properties['produces'])) {
382
            $produces = (array) $properties['produces'];
383
        }
384
        if (empty($produces) && isset($properties["responses"]["200"]["content"])) {
385
            $produces = array_keys($properties["responses"]["200"]["content"]);
386
        }
387
388
        if (empty($produces)) {
389
            return get_class($this->getDefaultHandler());
390
        }
391
392
        $produces = $produces[0];
393
394
        if (!isset($this->mimeTypeHandler[$produces])) {
395
            throw new OperationIdInvalidException("There is no handler for $produces");
396
        }
397
398
        return $this->mimeTypeHandler[$produces];
399
    }
400
}
401