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

ServerRequestHandler::handle()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 36
rs 8.439
cc 5
eloc 18
nc 3
nop 2
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 ByJG\RestServer\HandleOutput\HandleOutputInterface;
13
use FastRoute\Dispatcher;
14
use FastRoute\RouteCollector;
15
use InvalidArgumentException;
16
17
class ServerRequestHandler
18
{
19
20
    use Singleton;
21
22
    const OK = "OK";
23
    const METHOD_NOT_ALLOWED = "NOT_ALLOWED";
24
    const NOT_FOUND = "NOT FOUND";
25
26
    protected $routes = null;
27
    protected $_moduleAlias = [];
28
29
    public function getRoutes()
30
    {
31
        if (is_null($this->routes)) {
32
            $this->routes = [
33
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}/{action}/{id:[0-9]+}/{secondid}'),
34
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}/{action}/{id:[0-9]+}'),
35
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}/{id:[0-9]+}/{action}'),
36
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}/{id:[0-9]+}'),
37
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}/{action}'),
38
                new RoutePattern(['GET', 'POST', 'PUT', 'DELETE'], '/{module}')
39
            ];
40
        }
41
        return $this->routes;
42
    }
43
44
    public function addRoute(RoutePattern $route)
45
    {
46
        if (is_null($this->routes)) {
47
            $this->routes = [];
48
        }
49
        $this->routes[] = $route;
50
    }
51
52
    /**
53
     * There are a couple of basic routes pattern for the default parameters
54
     *
55
     * e.g.
56
     *   /1.0/command/1.json
57
     *   /1.0/command/1.xml
58
     *
59
     * You can create your own route pattern by define the methods here
60
     *
61
     * @param $methods
62
     */
63
    public function setRoutes($methods)
64
    {
65
        if ($methods == null) {
66
            $this->routes = null;
67
            return;
68
        }
69
70
        if (!is_array($methods)) {
71
            throw new InvalidArgumentException('You need pass an array');
72
        }
73
74
        foreach ($methods as $value) {
75
            $routeHandler = $value;
76
            if (is_array($routeHandler)) {
77
                if (!isset($value['method']) || !isset($value['pattern'])) {
78
                    throw new InvalidArgumentException('Array have to be the format ["method"=>"", "pattern"=>""]');
79
                }
80
                $routeHandler = new RoutePattern($value['method'], $value['pattern']);
81
            }
82
            $this->addRoute($routeHandler);
83
        }
84
    }
85
86
    public function getModuleAlias()
87
    {
88
        return $this->_moduleAlias;
89
    }
90
91
    /**
92
     * Module Alias contains the alias for full namespace class.
93
     *
94
     * For example, instead to request:
95
     * http://somehost/module/Full.NameSpace.To.Module
96
     *
97
     * you can request only:
98
     * http://somehost/module/somealias
99
     *
100
     * @param $moduleAlias
101
     */
102
    public function setModuleAlias($moduleAlias)
103
    {
104
        foreach ((array)$moduleAlias as $alias => $module) {
105
            $this->addModuleAlias($alias, $module);
106
        }
107
    }
108
109
    public function addModuleAlias($alias, $module)
110
    {
111
        $this->_moduleAlias[$alias] = $module;
112
    }
113
114
    public 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...
Coding Style introduced by
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
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...
115
    {
116
        // Initialize ErrorHandler with default error handler
117
        ErrorHandler::getInstance()->register();
118
119
        // Get the URL parameters
120
        $httpMethod = $_SERVER['REQUEST_METHOD'];
121
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
122
        parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $queryStr);
123
124
        // Generic Dispatcher for RestServer
125
        $dispatcher = \FastRoute\simpleDispatcher(function (RouteCollector $r) {
126
127
            foreach ($this->getRoutes() as $route) {
128
                $r->addRoute(
129
                    $route->getMethod(),
130
                    $route->getPattern(),
131
                    $route->getHandler()
132
                );
133
            }
134
        });
135
136
        $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 121 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...
