Completed
Pull Request — master (#2)
by Joao
02:23
created

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