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); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
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 returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.