137
138
        switch ($routeInfo[0]) {
139
            case Dispatcher::NOT_FOUND:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
140
141
                throw new Error404Exception('404 Not found');
142
143
            case Dispatcher::METHOD_NOT_ALLOWED:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
144
145
                throw new Error405Exception('405 Method Not Allowed');
146
147
            case Dispatcher::FOUND:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
148
149
                // ... 200 Process:
150
                $vars = array_merge($routeInfo[2], $queryStr);
151
152
                // Instantiate the Service Handler
153
                $handlerInstance = $this->getHandler($routeInfo[1]);
154
                $handlerInstance->writeHeader();
155
                ErrorHandler::getInstance()->setHandler($handlerInstance->getErrorHandler());
156
157
                // Check Alias
158
                $moduleAlias = $this->getModuleAlias();
159
                $vars['_class'] = $vars['module'];
160
                if (isset($moduleAlias[$vars['module']])) {
161
                    $vars['_class'] = $moduleAlias[$vars['module']];
162
                }
163
                $vars['_class'] = '\\' . str_replace('.', '\\', $vars['_class']);
164
165
                // Set all default values
166
                foreach ($vars as $key => $value) {
167
                    $_REQUEST[$key] = $_GET[$key] = $vars[$key];
168
                }
169
170
                $instance = $this->executeServiceMethod($vars['_class']);
171
172
                echo $handlerInstance->writeOutput($instance);
173
                break;
174
175
            default:
176
                throw new Error520Exception('Unknown');
177
        }
178
    }
179
180
    /**
181
     * Get the Handler based on the string
182
     *
183
     * @param string $handlerStr
184
     * @throws ClassNotFoundException
185
     * @throws InvalidClassException
186
     * @return HandleOutputInterface Return the Handler Interface
187
     */
188
    public function getHandler($handlerStr)
189
    {
190
        if (!class_exists($handlerStr)) {
191
            throw new ClassNotFoundException("Handler $handlerStr not found");
192
        }
193
        $handlerInstance = new $handlerStr();
194
        if (!($handlerInstance instanceof HandleOutputInterface)) {
195
            throw new InvalidClassException("Handler $handlerStr is not a HandleOutputInterface");
196
        }
197
198
        return $handlerInstance;
199
    }
200
201
    /**
202
     * Instantiate the class found in the route
203
     *
204
     * @param string $class
205
     * @return ServiceAbstract
206
     * @throws ClassNotFoundException
207
     * @throws InvalidClassException
208
     * @throws BadActionException
209
     */
210
    public function executeServiceMethod($class)
211
    {
212
        // Instantiate a new class
213
        if (!class_exists($class)) {
214
            throw new ClassNotFoundException("Class $class not found");
215
        }
216
        $instance = new $class();
217
218
        if (!($instance instanceof ServiceAbstract)) {
219
            throw new InvalidClassException("Class $class is not an instance of ServiceAbstract");
220
        }
221
222
        // Execute the method
223
        $method = strtolower($instance->getRequest()->server("REQUEST_METHOD")); // get, post, put, delete
224
        $customAction = $method . ($instance->getRequest()->get('action'));
225
        if (method_exists($instance, $customAction)) {
226
            $instance->$customAction();
227
        } else {
228
            throw new BadActionException("The action '$customAction' does not exists.");
229
        }
230
231
        return $instance;
232
    }
233
234
    /**
235
     * Process the ROUTE (see web/app-dist.php)
236
     *
237
     * ModuleAlias needs to be an array like:
238
     *  [ 'alias' => 'Full.Namespace.To.Class' ]
239
     *
240
     * RoutePattern needs to be an array like:
241
     * [
242
     *     [
243
     *         "method" => ['GET'],
244
     *         "pattern" => '/{module}/{action}/{id:[0-9]+}/{secondid}',
245
     *         "handler" => '\ByJG\RestServer\HandleOutput\HandleOutputInterface'
246
     *    ],
247
     * ]
248
     *
249
     * @param array $moduleAlias
250
     * @param array $routePattern
251
     */
