Completed
Push — master ( 066784...351105 )
by Joao Gilberto
02:29
created

RouteHandler::executeAction()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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