Completed
Push — master ( ca7d61...d91a9a )
by Pavel
05:38
created

ApiRoute::getFormatFull()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright   Copyright (c) 2016 ublaboo <[email protected]>
7
 * @author      Pavel Janda <[email protected]>
8
 * @package     Ublaboo
9
 */
10
11
namespace Ublaboo\ApiRouter;
12
13
use Nette;
14
use Nette\Application\IRouter;
15
use Nette\Application\Request;
16
use Nette\Utils\Strings;
17
18
/**
19
 * @method mixed onMatch(static, Nette\Application\Request $request)
20
 * 
21
 * @Annotation
22
 * @Target({"CLASS", "METHOD"})
23
 */
24
class ApiRoute extends ApiRouteSpec implements IRouter
25
{
26
27
	/**
28
	 * @var callable[]
29
	 */
30
	public $onMatch;
31
32
	/**
33
	 * @var string
34
	 */
35
	private $presenter;
36
37
	/**
38
	 * @var array
39
	 */
40
	private $actions = [
41
		'POST' => false,
42
		'GET' => false,
43
		'PUT' => false,
44
		'DELETE' => false,
45
		'OPTIONS' => false,
46
		'PATCH' => false,
47
	];
48
49
	/**
50
	 * @var array
51
	 */
52
	private $default_actions = [
53
		'POST' => 'create',
54
		'GET' => 'read',
55
		'PUT' => 'update',
56
		'DELETE' => 'delete',
57
		'OPTIONS' => 'options',
58
		'PATCH' => 'patch',
59
	];
60
61
	/**
62
	 * @var array
63
	 */
64
	private $formats = [
65
		'json' => 'application/json',
66
		'xml' => 'application/xml',
67
	];
68
69
	/**
70
	 * @var array
71
	 */
72
	private $placeholder_order = [];
73
74
75
	/**
76
	 * @param mixed  $data
77
	 * @param string $presenter
78
	 * @param array  $data
79
	 */
80
	public function __construct($path, $presenter = null, array $data = [])
81
	{
82
		/**
83
		 * Interface for setting route via annotation or directly
84
		 */
85
		if (!is_array($path)) {
86
			$data['value'] = $path;
87
			$data['presenter'] = $presenter;
88
89
			if (empty($data['methods'])) {
90
				$this->actions = $this->default_actions;
91
			} else {
92
				foreach ($data['methods'] as $method => $action) {
93
					if (is_string($method)) {
94
						$this->setAction($action, $method);
95
					} else {
96
						$m = $action;
97
98
						if (isset($this->default_actions[$m])) {
99
							$this->setAction($this->default_actions[$m], $m);
100
						}
101
					}
102
				}
103
104
				unset($data['methods']);
105
			}
106
		} else {
107
			$data = $path;
108
		}
109
110
		/**
111
		 * Set Path
112
		 */
113
		$this->setPath($data['value']);
114
		unset($data['value']);
115
116
		parent::__construct($data);
117
	}
118
119
120
	/**
121
	 * @param string $presenter
122
	 * @return void
123
	 */
124
	public function setPresenter($presenter)
125
	{
126
		$this->presenter = $presenter;
127
	}
128
129
130
	/**
131
	 * @return string
132
	 */
133
	public function getPresenter()
134
	{
135
		return $this->presenter;
136
	}
137
138
139
	/**
140
	 * @param string $action
141
	 * @param string $method
142
	 * @return void
143
	 */
144
	public function setAction($action, $method = null)
145
	{
146
		if ($method === null) {
147
			$method = array_search($action, $this->default_actions, true);
148
		}
149
150
		if (!isset($this->default_actions[$method])) {
151
			return;
152
		}
153
154
		$this->actions[$method] = $action;
155
	}
156
157
158
	/**
159
	 * @param  string $string
160
	 * @return string
161
	 */
162
	private function prepareForMatch($string)
163
	{
164
		return sprintf('/%s/', str_replace('/', '\/', $string));
165
	}
166
167
168
	/**
169
	 * Get all parameters from url mask
170
	 * @return array
171
	 */
172
	public function getPlacehodlerParameters()
173
	{
174
		if (!empty($this->placeholder_order)) {
175
			return array_filter($this->placeholder_order);
176
		}
177
178
		$return = [];
179
180
		preg_replace_callback('/<(\w+)>/', function ($item) use (&$return) {
181
			$return[] = end($item);
182
		}, $this->path);
183
184
		return $return;
185
	}
186
187
188
	/**
189
	 * Get required parameters from url mask
190
	 * @return array
191
	 */
192
	public function getRequiredParams()
193
	{
194
		$regex = '/\[[^\[]+?\]/';
195
		$path = $this->getPath();
196
197
		while (preg_match($regex, $path)) {
198
			$path = preg_replace($regex, '', $path);
199
		}
200
201
		$required = [];
202
203
		preg_replace_callback('/<(\w+)>/', function ($item) use (&$required) {
204
			$required[] = end($item);
205
		}, $path);
206
207
		return $required;
208
	}
209
210
211
	/**
212
	 * @param  Nette\Http\IRequest $httpRequest
213
	 * @return void
214
	 */
215
	public function resolveFormat(Nette\Http\IRequest $httpRequest)
216
	{
217
		if ($this->getFormat()) {
218
			return;
219
		}
220
221
		$header = $httpRequest->getHeader('Accept');
222
223
		foreach ($this->formats as $format => $format_full) {
224
			$format_full = Strings::replace($format_full, '/\//', '\/');
225
226
			if (Strings::match($header, "/{$format_full}/")) {
227
				$this->setFormat($format);
228
			}
229
		}
230
231
		$this->setFormat('json');
232
	}
233
234
235
	/**
236
	 * @return string
237
	 */
238
	public function getFormatFull()
239
	{
240
		return $this->formats[$this->getFormat()];
241
	}
242
243
244
	/**
245
	 * @param array $methods
246
	 */
247
	public function setMethods(array $methods)
248
	{
249
		foreach ($methods as $method => $action) {
250
			if (is_string($method)) {
251
				$this->setAction($action, $method);
252
			} else {
253
				$m = $action;
254
255
				if (isset($this->default_actions[$m])) {
256
					$this->setAction($this->default_actions[$m], $m);
257
				}
258
			}
259
		}
260
	}
261
262
263
	/**
264
	 * @return array
265
	 */
266
	public function getMethods()
267
	{
268
		return array_keys(array_filter($this->actions));
269
	}
270
271
272
	/**
273
	 * @param  Nette\Http\IRequest $request
274
	 * @return string
275
	 */
276
	public function resolveMethod(Nette\Http\IRequest $request)
277
	{
278
		if (!empty($request->getHeader('X-HTTP-Method-Override'))) {
279
			return Strings::upper($request->getHeader('X-HTTP-Method-Override'));
280
		}
281
282
		if ($method = Strings::upper($request->getQuery('__apiRouteMethod'))) {
283
			if (isset($this->actions[$method])) {
284
				return $method;
285
			}
286
		}
287
288
		return Strings::upper($request->getMethod());
289
	}
290
291
292
	/********************************************************************************
293
	 *                              Interface IRouter                               *
294
	 ********************************************************************************/
295
296
297
	/**
298
	 * Maps HTTP request to a Request object.
299
	 * @return Request|NULL
300
	 */
301
	public function match(Nette\Http\IRequest $httpRequest)
302
	{
303
		/**
304
		 * ApiRoute can be easily disabled
305
		 */
306
		if ($this->disable) {
307
			return null;
308
		}
309
310
		$url = $httpRequest->getUrl();
311
312
		$path = $url->getPath();
313
314
		/**
315
		 * Build path mask
316
		 */
317
		$order = &$this->placeholder_order;
318
		$parameters = $this->parameters;
319
320
		$mask = preg_replace_callback('/(<(\w+)>)|\[|\]/', function ($item) use (&$order, $parameters) {
321
			if ($item[0] == '[' || $item[0] == ']') {
322
				if ($item[0] == '[') {
323
					$order[] = null;
324
				}
325
326
				return $item[0];
327
			}
328
329
			[, , $placeholder] = $item;
0 ignored issues
show
Bug introduced by
The variable $placeholder does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
330
331
			$parameter = $parameters[$placeholder] ?? [];
332
333
			$regex = $parameter['requirement'] ?? '\w+';
334
			$has_default = array_key_exists('default', $parameter);
335
			$regex = preg_replace('~\(~', '(?:', $regex);
336
337
			if ($has_default) {
338
				$order[] = $placeholder;
339
340
				return sprintf('(%s)?', $regex);
341
			}
342
343
			$order[] = $placeholder;
344
345
			return sprintf('(%s)', $regex);
346
		}, $this->path);
347
348
		$mask = '^' . str_replace(['[', ']'], ['(', ')?'], $mask) . '$';
349
350
		/**
351
		 * Prepare paths for regex match (escape slashes)
352
		 */
353
		if (!preg_match_all($this->prepareForMatch($mask), $path, $matches)) {
354
			return null;
355
		}
356
357
		/**
358
		 * Did some action to the request method exists?
359
		 */
360
		$this->resolveFormat($httpRequest);
361
		$method = $this->resolveMethod($httpRequest);
362
		$action = $this->actions[$method];
363
364
		if (!$action) {
365
			return null;
366
		}
367
368
		/**
369
		 * Basic params
370
		 */
371
		$params = $httpRequest->getQuery();
372
		$params['action'] = $action;
373
		$required_params = $this->getRequiredParams();
374
375
		/**
376
		 * Route mask parameters
377
		 */
378
		array_shift($matches);
379
380
		foreach ($this->placeholder_order as $key => $name) {
381
			if ($name !== null&& isset($matches[$key])) {
382
				$params[$name] = reset($matches[$key]) ?: null;
383
384
				/**
385
				 * Required parameters
386
				 */
387
				if (empty($params[$name]) && in_array($name, $required_params, true)) {
388
					return null;
389
				}
390
			}
391
		}
392
393
		$request = new Request(
394
			$this->presenter,
395
			$method,
396
			$params,
397
			$httpRequest->getPost(),
398
			$httpRequest->getFiles(),
399
			[Request::SECURED => $httpRequest->isSecured()]
400
		);
401
402
		/**
403
		 * Trigger event - route matches
404
		 */
405
		$this->onMatch($this, $request);
406
407
		return $request;
408
	}
409
410
411
	/**
412
	 * Constructs absolute URL from Request object.
413
	 * @return string|NULL
414
	 */
415
	public function constructUrl(Request $request, Nette\Http\Url $url)
416
	{
417
		if ($this->presenter != $request->getPresenterName()) {
418
			return null;
419
		}
420
421
		$base_url = $url->getBaseUrl();
422
423
		$action = $request->getParameter('action');
424
		$parameters = $request->getParameters();
425
		unset($parameters['action']);
426
		$path = ltrim($this->getPath(), '/');
427
428
		if (array_search($action, $this->actions, true) === false) {
429
			return null;
430
		}
431
432
		foreach ($parameters as $name => $value) {
433
			if (strpos($path, "<{$name}>") !== false && $value !== null) {
434
				$path = str_replace("<{$name}>", $value, $path);
435
436
				unset($parameters[$name]);
437
			}
438
		}
439
440
		$path = preg_replace_callback('/\[.+?\]/', function ($item) {
441
			if (strpos(end($item), '<')) {
442
				return '';
443
			}
444
445
			return end($item);
446
		}, $path);
447
448
		/**
449
		 * There are still some required parameters in url mask
450
		 */
451
		if (preg_match('/<\w+>/', $path)) {
452
			return null;
453
		}
454
455
		$path = str_replace(['[', ']'], '', $path);
456
457
		$query = http_build_query($parameters);
458
459
		return $base_url . $path . ($query ? '?' . $query : '');
460
	}
461
}
462