252
    public static function handle($moduleAlias = [], $routePattern = null)
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...
253
    {
254
        ob_start();
255
        session_start();
256
257
        /**
258
         * @var ServerRequestHandler
259
         */
260
        $route = ServerRequestHandler::getInstance();
261
262
        $route->setModuleAlias($moduleAlias);
263
264
        $route->setRoutes($routePattern);
265
266
        // --------------------------------------------------------------------------
267
        // Check if script exists or if is itself
268
        // --------------------------------------------------------------------------
269
270
        $debugBacktrace =  debug_backtrace();
271
        if (!empty($_SERVER['SCRIPT_FILENAME'])
272
            && file_exists($_SERVER['SCRIPT_FILENAME'])
273
            && str_replace('//', '/', $_SERVER['SCRIPT_FILENAME']) !== $debugBacktrace[0]['file']
274
        ) {
275
            $file = $_SERVER['SCRIPT_FILENAME'];
276
            if (strpos($file, '.php') !== false) {
277
                require_once($file);
278
            } else {
279
                header("Content-Type: " . ServerRequestHandler::mimeContentType($file));
280
281
                echo file_get_contents($file);
282
            }
283
            return;
284
        }
285
286
        $route->process();
287
    }
288
289
    /**
290
     * Get the Mime Type based on the filename
291
     *
292
     * @param string $filename
293
     * @return string
294
     */
295
    protected static function mimeContentType($filename)
296
    {
297
298
        $mime_types = array(
299
            'txt' => 'text/plain',
300
            'htm' => 'text/html',
301
            'html' => 'text/html',
302
            'php' => 'text/html',
303
            'css' => 'text/css',
304
            'js' => 'application/javascript',
305
            'json' => 'application/json',
306
            'xml' => 'application/xml',
307
            'swf' => 'application/x-shockwave-flash',
308
            'flv' => 'video/x-flv',
309
            // images
310
            'png' => 'image/png',
311
            'jpe' => 'image/jpeg',
312
            'jpeg' => 'image/jpeg',
313
            'jpg' => 'image/jpeg',
314
            'gif' => 'image/gif',
315
            'bmp' => 'image/bmp',
316
            'ico' => 'image/vnd.microsoft.icon',
317
            'tiff' => 'image/tiff',
318
            'tif' => 'image/tiff',
319
            'svg' => 'image/svg+xml',
320
            'svgz' => 'image/svg+xml',
321
            // archives
322
            'zip' => 'application/zip',
323
            'rar' => 'application/x-rar-compressed',
324
            'exe' => 'application/x-msdownload',
325
            'msi' => 'application/x-msdownload',
326
            'cab' => 'application/vnd.ms-cab-compressed',
327
            // audio/video
328
            'mp3' => 'audio/mpeg',
329
            'qt' => 'video/quicktime',
330
            'mov' => 'video/quicktime',
331
            // adobe
332
            'pdf' => 'application/pdf',
333
            'psd' => 'image/vnd.adobe.photoshop',
334
            'ai' => 'application/postscript',
335
            'eps' => 'application/postscript',
336
            'ps' => 'application/postscript',
337
            // ms office
338
            'doc' => 'application/msword',
339
            'rtf' => 'application/rtf',
340
            'xls' => 'application/vnd.ms-excel',
341
            'ppt' => 'application/vnd.ms-powerpoint',
342
            // open office
343
            'odt' => 'application/vnd.oasis.opendocument.text',
344
            'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
345
        );
346
347
        $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...
348
        if (array_key_exists($ext, $mime_types)) {
349
            return $mime_types[$ext];
350
        } elseif (function_exists('finfo_open')) {
351
            $finfo = finfo_open(FILEINFO_MIME);
352
            $mimetype = finfo_file($finfo, $filename);
353
            finfo_close($finfo);
354
            return $mimetype;
355
        } else {
356
            return 'application/octet-stream';
357
        }
358
    }
359
}
360