Pattern::match()   B
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 1
Metric Value
cc 3
eloc 9
c 4
b 1
f 1
nc 3
nop 2
dl 0
loc 25
rs 8.8571
1
<?php
2
3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ICanBoogie\Routing;
13
14
use ICanBoogie\Accessor\AccessorTrait;
15
16
/**
17
 * Representation of a route pattern.
18
 *
19
 * <pre>
20
 * <?php
21
 *
22
 * use ICanBoogie\Routing\Pattern;
23
 *
24
 * $pattern = Pattern::from("/blog/<year:\d{4}>-<month:\d{2}>-:slug.html");
25
 * echo $pattern;       // "/blog/<year:\d{4}>-<month:\d{2}>-:slug.html"
26
 *
27
 * $pathname = $pattern->format([ 'year' => "2013", 'month' => "07", 'slug' => "test-is-a-test" ]);
28
 * echo $pathname;      // "/blog/2013-07-this-is-a-test.html"
29
 *
30
 * $matching = $pattern->match($pathname, $captured);
31
 *
32
 * var_dump($matching); // true
33
 * var_dump($captured); // [ 'year' => "2013", 'month' => "07", 'slug' => "test-is-a-test" ]
34
 * </pre>
35
 *
36
 * @property-read string $pattern The pattern.
37
 * @property-read array $interleaved The interleaved parts of the pattern.
38
 * @property-read array $params The names of the pattern params.
39
 * @property-read string $regex The regex of the pattern.
40
 */
