Completed
Push — master ( 94b286...8ac024 )
by Joao
06:47
created

src/RouteHandler.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace ByJG\RestServer;
4
5
use BadMethodCallException;
6
use ByJG\RestServer\ErrorHandler;
7
use ByJG\RestServer\Exception\ClassNotFoundException;
8
use ByJG\RestServer\Exception\Error404Exception;
9
use ByJG\RestServer\Exception\Error405Exception;
10
use ByJG\RestServer\Exception\Error520Exception;
11
use ByJG\RestServer\Exception\InvalidClassException;
12
use ByJG\RestServer\HandlerInterface;
13
use ByJG\RestServer\Output;
14
use ByJG\RestServer\RouteHandler;
15
use ByJG\RestServer\ServiceAbstract;
16
use FastRoute\Dispatcher;
17
use FastRoute\RouteCollector;
18
use InvalidArgumentException;
19
20
class RouteHandler
21
{
22
23
    use \ByJG\DesignPattern\Singleton;
24
25
    const OK = "OK";
26
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
27
    const NOT_FOUND = "NOT FOUND";
28
29
    protected $_defaultMethods = [
30
        // Service
31
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}/{action}/{id:[0-9]+}/{secondid}.{output}' ],
32
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}/{action}/{id:[0-9]+}.{output}' ],
33
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}/{id:[0-9]+}/{action}.{output}' ],
34
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}/{id:[0-9]+}.{output}' ],
35
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}/{action}.{output}' ],
36
        [ "method" => ['GET', 'POST', 'PUT', 'DELETE'], "pattern" => '/{version}/{module}.{output}' ]
37
    ];
38
    protected $_moduleAlias = [];
39
    protected $_defaultRestVersion = '1.0';
40
    protected $_defaultHandler = '\ByJG\RestServer\ServiceHandler';
41
    protected $_defaultOutput = null;
42
43
    public function getDefaultMethods()
44
    {
45
        return $this->_defaultMethods;
46
    }
47
48
    public function setDefaultMethods($methods)
49
    {
50
        if (!is_array($methods)) {
51
            throw new InvalidArgumentException('You need pass an array');
52
        }
53
54
        foreach ($methods as $value) {
55
            if (!isset($value['method']) || !isset($value['pattern'])) {
56
                throw new InvalidArgumentException('Array has not the valid format');
57
            }
58
        }
59
60
        $this->_defaultMethods = $methods;
61
    }
62
63
    public function getDefaultRestVersion()
64
    {
65
        return $this->_defaultRestVersion;
66
    }
67
68
    public function setDefaultRestVersion($version)
69
    {
70
        $this->_defaultRestVersion = $version;
71
    }
72
73
    public function getDefaultHandler()
74
    {
75
        return $this->_defaultHandler;
76
    }
77
78
    public function setDefaultHandler($value)
79
    {
80
        $this->_defaultHandler = $value;
81
    }
82
83
    public function getDefaultOutput()
84
    {
85
        return empty($this->_defaultOutput) ? Output::JSON : $this->_defaultOutput;
86
    }
87
88
    public function setDefaultOutput($defaultOutput)
89
    {
90
        $this->_defaultOutput = $defaultOutput;
91
    }
92
93
    public function getModuleAlias()
94
    {
95
        return $this->_moduleAlias;
96
    }
97
98
    public function addModuleAlias($alias, $module)
99
    {
100
        $this->_moduleAlias[$alias] = $module;
101
    }
102
103
    public function process()
104
    {
105
        // Initialize ErrorHandler with default error handler
106
        ErrorHandler::getInstance()->register();
107
108
        // Get the URL parameters
109
        $httpMethod = $_SERVER['REQUEST_METHOD'];
110
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
111
        parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr);
112
113
        // Generic Dispatcher for RestServer
114
        $dispatcher = \FastRoute\simpleDispatcher(function(RouteCollector $r) {
115
116
            foreach ($this->getDefaultMethods() as $route) {
117
                $r->addRoute(
118
                    $route['method'],
119
                    str_replace('{version}', $this->getDefaultRestVersion(), $route['pattern']),
120
                    isset($route['handler']) ? $route['handler'] : $this->getDefaultHandler()
121
                );
122
            }
123
        });
124
125
        $routeInfo = $dispatcher->dispatch($httpMethod, $uri);
