Completed
Push — master ( c6d651...f3cc34 )
by Pavel
02:14
created

ApiRoute::getFormatFull()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
368
369
		return $request;
370
	}
371
372
373
	/**
374
	 * Constructs absolute URL from Request object.
375
	 * @return string|NULL
376
	 */
377
	public function constructUrl(Request $request, Nette\Http\Url $url)
378
	{
379
		if ($this->presenter != $request->getPresenterName()) {
380
			return NULL;
381
		}
382
383
		$base_url = $url->getBaseUrl();
384
385
		$action = $request->getParameter('action');
386
		$parameters = $request->getParameters();
387
		unset($parameters['action']);
388
		$path = ltrim($this->getPath(), '/');
389
390
		if (FALSE === array_search($action, $this->actions)) {
391
			return NULL;
392
		}
393
394
		foreach ($parameters as $name => $value) {
395
			if (strpos($path, "<{$name}>") !== FALSE && $value !== NULL) {
396
				$path = str_replace("<{$name}>", $value, $path);
397
398
				unset($parameters[$name]);
399
			}
400
		}
401
402
		$path = preg_replace_callback('/\[.+?\]/', function($item) {
403
			if (strpos(end($item), '<')) {
404
				return '';
405
			}
406
407
			return end($item);
408
		}, $path);
409
410
		/**
411
		 * There are still some required parameters in url mask
412
		 */
413
		if (preg_match('/<\w+>/', $path)) {
414
			return NULL;
415
		}
416
417
		$path = str_replace(['[', ']'], '', $path);
418
419
		$query = http_build_query($parameters);
420
421
		return $base_url . $path . ($query ? '?' . $query : '');
422
	}
423
424
}
425