41
class Pattern
42
{
43
	use AccessorTrait;
44
45
	static protected $extended_character_classes = [
46
47
		'{:uuid:}' => '[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}',
48
		'{:sha1:}' => '[a-f0-9]{40}'
49
50
	];
51
52
	/**
53
	 * Parses a route pattern and returns an array of interleaved paths and parameters, the
54
	 * parameter names and the regular expression for the specified pattern.
55
	 *
56
	 * @param string $pattern A pattern.
57
	 *
58
	 * @return array
59
	 */
60
	static private function parse($pattern)
61
	{
62
		$catchall = false;
63
64
		if ($pattern{strlen($pattern) - 1} == '*')
65
		{
66
			$catchall = true;
67
			$pattern = substr($pattern, 0, -1);
68
		}
69
70
		$pattern = strtr($pattern, self::$extended_character_classes);
71
		$parts = preg_split('#(:\w+|<(\w+:)?([^>]+)>)#', $pattern, -1, PREG_SPLIT_DELIM_CAPTURE);
72
		list($interleaved, $params, $regex) = self::parse_parts($parts);
73
74
		if ($catchall)
75
		{
76
			$regex .= '(.*)';
77
			$params[] = 'all';
78
		}
79
80
		$regex .= '$#';
81
82
		return [ $interleaved, $params, $regex ];
83
	}
84
85
	/**
86
	 * Parses pattern parts.
87
	 *
88
	 * @param array $parts
89
	 *
90
	 * @return array
91
	 */
92
	static private function parse_parts(array $parts)
93
	{
94
		$regex = '#^';
95
		$interleaved = [];
96
		$params = [];
97
		$n = 0;
98
99
		for ($i = 0, $j = count($parts); $i < $j ;)
100
		{
101
			$part = $parts[$i++];
102
103
			$regex .= preg_quote($part, '#');
104
			$interleaved[] = $part;
105
106
			if ($i == $j)
107
			{
108
				break;
109
			}
110
111
			$part = $parts[$i++];
112
113
			if ($part{0} == ':')
114
			{
115
				$identifier = substr($part, 1);
116
				$separator = $parts[$i];
117
				$selector = $separator ? '[^/\\' . $separator{0} . ']+' : '[^/]+';
118
			}
119
			else
120
			{
121
				$identifier = substr($parts[$i++], 0, -1);
122
123
				if (!$identifier)
124
				{
125
					$identifier = $n++;
126
				}
127
128
				$selector = $parts[$i++];
129
			}
130
131
			$regex .= '(' . $selector . ')';
132
			$interleaved[] = [ $identifier, $selector ];
133
			$params[] = $identifier;
134
		}
135
136
		return [ $interleaved, $params, $regex ];
137
	}
138
139
	/**
140
	 * Reads an offset from an array.
141
	 *
142
	 * @param array $container
143
	 * @param string $key
144
	 *
145
	 * @return mixed
146
	 */
147
	static protected function read_value_from_array(array $container, $key)
148
	{
149
		return $container[$key];
150
	}
151
152
	/**
153
	 * Reads a property from an object.
154
	 *
155
	 * @param object $container
156
	 * @param string $key
157
	 *
158
	 * @return mixed
159
	 */
160
	static protected function read_value_from_object($container, $key)
161
	{
162
		return $container->$key;
163
	}
164
165
	/**
166
	 * Checks if the given string is a route pattern.
167
	 *
168
	 * @param string $pattern
169
	 *
170
	 * @return bool `true` if the given pattern is a route pattern, `false` otherwise.
171
	 */
172
	static public function is_pattern($pattern)
173
	{
174
		return (strpos($pattern, '<') !== false) || (strpos($pattern, ':') !== false) || (strpos($pattern, '*') !== false);
175
	}
176
177
	static private $instances;
178
179
	/**
180
	 * Creates a {@link Pattern} instance from the specified pattern.
181
	 *
182
	 * @param mixed $pattern
183
	 *
184
	 * @return Pattern
185
	 */
186
	static public function from($pattern)
187
	{
188
		if ($pattern instanceof static)
189
		{
190
			return $pattern;
191
		}
192
193
		if (isset(self::$instances[$pattern]))
194
		{
195
			return self::$instances[$pattern];
196
		}
197
198
		return self::$instances[$pattern] = new static($pattern);
199
	}
200
201
	/**
202
	 * Pattern.
203
	 *
204
	 * @var string
205
	 */
206
	private $pattern;
207
208
	protected function get_pattern()
209
	{
210
		return $this->pattern;
211
	}
212
213
	/**
214
	 * Interleaved pattern.
215
	 *
216
	 * @var array
217
	 */
218
	private $interleaved;
219
220
	protected function get_interleaved()
221
	{
222
		return $this->interleaved;
223
	}
224
225
	/**
226
	 * Params of the pattern.
227
	 *
228
	 * @var array
229
	 */
230
	private $params;
231
232
	protected function get_params()
233
	{
234
		return $this->params;
235
	}
236
237
	/**
238
	 * Regex of the pattern.
239
	 *
240
	 * @var string
241
	 */
242
	private $regex;
243
244
	protected function get_regex()
245
	{
246
		return $this->regex;
247
	}
248
249
	/**
250
	 * Initializes the {@link $pattern}, {@link $interleaved}, {@link $params} and {@link $regex}
251
	 * properties.
252
	 *
253
	 * @param string $pattern A route pattern.
254
	 */
255
	protected function __construct($pattern)
256
	{
257
		list($interleaved, $params, $regex) = self::parse($pattern);
258
259
		$this->pattern = $pattern;
260
		$this->interleaved = $interleaved;
261
		$this->params = $params;
262
		$this->regex = $regex;
263
	}
264
265
	/**
266
	 * Returns the route pattern specified during construct.
267
	 *
268
	 * @return string
269
	 */
270
	public function __toString()
271
	{
272
		return $this->pattern;
273
	}
274
275
	/**
276
	 * Formats a pattern with the specified values.
277
	 *
278
	 * @param array|object $values The values to format the pattern, either as an array or an
279
	 * object. If value is an instance of {@link ToSlug} the `to_slug()` method is used to
280
	 * transform the instance into a URL component.
281
	 *
282
	 * @return string
283
	 *
284
	 * @throws PatternRequiresValues in attempt to format a pattern requiring values without
285
	 * providing any.
286
	 */
287
	public function format($values = null)
288
	{
289
		if (!$this->params)
290
		{
291
			return $this->pattern;
292
		}
293
294
		if (!$values)
295
		{
296
			throw new PatternRequiresValues($this);
297
		}
298
299
		return $this->format_parts($values);
300
	}
301
302
	/**
303
	 * Formats pattern parts.
304
	 *
305
	 * @param array|object $container
306
	 *
307
	 * @return string
308
	 */
309
	private function format_parts($container)
310
	{
311
		$url = '';
312
		$method = 'read_value_from_' . (is_array($container) ? 'array' : 'object');
313
314
		foreach ($this->interleaved as $i => $value)
315
		{
316
			$url .= $i % 2 ? $this->format_part(self::$method($container, $value[0])) : $value;
317
		}
318
319
		return $url;
320
	}
321
322
	/**
323
	 * Formats pattern part.
324
	 *
325
	 * @param mixed $value
326
	 *
327
	 * @return string
328
	 */
329
	private function format_part($value)
330
	{
331
		if ($value instanceof ToSlug)
332
		{
333
			$value = $value->to_slug();
334
		}
335
336
		return urlencode($value);
337
	}
338
339
	/**
340
	 * Checks if a pathname matches the pattern.
341
	 *
342
	 * @param string $pathname The pathname.
343
	 * @param array $captured The parameters captured from the pathname.
344
	 *
345
	 * @return bool `true` if the pathname matches the pattern, `false` otherwise.
346
	 */
347
	public function match($pathname, &$captured = null)
348
	{
349
		$captured = [];
350
351
		#
352
		# `params` is empty if the pattern is a plain string,
353
		# thus we can simply compare strings.
354
		#
355
356
		if (!$this->params)
357
		{
358
			return $pathname === $this->pattern;
359
		}
360
361
		if (!preg_match($this->regex, $pathname, $matches))
362
		{
363
			return false;
364
		}
365
366
		array_shift($matches);
367
368
		$captured = array_combine($this->params, $matches);
369
370
		return true;
371
	}
372
}
373