0 ignored issues
show
It seems like $uri defined by parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) on line 110 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...
126
127
        switch ($routeInfo[0]) {
128
            case Dispatcher::NOT_FOUND:
129
130
                throw new Error404Exception('404 Not found');
131
132
            case Dispatcher::METHOD_NOT_ALLOWED:
133
134
                throw new Error405Exception('405 Method Not Allowed');
135
136
            case Dispatcher::FOUND:
137
138
                // ... 200 Process:
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...
139
                $vars = array_merge($routeInfo[2], $queryStr);
140
141
                // Check Alias
142
                $moduleAlias = $this->getModuleAlias();
143
                if (isset($moduleAlias[$vars['module']])) {
144
                    $vars['module'] = $moduleAlias[$vars['module']];
145
                }
146
                $vars['module'] = '\\' . str_replace('.', '\\', $vars['module']);
147
148
                // Define output
149
                if (!isset($vars['output'])) {
150
                    $vars['output'] = $this->getDefaultOutput();
151
                }
152
                ErrorHandler::getInstance()->setHandler($vars['output']);
153
154
                // Set all default values
155
                foreach ($vars as $key => $value) {
156
                    $_REQUEST[$key] = $_GET[$key] = $vars[$key];
157
                }
158
159
                // Instantiate the Service Handler
160
                $handlerInstance = $this->getHandler($routeInfo[1], $vars['output']);
161
                $instance = $this->executeAction($vars['module']);
162
163
                echo $handlerInstance->execute($instance);
164
                break;
165
166
            default:
167
                throw new Error520Exception('Unknown');
168
        }
169
    }
170
171
    /**
172
     * Get the Handler based on the string
173
     *
174
     * @param string $handler
175
     * @param string $output
176
     * @throws ClassNotFoundException
177
     * @throws InvalidClassException
178
     * @return HandlerInterface Return the Handler Interface
179
     */
180
    public function getHandler($handler, $output)
181
    {
182
        if (!class_exists($handler)) {
183
            throw new ClassNotFoundException("Handler $handler not found");
184
        }
185
        $handlerInstance = new $handler();
186
        if (!($handlerInstance instanceof HandlerInterface)) {
187
            throw new InvalidClassException("Handler $handler is not a HandlerInterface");
188
        }
189
        $handlerInstance->setOutput($output);
190
        $handlerInstance->setHeader();
191
192
        return $handlerInstance;
193
    }
194
195
    /**
196
     * Instantiate the class found in the route
197
     *
198
     * @param string $class
199
     * @return ServiceAbstract
200
     * @throws ClassNotFoundException
201
     * @throws InvalidClassException
202
     * @throws BadMethodCallException
203
     */
204
    public function executeAction($class)
205
    {
206
        // Instantiate a new class
207
        if (!class_exists($class)) {
208
            throw new ClassNotFoundException("Class $class not found");
209
        }
210
        $instance = new $class();
211
212
        if (!($instance instanceof ServiceAbstract)) {
213
            throw new InvalidClassException("Class $class is not an instance of ServiceAbstract");
214
        }
215
216
        // Execute the method
217
        $method = strtolower($instance->getRequest()->server("REQUEST_METHOD")); // get, post, put, delete
218
        $customAction = $method . ($instance->getRequest()->get('action'));
219
        if (method_exists($instance, $customAction)) {
220
            $instance->$customAction();
221
        } else {
222
            throw new BadMethodCallException("The method '$customAction' does not exists.");
223
        }
224
225
        return $instance;
226
    }
227
228
    /**
229
     * Process the ROUTE (see httpdocs/route-dist.php)
230
     *
231
     * ModuleAlias needs to be an array like:
232
     *  [ 'alias' => 'Full.Namespace.To.Class' ]
233
     *
234
     * RoutePattern needs to be an array like:
235
     * [
236
     *     [
237
     *         "method" => ['GET'],
238
     *         "pattern" => '/{version}/{module}/{action}/{id:[0-9]+}/{secondid}.{output}',
239
     *         "handler" => '\ByJG\RestServer\ServiceHandler'
240
     *    ],
241
     * ]
242
     *
243
     * @param array $moduleAlias
244
     * @param array $routePattern
245
     * @param string $version
246
     * @param string $routeIndex
247
     */
248
    public static function handleRoute($moduleAlias = [], $routePattern = null, $version = '1.0', $defaultOutput = null ,$routeIndex = "index.php")
