1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Routing;
13
14/**
15 * A Route describes a route and its parameters.
16 *
17 * @author Fabien Potencier <fabien@symfony.com>
18 * @author Tobias Schultze <http://tobion.de>
19 */
20class Route implements \Serializable
21{
22    private $path = '/';
23    private $host = '';
24    private $schemes = [];
25    private $methods = [];
26    private $defaults = [];
27    private $requirements = [];
28    private $options = [];
29    private $condition = '';
30
31    /**
32     * @var CompiledRoute|null
33     */
34    private $compiled;
35
36    /**
37     * Constructor.
38     *
39     * Available options:
40     *
41     *  * compiler_class: A class name able to compile this route instance (RouteCompiler by default)
42     *  * utf8:           Whether UTF-8 matching is enforced ot not
43     *
44     * @param string          $path         The path pattern to match
45     * @param array           $defaults     An array of default parameter values
46     * @param array           $requirements An array of requirements for parameters (regexes)
47     * @param array           $options      An array of options
48     * @param string          $host         The host pattern to match
49     * @param string|string[] $schemes      A required URI scheme or an array of restricted schemes
50     * @param string|string[] $methods      A required HTTP method or an array of restricted methods
51     * @param string          $condition    A condition that should evaluate to true for the route to match
52     */
53    public function __construct($path, array $defaults = [], array $requirements = [], array $options = [], $host = '', $schemes = [], $methods = [], $condition = '')
54    {
55        $this->setPath($path);
56        $this->setDefaults($defaults);
57        $this->setRequirements($requirements);
58        $this->setOptions($options);
59        $this->setHost($host);
60        $this->setSchemes($schemes);
61        $this->setMethods($methods);
62        $this->setCondition($condition);
63    }
64
65    /**
66     * {@inheritdoc}
67     */
68    public function serialize()
69    {
70        return serialize([
71            'path' => $this->path,
72            'host' => $this->host,
73            'defaults' => $this->defaults,
74            'requirements' => $this->requirements,
75            'options' => $this->options,
76            'schemes' => $this->schemes,
77            'methods' => $this->methods,
78            'condition' => $this->condition,
79            'compiled' => $this->compiled,
80        ]);
81    }
82
83    /**
84     * {@inheritdoc}
85     */
86    public function unserialize($serialized)
87    {
88        $data = unserialize($serialized);
89        $this->path = $data['path'];
90        $this->host = $data['host'];
91        $this->defaults = $data['defaults'];
92        $this->requirements = $data['requirements'];
93        $this->options = $data['options'];
94        $this->schemes = $data['schemes'];
95        $this->methods = $data['methods'];
96
97        if (isset($data['condition'])) {
98            $this->condition = $data['condition'];
99        }
100        if (isset($data['compiled'])) {
101            $this->compiled = $data['compiled'];
102        }
103    }
104
105    /**
106     * Returns the pattern for the path.
107     *
108     * @return string The path pattern
109     */
110    public function getPath()
111    {
112        return $this->path;
113    }
114
115    /**
116     * Sets the pattern for the path.
117     *
118     * This method implements a fluent interface.
119     *
120     * @param string $pattern The path pattern
121     *
122     * @return $this
123     */
124    public function setPath($pattern)
125    {
126        // A pattern must start with a slash and must not have multiple slashes at the beginning because the
127        // generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
128        $this->path = '/'.ltrim(trim($pattern), '/');
129        $this->compiled = null;
130
131        return $this;
132    }
133
134    /**
135     * Returns the pattern for the host.
136     *
137     * @return string The host pattern
138     */
139    public function getHost()
140    {
141        return $this->host;
142    }
143
144    /**
145     * Sets the pattern for the host.
146     *
147     * This method implements a fluent interface.
148     *
149     * @param string $pattern The host pattern
150     *
151     * @return $this
152     */
153    public function setHost($pattern)
154    {
155        $this->host = (string) $pattern;
156        $this->compiled = null;
157
158        return $this;
159    }
160
161    /**
162     * Returns the lowercased schemes this route is restricted to.
163     * So an empty array means that any scheme is allowed.
164     *
165     * @return string[] The schemes
166     */
167    public function getSchemes()
168    {
169        return $this->schemes;
170    }
171
172    /**
173     * Sets the schemes (e.g. 'https') this route is restricted to.
174     * So an empty array means that any scheme is allowed.
175     *
176     * This method implements a fluent interface.
177     *
178     * @param string|string[] $schemes The scheme or an array of schemes
179     *
180     * @return $this
181     */
182    public function setSchemes($schemes)
183    {
184        $this->schemes = array_map('strtolower', (array) $schemes);
185        $this->compiled = null;
186
187        return $this;
188    }
189
190    /**
191     * Checks if a scheme requirement has been set.
192     *
193     * @param string $scheme
194     *
195     * @return bool true if the scheme requirement exists, otherwise false
196     */
197    public function hasScheme($scheme)
198    {
199        return \in_array(strtolower($scheme), $this->schemes, true);
200    }
201
202    /**
203     * Returns the uppercased HTTP methods this route is restricted to.
204     * So an empty array means that any method is allowed.
205     *
206     * @return string[] The methods
207     */
208    public function getMethods()
209    {
210        return $this->methods;
211    }
212
213    /**
214     * Sets the HTTP methods (e.g. 'POST') this route is restricted to.
215     * So an empty array means that any method is allowed.
216     *
217     * This method implements a fluent interface.
218     *
219     * @param string|string[] $methods The method or an array of methods
220     *
221     * @return $this
222     */
223    public function setMethods($methods)
224    {
225        $this->methods = array_map('strtoupper', (array) $methods);
226        $this->compiled = null;
227
228        return $this;
229    }
230
231    /**
232     * Returns the options.
233     *
234     * @return array The options
235     */
236    public function getOptions()
237    {
238        return $this->options;
239    }
240
241    /**
242     * Sets the options.
243     *
244     * This method implements a fluent interface.
245     *
246     * @param array $options The options
247     *
248     * @return $this
249     */
250    public function setOptions(array $options)
251    {
252        $this->options = [
253            'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
254        ];
255
256        return $this->addOptions($options);
257    }
258
259    /**
260     * Adds options.
261     *
262     * This method implements a fluent interface.
263     *
264     * @param array $options The options
265     *
266     * @return $this
267     */
268    public function addOptions(array $options)
269    {
270        foreach ($options as $name => $option) {
271            $this->options[$name] = $option;
272        }
273        $this->compiled = null;
274
275        return $this;
276    }
277
278    /**
279     * Sets an option value.
280     *
281     * This method implements a fluent interface.
282     *
283     * @param string $name  An option name
284     * @param mixed  $value The option value
285     *
286     * @return $this
287     */
288    public function setOption($name, $value)
289    {
290        $this->options[$name] = $value;
291        $this->compiled = null;
292
293        return $this;
294    }
295
296    /**
297     * Get an option value.
298     *
299     * @param string $name An option name
300     *
301     * @return mixed The option value or null when not given
302     */
303    public function getOption($name)
304    {
305        return isset($this->options[$name]) ? $this->options[$name] : null;
306    }
307
308    /**
309     * Checks if an option has been set.
310     *
311     * @param string $name An option name
312     *
313     * @return bool true if the option is set, false otherwise
314     */
315    public function hasOption($name)
316    {
317        return \array_key_exists($name, $this->options);
318    }
319
320    /**
321     * Returns the defaults.
322     *
323     * @return array The defaults
324     */
325    public function getDefaults()
326    {
327        return $this->defaults;
328    }
329
330    /**
331     * Sets the defaults.
332     *
333     * This method implements a fluent interface.
334     *
335     * @param array $defaults The defaults
336     *
337     * @return $this
338     */
339    public function setDefaults(array $defaults)
340    {
341        $this->defaults = [];
342
343        return $this->addDefaults($defaults);
344    }
345
346    /**
347     * Adds defaults.
348     *
349     * This method implements a fluent interface.
350     *
351     * @param array $defaults The defaults
352     *
353     * @return $this
354     */
355    public function addDefaults(array $defaults)
356    {
357        foreach ($defaults as $name => $default) {
358            $this->defaults[$name] = $default;
359        }
360        $this->compiled = null;
361
362        return $this;
363    }
364
365    /**
366     * Gets a default value.
367     *
368     * @param string $name A variable name
369     *
370     * @return mixed The default value or null when not given
371     */
372    public function getDefault($name)
373    {
374        return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
375    }
376
377    /**
378     * Checks if a default value is set for the given variable.
379     *
380     * @param string $name A variable name
381     *
382     * @return bool true if the default value is set, false otherwise
383     */
384    public function hasDefault($name)
385    {
386        return \array_key_exists($name, $this->defaults);
387    }
388
389    /**
390     * Sets a default value.
391     *
392     * @param string $name    A variable name
393     * @param mixed  $default The default value
394     *
395     * @return $this
396     */
397    public function setDefault($name, $default)
398    {
399        $this->defaults[$name] = $default;
400        $this->compiled = null;
401
402        return $this;
403    }
404
405    /**
406     * Returns the requirements.
407     *
408     * @return array The requirements
409     */
410    public function getRequirements()
411    {
412        return $this->requirements;
413    }
414
415    /**
416     * Sets the requirements.
417     *
418     * This method implements a fluent interface.
419     *
420     * @param array $requirements The requirements
421     *
422     * @return $this
423     */
424    public function setRequirements(array $requirements)
425    {
426        $this->requirements = [];
427
428        return $this->addRequirements($requirements);
429    }
430
431    /**
432     * Adds requirements.
433     *
434     * This method implements a fluent interface.
435     *
436     * @param array $requirements The requirements
437     *
438     * @return $this
439     */
440    public function addRequirements(array $requirements)
441    {
442        foreach ($requirements as $key => $regex) {
443            $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
444        }
445        $this->compiled = null;
446
447        return $this;
448    }
449
450    /**
451     * Returns the requirement for the given key.
452     *
453     * @param string $key The key
454     *
455     * @return string|null The regex or null when not given
456     */
457    public function getRequirement($key)
458    {
459        return isset($this->requirements[$key]) ? $this->requirements[$key] : null;
460    }
461
462    /**
463     * Checks if a requirement is set for the given key.
464     *
465     * @param string $key A variable name
466     *
467     * @return bool true if a requirement is specified, false otherwise
468     */
469    public function hasRequirement($key)
470    {
471        return \array_key_exists($key, $this->requirements);
472    }
473
474    /**
475     * Sets a requirement for the given key.
476     *
477     * @param string $key   The key
478     * @param string $regex The regex
479     *
480     * @return $this
481     */
482    public function setRequirement($key, $regex)
483    {
484        $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
485        $this->compiled = null;
486
487        return $this;
488    }
489
490    /**
491     * Returns the condition.
492     *
493     * @return string The condition
494     */
495    public function getCondition()
496    {
497        return $this->condition;
498    }
499
500    /**
501     * Sets the condition.
502     *
503     * This method implements a fluent interface.
504     *
505     * @param string $condition The condition
506     *
507     * @return $this
508     */
509    public function setCondition($condition)
510    {
511        $this->condition = (string) $condition;
512        $this->compiled = null;
513
514        return $this;
515    }
516
517    /**
518     * Compiles the route.
519     *
520     * @return CompiledRoute A CompiledRoute instance
521     *
522     * @throws \LogicException If the Route cannot be compiled because the
523     *                         path or host pattern is invalid
524     *
525     * @see RouteCompiler which is responsible for the compilation process
526     */
527    public function compile()
528    {
529        if (null !== $this->compiled) {
530            return $this->compiled;
531        }
532
533        $class = $this->getOption('compiler_class');
534
535        return $this->compiled = $class::compile($this);
536    }
537
538    private function sanitizeRequirement($key, $regex)
539    {
540        if (!\is_string($regex)) {
541            throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" must be a string.', $key));
542        }
543
544        if ('' !== $regex && '^' === $regex[0]) {
545            $regex = (string) substr($regex, 1); // returns false for a single character
546        }
547
548        if ('$' === substr($regex, -1)) {
549            $regex = substr($regex, 0, -1);
550        }
551
552        if ('' === $regex) {
553            throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key));
554        }
555
556        return $regex;
557    }
558}
559