249
    {
250
        ob_start();
251
        session_start();
252
253
        /**
254
         * @var RouteHandler
255
         */
256
        $route = RouteHandler::getInstance();
257
258
        /**
259
         * Module Alias contains the alias for full namespace class.
260
         *
261
         * For example, instead to request:
262
         * http://somehost/module/Full.NameSpace.To.Module
263
         *
264
         * you can request only:
265
         * http://somehost/module/somealias
266
         */
267
        foreach ((array) $moduleAlias as $alias => $module) {
268
            $route->addModuleAlias($alias, $module);
269
        }
270
271
        /**
272
         * You can create RESTFul compliant URL by adding the version.
273
         *
274
         * In the route pattern:
275
         * /{version}/someurl
276
         *
277
         * Setting the value here XMLNuke route will automatically replace it.
278
         *
279
         * The default value is "1.0"
280
         */
281
        $route->setDefaultRestVersion($version);
282
283
        /**
284
         * You can set the defaultOutput where is not necessary to set the output in the URL
285
         */
286
        $route->setDefaultOutput($defaultOutput);
287
288
        /**
289
         * There are a couple of basic routes pattern for the default parameters
290
         *
291
         * e.g.
292
         *
293
         * /1.0/command/1.json
294
         * /1.0/command/1.xml
295
         *
296
         * You can create your own route pattern by define the methods here
297
         */
298
        if (!empty($routePattern)) {
299
            $route->setDefaultMethods($routePattern);
300
        }
301
302
        // --------------------------------------------------------------------------
303
        // You do not need change from this point
304
        // --------------------------------------------------------------------------
305
306
        if (!empty($_SERVER['SCRIPT_FILENAME'])
307
            && file_exists($_SERVER['SCRIPT_FILENAME'])
308
            && basename($_SERVER['SCRIPT_FILENAME']) !== "route.php"
309
            && basename($_SERVER['SCRIPT_FILENAME']) !== $routeIndex
310
        )  {
311
            $file = $_SERVER['SCRIPT_FILENAME'];
312
            if (strpos($file, '.php') !== false) {
313
                require_once($file);
314
            } else {
315
                header("Content-Type: " . RouteHandler::mimeContentType($file));
316
317
                echo file_get_contents($file);
318
            }
319
            return;
320
        }
321
322
        $route->process();
323
    }
324
325
    /**
326
     * Get the Mime Type based on the filename
327
     *
328
     * @param string $filename
329
     * @return string
330
     */
331
    protected static function mimeContentType($filename)
332
    {
333
334
        $mime_types = array(
335
            'txt' => 'text/plain',
336
            'htm' => 'text/html',
337
            'html' => 'text/html',
338
            'php' => 'text/html',
339
            'css' => 'text/css',
340
            'js' => 'application/javascript',
341
            'json' => 'application/json',
342
            'xml' => 'application/xml',
343
            'swf' => 'application/x-shockwave-flash',
344
            'flv' => 'video/x-flv',
345
            // images
346
            'png' => 'image/png',
347
            'jpe' => 'image/jpeg',
348
            'jpeg' => 'image/jpeg',
349
            'jpg' => 'image/jpeg',
350
            'gif' => 'image/gif',
351
            'bmp' => 'image/bmp',
352
            'ico' => 'image/vnd.microsoft.icon',
353
            'tiff' => 'image/tiff',
354
            'tif' => 'image/tiff',
355
            'svg' => 'image/svg+xml',
356
            'svgz' => 'image/svg+xml',
357
            // archives
358
            'zip' => 'application/zip',
359
            'rar' => 'application/x-rar-compressed',
360
            'exe' => 'application/x-msdownload',
361
            'msi' => 'application/x-msdownload',
362
            'cab' => 'application/vnd.ms-cab-compressed',
363
            // audio/video
364
            'mp3' => 'audio/mpeg',
365
            'qt' => 'video/quicktime',
366
            'mov' => 'video/quicktime',
367
            // adobe
368
            'pdf' => 'application/pdf',
369
            'psd' => 'image/vnd.adobe.photoshop',
370
            'ai' => 'application/postscript',
371
            'eps' => 'application/postscript',
372
            'ps' => 'application/postscript',
373
            // ms office
374
            'doc' => 'application/msword',
375
            'rtf' => 'application/rtf',
376
            'xls' => 'application/vnd.ms-excel',
377
            'ppt' => 'application/vnd.ms-powerpoint',
378
            // open office
379
            'odt' => 'application/vnd.oasis.opendocument.text',
380
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
381
        );
382
383
        $ext = strtolower(array_pop(explode('.', $filename)));
384
        if (array_key_exists($ext, $mime_types)) {
385
            return $mime_types[$ext];
386
        } elseif (function_exists('finfo_open')) {
387
            $finfo = finfo_open(FILEINFO_MIME);
388
            $mimetype = finfo_file($finfo, $filename);
389
            finfo_close($finfo);
390
            return $mimetype;
391
        } else {
392
            return 'application/octet-stream';
393
        }
394
    }
395
}
396