# WebFinger
.well-known
/.buildpath
-3rdparty/autoload.php
-3rdparty/composer/
-3rdparty/symfony/
-composer.lock
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Annotation;
+
+/**
+ * Annotation class for @Route().
+ *
+ * @Annotation
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Route
+{
+ private $pattern;
+ private $name;
+ private $requirements;
+ private $options;
+ private $defaults;
+
+ /**
+ * Constructor.
+ *
+ * @param array $data An array of key/value parameters.
+ */
+ public function __construct(array $data)
+ {
+ $this->requirements = array();
+ $this->options = array();
+ $this->defaults = array();
+
+ if (isset($data['value'])) {
+ $data['pattern'] = $data['value'];
+ unset($data['value']);
+ }
+
+ foreach ($data as $key => $value) {
+ $method = 'set'.$key;
+ if (!method_exists($this, $method)) {
+ throw new \BadMethodCallException(sprintf("Unknown property '%s' on annotation '%s'.", $key, get_class($this)));
+ }
+ $this->$method($value);
+ }
+ }
+
+ public function setPattern($pattern)
+ {
+ $this->pattern = $pattern;
+ }
+
+ public function getPattern()
+ {
+ return $this->pattern;
+ }
+
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setRequirements($requirements)
+ {
+ $this->requirements = $requirements;
+ }
+
+ public function getRequirements()
+ {
+ return $this->requirements;
+ }
+
+ public function setOptions($options)
+ {
+ $this->options = $options;
+ }
+
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ public function setDefaults($defaults)
+ {
+ $this->defaults = $defaults;
+ }
+
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * CompiledRoutes are returned by the RouteCompiler class.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class CompiledRoute
+{
+ private $route;
+ private $variables;
+ private $tokens;
+ private $staticPrefix;
+ private $regex;
+
+ /**
+ * Constructor.
+ *
+ * @param Route $route A original Route instance
+ * @param string $staticPrefix The static prefix of the compiled route
+ * @param string $regex The regular expression to use to match this route
+ * @param array $tokens An array of tokens to use to generate URL for this route
+ * @param array $variables An array of variables
+ */
+ public function __construct(Route $route, $staticPrefix, $regex, array $tokens, array $variables)
+ {
+ $this->route = $route;
+ $this->staticPrefix = $staticPrefix;
+ $this->regex = $regex;
+ $this->tokens = $tokens;
+ $this->variables = $variables;
+ }
+
+ /**
+ * Returns the Route instance.
+ *
+ * @return Route A Route instance
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
+
+ /**
+ * Returns the static prefix.
+ *
+ * @return string The static prefix
+ */
+ public function getStaticPrefix()
+ {
+ return $this->staticPrefix;
+ }
+
+ /**
+ * Returns the regex.
+ *
+ * @return string The regex
+ */
+ public function getRegex()
+ {
+ return $this->regex;
+ }
+
+ /**
+ * Returns the tokens.
+ *
+ * @return array The tokens
+ */
+ public function getTokens()
+ {
+ return $this->tokens;
+ }
+
+ /**
+ * Returns the variables.
+ *
+ * @return array The variables
+ */
+ public function getVariables()
+ {
+ return $this->variables;
+ }
+
+ /**
+ * Returns the pattern.
+ *
+ * @return string The pattern
+ */
+ public function getPattern()
+ {
+ return $this->route->getPattern();
+ }
+
+ /**
+ * Returns the options.
+ *
+ * @return array The options
+ */
+ public function getOptions()
+ {
+ return $this->route->getOptions();
+ }
+
+ /**
+ * Returns the defaults.
+ *
+ * @return array The defaults
+ */
+ public function getDefaults()
+ {
+ return $this->route->getDefaults();
+ }
+
+ /**
+ * Returns the requirements.
+ *
+ * @return array The requirements
+ */
+ public function getRequirements()
+ {
+ return $this->route->getRequirements();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * ExceptionInterface
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * Exception thrown when a parameter is not valid
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class InvalidParameterException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * The resource was found but the request method is not allowed.
+ *
+ * This exception should trigger an HTTP 405 response in your application code.
+ *
+ * @author Kris Wallsmith <kris@symfony.com>
+ *
+ * @api
+ */
+class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface
+{
+ protected $allowedMethods;
+
+ public function __construct(array $allowedMethods, $message = null, $code = 0, \Exception $previous = null)
+ {
+ $this->allowedMethods = array_map('strtoupper', $allowedMethods);
+
+ parent::__construct($message, $code, $previous);
+ }
+
+ public function getAllowedMethods()
+ {
+ return $this->allowedMethods;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * Exception thrown when a route cannot be generated because of missing
+ * mandatory parameters.
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * The resource was not found.
+ *
+ * This exception should trigger an HTTP 404 response in your application code.
+ *
+ * @author Kris Wallsmith <kris@symfony.com>
+ *
+ * @api
+ */
+class ResourceNotFoundException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Exception;
+
+/**
+ * Exception thrown when a route does not exists
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class RouteNotFoundException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumper is the base class for all built-in generator dumpers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class GeneratorDumper implements GeneratorDumperInterface
+{
+ private $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes The RouteCollection to dump
+ */
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumperInterface is the interface that all generator dumper classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface GeneratorDumperInterface
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the generator class
+ */
+ public function dump(array $options = array());
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRoutes();
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class PhpGeneratorDumper extends GeneratorDumper
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the generator class
+ *
+ * @api
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_merge(array(
+ 'class' => 'ProjectUrlGenerator',
+ 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ ), $options);
+
+ return
+ $this->startClass($options['class'], $options['base_class']).
+ $this->addConstructor().
+ $this->addGenerator().
+ $this->endClass()
+ ;
+ }
+
+ private function addGenerator()
+ {
+ $methods = array();
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ $variables = str_replace("\n", '', var_export($compiledRoute->getVariables(), true));
+ $defaults = str_replace("\n", '', var_export($compiledRoute->getDefaults(), true));
+ $requirements = str_replace("\n", '', var_export($compiledRoute->getRequirements(), true));
+ $tokens = str_replace("\n", '', var_export($compiledRoute->getTokens(), true));
+
+ $escapedName = str_replace('.', '__', $name);
+
+ $methods[] = <<<EOF
+ private function get{$escapedName}RouteInfo()
+ {
+ return array($variables, $defaults, $requirements, $tokens);
+ }
+
+EOF
+ ;
+ }
+
+ $methods = implode("\n", $methods);
+
+ return <<<EOF
+
+ public function generate(\$name, \$parameters = array(), \$absolute = false)
+ {
+ if (!isset(self::\$declaredRouteNames[\$name])) {
+ throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', \$name));
+ }
+
+ \$escapedName = str_replace('.', '__', \$name);
+
+ list(\$variables, \$defaults, \$requirements, \$tokens) = \$this->{'get'.\$escapedName.'RouteInfo'}();
+
+ return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$absolute);
+ }
+
+$methods
+EOF;
+ }
+
+ private function startClass($class, $baseClass)
+ {
+ $routes = array();
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ $routes[] = " '$name' => true,";
+ }
+ $routes = implode("\n", $routes);
+
+ return <<<EOF
+<?php
+
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+
+
+/**
+ * $class
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class $class extends $baseClass
+{
+ static private \$declaredRouteNames = array(
+$routes
+ );
+
+
+EOF;
+ }
+
+ private function addConstructor()
+ {
+ return <<<EOF
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext \$context)
+ {
+ \$this->context = \$context;
+ }
+
+EOF;
+ }
+
+ private function endClass()
+ {
+ return <<<EOF
+}
+
+EOF;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+
+/**
+ * UrlGenerator generates URL based on a set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class UrlGenerator implements UrlGeneratorInterface
+{
+ protected $context;
+ protected $decodedChars = array(
+ // %2F is not valid in a URL, so we don't encode it (which is fine as the requirements explicitly allowed it)
+ '%2F' => '/',
+ );
+
+ protected $routes;
+ protected $cache;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes A RouteCollection instance
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function __construct(RouteCollection $routes, RequestContext $context)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ $this->cache = array();
+ }
+
+ /**
+ * Sets the request context.
+ *
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * Gets the request context.
+ *
+ * @return RequestContext The context
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * Generates a URL from the given parameters.
+ *
+ * @param string $name The name of the route
+ * @param mixed $parameters An array of parameters
+ * @param Boolean $absolute Whether to generate an absolute URL
+ *
+ * @return string The generated URL
+ *
+ * @throws Symfony\Component\Routing\Exception\RouteNotFoundException When route doesn't exist
+ *
+ * @api
+ */
+ public function generate($name, $parameters = array(), $absolute = false)
+ {
+ if (null === $route = $this->routes->get($name)) {
+ throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name));
+ }
+
+ if (!isset($this->cache[$name])) {
+ $this->cache[$name] = $route->compile();
+ }
+
+ return $this->doGenerate($this->cache[$name]->getVariables(), $route->getDefaults(), $route->getRequirements(), $this->cache[$name]->getTokens(), $parameters, $name, $absolute);
+ }
+
+ /**
+ * @throws Symfony\Component\Routing\Exception\MissingMandatoryParametersException When route has some missing mandatory parameters
+ * @throws Symfony\Component\Routing\Exception\InvalidParameterException When a parameter value is not correct
+ */
+ protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute)
+ {
+ $variables = array_flip($variables);
+
+ $originParameters = $parameters;
+ $parameters = array_replace($this->context->getParameters(), $parameters);
+ $tparams = array_replace($defaults, $parameters);
+
+ // all params must be given
+ if ($diff = array_diff_key($variables, $tparams)) {
+ throw new MissingMandatoryParametersException(sprintf('The "%s" route has some missing mandatory parameters ("%s").', $name, implode('", "', array_keys($diff))));
+ }
+
+ $url = '';
+ $optional = true;
+ foreach ($tokens as $token) {
+ if ('variable' === $token[0]) {
+ if (false === $optional || !array_key_exists($token[3], $defaults) || (isset($parameters[$token[3]]) && (string) $parameters[$token[3]] != (string) $defaults[$token[3]])) {
+ if (!$isEmpty = in_array($tparams[$token[3]], array(null, '', false), true)) {
+ // check requirement
+ if ($tparams[$token[3]] && !preg_match('#^'.$token[2].'$#', $tparams[$token[3]])) {
+ throw new InvalidParameterException(sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given).', $token[3], $name, $token[2], $tparams[$token[3]]));
+ }
+ }
+
+ if (!$isEmpty || !$optional) {
+ $url = $token[1].strtr(rawurlencode($tparams[$token[3]]), $this->decodedChars).$url;
+ }
+
+ $optional = false;
+ }
+ } elseif ('text' === $token[0]) {
+ $url = $token[1].$url;
+ $optional = false;
+ }
+ }
+
+ if (!$url) {
+ $url = '/';
+ }
+
+ // add a query string if needed
+ $extra = array_diff_key($originParameters, $variables, $defaults);
+ if ($extra && $query = http_build_query($extra, '', '&')) {
+ $url .= '?'.$query;
+ }
+
+ $url = $this->context->getBaseUrl().$url;
+
+ if ($this->context->getHost()) {
+ $scheme = $this->context->getScheme();
+ if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme != $req) {
+ $absolute = true;
+ $scheme = $req;
+ }
+
+ if ($absolute) {
+ $port = '';
+ if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
+ $port = ':'.$this->context->getHttpPort();
+ } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
+ $port = ':'.$this->context->getHttpsPort();
+ }
+
+ $url = $scheme.'://'.$this->context->getHost().$port.$url;
+ }
+ }
+
+ return $url;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator;
+
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * UrlGeneratorInterface is the interface that all URL generator classes must implements.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface UrlGeneratorInterface extends RequestContextAwareInterface
+{
+ /**
+ * Generates a URL from the given parameters.
+ *
+ * @param string $name The name of the route
+ * @param mixed $parameters An array of parameters
+ * @param Boolean $absolute Whether to generate an absolute URL
+ *
+ * @return string The generated URL
+ *
+ * @api
+ */
+ public function generate($name, $parameters = array(), $absolute = false);
+}
--- /dev/null
+Copyright (c) 2004-2012 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Doctrine\Common\Annotations\Reader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\Loader\LoaderResolver;
+
+/**
+ * AnnotationClassLoader loads routing information from a PHP class and its methods.
+ *
+ * You need to define an implementation for the getRouteDefaults() method. Most of the
+ * time, this method should define some PHP callable to be called for the route
+ * (a controller in MVC speak).
+ *
+ * The @Route annotation can be set on the class (for global parameters),
+ * and on each method.
+ *
+ * The @Route annotation main value is the route pattern. The annotation also
+ * recognizes three parameters: requirements, options, and name. The name parameter
+ * is mandatory. Here is an example of how you should be able to use it:
+ *
+ * /**
+ * * @Route("/Blog")
+ * * /
+ * class Blog
+ * {
+ * /**
+ * * @Route("/", name="blog_index")
+ * * /
+ * public function index()
+ * {
+ * }
+ *
+ * /**
+ * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
+ * * /
+ * public function show()
+ * {
+ * }
+ * }
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class AnnotationClassLoader implements LoaderInterface
+{
+ protected $reader;
+ protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
+ protected $defaultRouteIndex;
+
+ /**
+ * Constructor.
+ *
+ * @param Reader $reader
+ */
+ public function __construct(Reader $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ /**
+ * Sets the annotation class to read route properties from.
+ *
+ * @param string $class A fully-qualified class name
+ */
+ public function setRouteAnnotationClass($class)
+ {
+ $this->routeAnnotationClass = $class;
+ }
+
+ /**
+ * Loads from annotations from a class.
+ *
+ * @param string $class A class name
+ * @param string $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When route can't be parsed
+ */
+ public function load($class, $type = null)
+ {
+ if (!class_exists($class)) {
+ throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
+ }
+
+ $globals = array(
+ 'pattern' => '',
+ 'requirements' => array(),
+ 'options' => array(),
+ 'defaults' => array(),
+ );
+
+ $class = new \ReflectionClass($class);
+ if ($class->isAbstract()) {
+ throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class));
+ }
+
+ if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
+ if (null !== $annot->getPattern()) {
+ $globals['pattern'] = $annot->getPattern();
+ }
+
+ if (null !== $annot->getRequirements()) {
+ $globals['requirements'] = $annot->getRequirements();
+ }
+
+ if (null !== $annot->getOptions()) {
+ $globals['options'] = $annot->getOptions();
+ }
+
+ if (null !== $annot->getDefaults()) {
+ $globals['defaults'] = $annot->getDefaults();
+ }
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($class->getFileName()));
+
+ foreach ($class->getMethods() as $method) {
+ $this->defaultRouteIndex = 0;
+ foreach ($this->reader->getMethodAnnotations($method) as $annot) {
+ if ($annot instanceof $this->routeAnnotationClass) {
+ $this->addRoute($collection, $annot, $globals, $class, $method);
+ }
+ }
+ }
+
+ return $collection;
+ }
+
+ protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
+ {
+ $name = $annot->getName();
+ if (null === $name) {
+ $name = $this->getDefaultRouteName($class, $method);
+ }
+
+ $defaults = array_merge($globals['defaults'], $annot->getDefaults());
+ $requirements = array_merge($globals['requirements'], $annot->getRequirements());
+ $options = array_merge($globals['options'], $annot->getOptions());
+
+ $route = new Route($globals['pattern'].$annot->getPattern(), $defaults, $requirements, $options);
+
+ $this->configureRoute($route, $class, $method, $annot);
+
+ $collection->add($name, $route);
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * Sets the loader resolver.
+ *
+ * @param LoaderResolver $resolver A LoaderResolver instance
+ */
+ public function setResolver(LoaderResolver $resolver)
+ {
+ }
+
+ /**
+ * Gets the loader resolver.
+ *
+ * @return LoaderResolver A LoaderResolver instance
+ */
+ public function getResolver()
+ {
+ }
+
+ /**
+ * Gets the default route name for a class method.
+ *
+ * @param \ReflectionClass $class
+ * @param \ReflectionMethod $method
+ *
+ * @return string
+ */
+ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
+ {
+ $name = strtolower(str_replace('\\', '_', $class->name).'_'.$method->name);
+ if ($this->defaultRouteIndex > 0) {
+ $name .= '_'.$this->defaultRouteIndex;
+ }
+ $this->defaultRouteIndex++;
+
+ return $name;
+ }
+
+ abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot);
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Resource\DirectoryResource;
+
+/**
+ * AnnotationDirectoryLoader loads routing information from annotations set
+ * on PHP classes and methods.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class AnnotationDirectoryLoader extends AnnotationFileLoader
+{
+ /**
+ * Loads from annotations from a directory.
+ *
+ * @param string $path A directory path
+ * @param string $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
+ */
+ public function load($path, $type = null)
+ {
+ $dir = $this->locator->locate($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new DirectoryResource($dir, '/\.php$/'));
+ foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
+ if (!$file->isFile() || '.php' !== substr($file->getFilename(), -4)) {
+ continue;
+ }
+
+ if ($class = $this->findClass($file)) {
+ $refl = new \ReflectionClass($class);
+ if ($refl->isAbstract()) {
+ continue;
+ }
+
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ */
+ public function supports($resource, $type = null)
+ {
+ try {
+ $path = $this->locator->locate($resource);
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return is_string($resource) && is_dir($path) && (!$type || 'annotation' === $type);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\FileLocator;
+
+/**
+ * AnnotationFileLoader loads routing information from annotations set
+ * on a PHP class and its methods.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class AnnotationFileLoader extends FileLoader
+{
+ protected $loader;
+
+ /**
+ * Constructor.
+ *
+ * @param FileLocator $locator A FileLocator instance
+ * @param AnnotationClassLoader $loader An AnnotationClassLoader instance
+ * @param string|array $paths A path or an array of paths where to look for resources
+ */
+ public function __construct(FileLocator $locator, AnnotationClassLoader $loader, $paths = array())
+ {
+ if (!function_exists('token_get_all')) {
+ throw new \RuntimeException('The Tokenizer extension is required for the routing annotation loaders.');
+ }
+
+ parent::__construct($locator, $paths);
+
+ $this->loader = $loader;
+ }
+
+ /**
+ * Loads from annotations from a file.
+ *
+ * @param string $file A PHP file path
+ * @param string $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $collection = new RouteCollection();
+ if ($class = $this->findClass($path)) {
+ $collection->addResource(new FileResource($path));
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * Returns the full class name for the first class in the file.
+ *
+ * @param string $file A PHP file path
+ *
+ * @return string|false Full class name if found, false otherwise
+ */
+ protected function findClass($file)
+ {
+ $class = false;
+ $namespace = false;
+ $tokens = token_get_all(file_get_contents($file));
+ for ($i = 0, $count = count($tokens); $i < $count; $i++) {
+ $token = $tokens[$i];
+
+ if (!is_array($token)) {
+ continue;
+ }
+
+ if (true === $class && T_STRING === $token[0]) {
+ return $namespace.'\\'.$token[1];
+ }
+
+ if (true === $namespace && T_STRING === $token[0]) {
+ $namespace = '';
+ do {
+ $namespace .= $token[1];
+ $token = $tokens[++$i];
+ } while ($i < $count && is_array($token) && in_array($token[0], array(T_NS_SEPARATOR, T_STRING)));
+ }
+
+ if (T_CLASS === $token[0]) {
+ $class = true;
+ }
+
+ if (T_NAMESPACE === $token[0]) {
+ $namespace = true;
+ }
+ }
+
+ return false;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Loader\Loader;
+
+/**
+ * ClosureLoader loads routes from a PHP closure.
+ *
+ * The Closure must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class ClosureLoader extends Loader
+{
+ /**
+ * Loads a Closure.
+ *
+ * @param \Closure $closure A Closure
+ * @param string $type The resource type
+ *
+ * @api
+ */
+ public function load($closure, $type = null)
+ {
+ return call_user_func($closure);
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return $resource instanceof \Closure && (!$type || 'closure' === $type);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Loader\FileLoader;
+
+/**
+ * PhpFileLoader loads routes from a PHP file.
+ *
+ * The file must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class PhpFileLoader extends FileLoader
+{
+ /**
+ * Loads a PHP file.
+ *
+ * @param mixed $file A PHP file path
+ * @param string $type The resource type
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ // the loader variable is exposed to the included file below
+ $loader = $this;
+
+ $path = $this->locator->locate($file);
+ $this->setCurrentDir(dirname($path));
+
+ $collection = include $path;
+ $collection->addResource(new FileResource($path));
+
+ return $collection;
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Loader\FileLoader;
+
+/**
+ * XmlFileLoader loads XML routing files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class XmlFileLoader extends FileLoader
+{
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ * @param string $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When a tag can't be parsed
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $xml = $this->loadFile($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // process routes and imports
+ foreach ($xml->documentElement->childNodes as $node) {
+ if (!$node instanceof \DOMElement) {
+ continue;
+ }
+
+ $this->parseNode($collection, $node, $path, $file);
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Parses a node from a loaded XML file.
+ *
+ * @param RouteCollection $collection the collection to associate with the node
+ * @param DOMElement $node the node to parse
+ * @param string $path the path of the XML file being processed
+ * @param string $file
+ */
+ protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
+ {
+ switch ($node->tagName) {
+ case 'route':
+ $this->parseRoute($collection, $node, $path);
+ break;
+ case 'import':
+ $resource = (string) $node->getAttribute('resource');
+ $type = (string) $node->getAttribute('type');
+ $prefix = (string) $node->getAttribute('prefix');
+ $this->setCurrentDir(dirname($path));
+ $collection->addCollection($this->import($resource, ('' !== $type ? $type : null), false, $file), $prefix);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName));
+ }
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ * @param \DOMElement $definition Route definition
+ * @param string $file An XML file path
+ *
+ * @throws \InvalidArgumentException When the definition cannot be parsed
+ */
+ protected function parseRoute(RouteCollection $collection, \DOMElement $definition, $file)
+ {
+ $defaults = array();
+ $requirements = array();
+ $options = array();
+
+ foreach ($definition->childNodes as $node) {
+ if (!$node instanceof \DOMElement) {
+ continue;
+ }
+
+ switch ($node->tagName) {
+ case 'default':
+ $defaults[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
+ break;
+ case 'option':
+ $options[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
+ break;
+ case 'requirement':
+ $requirements[(string) $node->getAttribute('key')] = trim((string) $node->nodeValue);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName));
+ }
+ }
+
+ $route = new Route((string) $definition->getAttribute('pattern'), $defaults, $requirements, $options);
+
+ $collection->add((string) $definition->getAttribute('id'), $route);
+ }
+
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ *
+ * @return \DOMDocument
+ *
+ * @throws \InvalidArgumentException When loading of XML file returns error
+ */
+ protected function loadFile($file)
+ {
+ $internalErrors = libxml_use_internal_errors(true);
+ $disableEntities = libxml_disable_entity_loader(true);
+ libxml_clear_errors();
+
+ $dom = new \DOMDocument();
+ $dom->validateOnParse = true;
+ if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
+ libxml_disable_entity_loader($disableEntities);
+
+ throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($internalErrors)));
+ }
+ $dom->normalizeDocument();
+
+ libxml_use_internal_errors($internalErrors);
+ libxml_disable_entity_loader($disableEntities);
+
+ foreach ($dom->childNodes as $child) {
+ if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
+ throw new \InvalidArgumentException('Document types are not allowed.');
+ }
+ }
+
+ $this->validate($dom);
+
+ return $dom;
+ }
+
+ /**
+ * Validates a loaded XML file.
+ *
+ * @param \DOMDocument $dom A loaded XML file
+ *
+ * @throws \InvalidArgumentException When XML doesn't validate its XSD schema
+ */
+ protected function validate(\DOMDocument $dom)
+ {
+ $location = __DIR__.'/schema/routing/routing-1.0.xsd';
+
+ $current = libxml_use_internal_errors(true);
+ libxml_clear_errors();
+
+ if (!$dom->schemaValidate($location)) {
+ throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors($current)));
+ }
+ libxml_use_internal_errors($current);
+ }
+
+ /**
+ * Retrieves libxml errors and clears them.
+ *
+ * @return array An array of libxml error strings
+ */
+ private function getXmlErrors($internalErrors)
+ {
+ $errors = array();
+ foreach (libxml_get_errors() as $error) {
+ $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
+ LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
+ $error->code,
+ trim($error->message),
+ $error->file ? $error->file : 'n/a',
+ $error->line,
+ $error->column
+ );
+ }
+
+ libxml_clear_errors();
+ libxml_use_internal_errors($internalErrors);
+
+ return $errors;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Yaml\Yaml;
+use Symfony\Component\Config\Loader\FileLoader;
+
+/**
+ * YamlFileLoader loads Yaml routing files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class YamlFileLoader extends FileLoader
+{
+ private static $availableKeys = array(
+ 'type', 'resource', 'prefix', 'pattern', 'options', 'defaults', 'requirements'
+ );
+
+ /**
+ * Loads a Yaml file.
+ *
+ * @param string $file A Yaml file path
+ * @param string $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When route can't be parsed
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $config = Yaml::parse($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // empty file
+ if (null === $config) {
+ $config = array();
+ }
+
+ // not an array
+ if (!is_array($config)) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $file));
+ }
+
+ foreach ($config as $name => $config) {
+ $config = $this->normalizeRouteConfig($config);
+
+ if (isset($config['resource'])) {
+ $type = isset($config['type']) ? $config['type'] : null;
+ $prefix = isset($config['prefix']) ? $config['prefix'] : null;
+ $this->setCurrentDir(dirname($path));
+ $collection->addCollection($this->import($config['resource'], $type, false, $file), $prefix);
+ } else {
+ $this->parseRoute($collection, $name, $config, $path);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Returns true if this class supports the given resource.
+ *
+ * @param mixed $resource A resource
+ * @param string $type The resource type
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'yaml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ * @param string $name Route name
+ * @param array $config Route definition
+ * @param string $file A Yaml file path
+ *
+ * @throws \InvalidArgumentException When config pattern is not defined for the given route
+ */
+ protected function parseRoute(RouteCollection $collection, $name, $config, $file)
+ {
+ $defaults = isset($config['defaults']) ? $config['defaults'] : array();
+ $requirements = isset($config['requirements']) ? $config['requirements'] : array();
+ $options = isset($config['options']) ? $config['options'] : array();
+
+ if (!isset($config['pattern'])) {
+ throw new \InvalidArgumentException(sprintf('You must define a "pattern" for the "%s" route.', $name));
+ }
+
+ $route = new Route($config['pattern'], $defaults, $requirements, $options);
+
+ $collection->add($name, $route);
+ }
+
+ /**
+ * Normalize route configuration.
+ *
+ * @param array $config A resource config
+ *
+ * @return array
+ *
+ * @throws InvalidArgumentException if one of the provided config keys is not supported
+ */
+ private function normalizeRouteConfig(array $config)
+ {
+ foreach ($config as $key => $value) {
+ if (!in_array($key, self::$availableKeys)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Yaml routing loader does not support given key: "%s". Expected one of the (%s).',
+ $key, implode(', ', self::$availableKeys)
+ ));
+ }
+ }
+
+ return $config;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<xsd:schema xmlns="http://symfony.com/schema/routing"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ targetNamespace="http://symfony.com/schema/routing"
+ elementFormDefault="qualified">
+
+ <xsd:element name="routes" type="routes" />
+
+ <xsd:complexType name="routes">
+ <xsd:choice maxOccurs="unbounded" minOccurs="0">
+ <xsd:element name="import" type="import" />
+ <xsd:element name="route" type="route" />
+ </xsd:choice>
+ </xsd:complexType>
+
+ <xsd:complexType name="route">
+ <xsd:sequence>
+ <xsd:element name="default" type="element" minOccurs="0" maxOccurs="unbounded" />
+ <xsd:element name="requirement" type="element" minOccurs="0" maxOccurs="unbounded" />
+ <xsd:element name="option" type="element" minOccurs="0" maxOccurs="unbounded" />
+ </xsd:sequence>
+
+ <xsd:attribute name="id" type="xsd:string" />
+ <xsd:attribute name="pattern" type="xsd:string" />
+ </xsd:complexType>
+
+ <xsd:complexType name="import">
+ <xsd:attribute name="resource" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="prefix" type="xsd:string" />
+ <xsd:attribute name="class" type="xsd:string" />
+ </xsd:complexType>
+
+ <xsd:complexType name="element" mixed="true">
+ <xsd:attribute name="key" type="xsd:string" />
+ </xsd:complexType>
+</xsd:schema>
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+/**
+ * ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ApacheUrlMatcher extends UrlMatcher
+{
+ /**
+ * Tries to match a URL based on Apache mod_rewrite matching.
+ *
+ * Returns false if no route matches the URL.
+ *
+ * @param string $pathinfo The pathinfo to be parsed
+ *
+ * @return array An array of parameters
+ *
+ * @throws MethodNotAllowedException If the current method is not allowed
+ */
+ public function match($pathinfo)
+ {
+ $parameters = array();
+ $defaults = array();
+ $allow = array();
+ $match = false;
+
+ foreach ($_SERVER as $key => $value) {
+ $name = $key;
+
+ if (0 === strpos($name, 'REDIRECT_')) {
+ $name = substr($name, 9);
+ }
+
+ if (0 === strpos($name, '_ROUTING_DEFAULTS_')) {
+ $name = substr($name, 18);
+ $defaults[$name] = $value;
+ } elseif (0 === strpos($name, '_ROUTING_')) {
+ $name = substr($name, 9);
+ if ('_route' == $name) {
+ $match = true;
+ $parameters[$name] = $value;
+ } elseif (0 === strpos($name, '_allow_')) {
+ $allow[] = substr($name, 7);
+ } else {
+ $parameters[$name] = $value;
+ }
+ } else {
+ continue;
+ }
+
+ unset($_SERVER[$key]);
+ }
+
+ if ($match) {
+ return $this->mergeDefaults($parameters, $defaults);
+ } elseif (0 < count($allow)) {
+ throw new MethodNotAllowedException($allow);
+ } else {
+ return parent::match($pathinfo);
+ }
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+/**
+ * Dumps a set of Apache mod_rewrite rules.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Kris Wallsmith <kris@symfony.com>
+ */
+class ApacheMatcherDumper extends MatcherDumper
+{
+ /**
+ * Dumps a set of Apache mod_rewrite rules.
+ *
+ * Available options:
+ *
+ * * script_name: The script name (app.php by default)
+ * * base_uri: The base URI ("" by default)
+ *
+ * @param array $options An array of options
+ *
+ * @return string A string to be used as Apache rewrite rules
+ *
+ * @throws \LogicException When the route regex is invalid
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_merge(array(
+ 'script_name' => 'app.php',
+ 'base_uri' => '',
+ ), $options);
+
+ $options['script_name'] = self::escape($options['script_name'], ' ', '\\');
+
+ $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
+ $methodVars = array();
+
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ // prepare the apache regex
+ $regex = $compiledRoute->getRegex();
+ $delimiter = $regex[0];
+ $regexPatternEnd = strrpos($regex, $delimiter);
+ if (strlen($regex) < 2 || 0 === $regexPatternEnd) {
+ throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex);
+ }
+ $regex = preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
+ $regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
+
+ $hasTrailingSlash = '/$' == substr($regex, -2) && '^/$' != $regex;
+
+ $variables = array('E=_ROUTING__route:'.$name);
+ foreach ($compiledRoute->getVariables() as $i => $variable) {
+ $variables[] = 'E=_ROUTING_'.$variable.':%'.($i + 1);
+ }
+ foreach ($route->getDefaults() as $key => $value) {
+ $variables[] = 'E=_ROUTING_DEFAULTS_'.$key.':'.strtr($value, array(
+ ':' => '\\:',
+ '=' => '\\=',
+ '\\' => '\\\\',
+ ' ' => '\\ ',
+ ));
+ }
+ $variables = implode(',', $variables);
+
+ $rule = array("# $name");
+
+ // method mismatch
+ if ($req = $route->getRequirement('_method')) {
+ $methods = explode('|', strtoupper($req));
+ // GET and HEAD are equivalent
+ if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
+ $methods[] = 'HEAD';
+ }
+ $allow = array();
+ foreach ($methods as $method) {
+ $methodVars[] = $method;
+ $allow[] = 'E=_ROUTING__allow_'.$method.':1';
+ }
+
+ $rule[] = "RewriteCond %{REQUEST_URI} $regex";
+ $rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods));
+ $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
+ }
+
+ // redirect with trailing slash appended
+ if ($hasTrailingSlash) {
+ $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
+ $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
+ }
+
+ // the main rule
+ $rule[] = "RewriteCond %{REQUEST_URI} $regex";
+ $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
+
+ $rules[] = implode("\n", $rule);
+ }
+
+ if (0 < count($methodVars)) {
+ $rule = array('# 405 Method Not Allowed');
+ $methodVars = array_values(array_unique($methodVars));
+ foreach ($methodVars as $i => $methodVar) {
+ $rule[] = sprintf('RewriteCond %%{_ROUTING__allow_%s} !-z%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
+ }
+ $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']);
+
+ $rules[] = implode("\n", $rule);
+ }
+
+ return implode("\n\n", $rules)."\n";
+ }
+
+ /**
+ * Escapes a string.
+ *
+ * @param string $string The string to be escaped
+ * @param string $char The character to be escaped
+ * @param string $with The character to be used for escaping
+ *
+ * @return string The escaped string
+ */
+ private static function escape($string, $char, $with)
+ {
+ $escaped = false;
+ $output = '';
+ foreach (str_split($string) as $symbol) {
+ if ($escaped) {
+ $output .= $symbol;
+ $escaped = false;
+ continue;
+ }
+ if ($symbol === $char) {
+ $output .= $with.$char;
+ continue;
+ }
+ if ($symbol === $with) {
+ $escaped = true;
+ }
+ $output .= $symbol;
+ }
+
+ return $output;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * MatcherDumper is the abstract class for all built-in matcher dumpers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class MatcherDumper implements MatcherDumperInterface
+{
+ private $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes The RouteCollection to dump
+ */
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+/**
+ * MatcherDumperInterface is the interface that all matcher dumper classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface MatcherDumperInterface
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the matcher class
+ */
+ public function dump(array $options = array());
+
+ /**
+ * Gets the routes to match.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRoutes();
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class PhpMatcherDumper extends MatcherDumper
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the matcher class
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_merge(array(
+ 'class' => 'ProjectUrlMatcher',
+ 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ ), $options);
+
+ // trailing slash support is only enabled if we know how to redirect the user
+ $interfaces = class_implements($options['base_class']);
+ $supportsRedirections = isset($interfaces['Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface']);
+
+ return
+ $this->startClass($options['class'], $options['base_class']).
+ $this->addConstructor().
+ $this->addMatcher($supportsRedirections).
+ $this->endClass()
+ ;
+ }
+
+ private function addMatcher($supportsRedirections)
+ {
+ // we need to deep clone the routes as we will modify the structure to optimize the dump
+ $code = implode("\n", $this->compileRoutes(clone $this->getRoutes(), $supportsRedirections));
+
+ return <<<EOF
+
+ public function match(\$pathinfo)
+ {
+ \$allow = array();
+ \$pathinfo = urldecode(\$pathinfo);
+
+$code
+ throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
+ }
+
+EOF;
+ }
+
+ private function compileRoutes(RouteCollection $routes, $supportsRedirections, $parentPrefix = null)
+ {
+ $code = array();
+
+ $routeIterator = $routes->getIterator();
+ $keys = array_keys($routeIterator->getArrayCopy());
+ $keysCount = count($keys);
+
+ $i = 0;
+ foreach ($routeIterator as $name => $route) {
+ $i++;
+
+ if ($route instanceof RouteCollection) {
+ $prefix = $route->getPrefix();
+ $optimizable = $prefix && count($route->all()) > 1 && false === strpos($route->getPrefix(), '{');
+ $indent = '';
+ if ($optimizable) {
+ for ($j = $i; $j < $keysCount; $j++) {
+ if ($keys[$j] === null) {
+ continue;
+ }
+
+ $testRoute = $routeIterator->offsetGet($keys[$j]);
+ $isCollection = ($testRoute instanceof RouteCollection);
+
+ $testPrefix = $isCollection ? $testRoute->getPrefix() : $testRoute->getPattern();
+
+ if (0 === strpos($testPrefix, $prefix)) {
+ $routeIterator->offsetUnset($keys[$j]);
+
+ if ($isCollection) {
+ $route->addCollection($testRoute);
+ } else {
+ $route->add($keys[$j], $testRoute);
+ }
+
+ $i++;
+ $keys[$j] = null;
+ }
+ }
+
+ if ($prefix !== $parentPrefix) {
+ $code[] = sprintf(" if (0 === strpos(\$pathinfo, %s)) {", var_export($prefix, true));
+ $indent = ' ';
+ }
+ }
+
+ foreach ($this->compileRoutes($route, $supportsRedirections, $prefix) as $line) {
+ foreach (explode("\n", $line) as $l) {
+ if ($l) {
+ $code[] = $indent.$l;
+ } else {
+ $code[] = $l;
+ }
+ }
+ }
+
+ if ($optimizable && $prefix !== $parentPrefix) {
+ $code[] = " }\n";
+ }
+ } else {
+ foreach ($this->compileRoute($route, $name, $supportsRedirections, $parentPrefix) as $line) {
+ $code[] = $line;
+ }
+ }
+ }
+
+ return $code;
+ }
+
+ private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
+ {
+ $code = array();
+ $compiledRoute = $route->compile();
+ $conditions = array();
+ $hasTrailingSlash = false;
+ $matches = false;
+ if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
+ if ($supportsRedirections && substr($m['url'], -1) === '/') {
+ $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
+ $hasTrailingSlash = true;
+ } else {
+ $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true));
+ }
+ } else {
+ if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() != $parentPrefix) {
+ $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
+ }
+
+ $regex = $compiledRoute->getRegex();
+ if ($supportsRedirections && $pos = strpos($regex, '/$')) {
+ $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
+ $hasTrailingSlash = true;
+ }
+ $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true));
+
+ $matches = true;
+ }
+
+ $conditions = implode(' && ', $conditions);
+
+ $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
+
+ $code[] = <<<EOF
+ // $name
+ if ($conditions) {
+EOF;
+
+ if ($req = $route->getRequirement('_method')) {
+ $methods = explode('|', strtoupper($req));
+ // GET and HEAD are equivalent
+ if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
+ $methods[] = 'HEAD';
+ }
+ if (1 === count($methods)) {
+ $code[] = <<<EOF
+ if (\$this->context->getMethod() != '$methods[0]') {
+ \$allow[] = '$methods[0]';
+ goto $gotoname;
+ }
+EOF;
+ } else {
+ $methods = implode('\', \'', $methods);
+ $code[] = <<<EOF
+ if (!in_array(\$this->context->getMethod(), array('$methods'))) {
+ \$allow = array_merge(\$allow, array('$methods'));
+ goto $gotoname;
+ }
+EOF;
+ }
+ }
+
+ if ($hasTrailingSlash) {
+ $code[] = sprintf(<<<EOF
+ if (substr(\$pathinfo, -1) !== '/') {
+ return \$this->redirect(\$pathinfo.'/', '%s');
+ }
+EOF
+ , $name);
+ }
+
+ if ($scheme = $route->getRequirement('_scheme')) {
+ if (!$supportsRedirections) {
+ throw new \LogicException('The "_scheme" requirement is only supported for route dumper that implements RedirectableUrlMatcherInterface.');
+ }
+
+ $code[] = sprintf(<<<EOF
+ if (\$this->context->getScheme() !== '$scheme') {
+ return \$this->redirect(\$pathinfo, '%s', '$scheme');
+ }
+EOF
+ , $name);
+ }
+
+ // optimize parameters array
+ if (true === $matches && $compiledRoute->getDefaults()) {
+ $code[] = sprintf(" return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));"
+ , str_replace("\n", '', var_export($compiledRoute->getDefaults(), true)), $name);
+ } elseif (true === $matches) {
+ $code[] = sprintf(" \$matches['_route'] = '%s';", $name);
+ $code[] = sprintf(" return \$matches;", $name);
+ } elseif ($compiledRoute->getDefaults()) {
+ $code[] = sprintf(' return %s;', str_replace("\n", '', var_export(array_merge($compiledRoute->getDefaults(), array('_route' => $name)), true)));
+ } else {
+ $code[] = sprintf(" return array('_route' => '%s');", $name);
+ }
+ $code[] = " }";
+
+ if ($req) {
+ $code[] = " $gotoname:";
+ }
+
+ $code[] = '';
+
+ return $code;
+ }
+
+ private function startClass($class, $baseClass)
+ {
+ return <<<EOF
+<?php
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * $class
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class $class extends $baseClass
+{
+
+EOF;
+ }
+
+ private function addConstructor()
+ {
+ return <<<EOF
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext \$context)
+ {
+ \$this->context = \$context;
+ }
+
+EOF;
+ }
+
+ private function endClass()
+ {
+ return <<<EOF
+}
+
+EOF;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
+{
+ private $trailingSlashTest = false;
+
+ /**
+ * @see UrlMatcher::match()
+ *
+ * @api
+ */
+ public function match($pathinfo)
+ {
+ try {
+ $parameters = parent::match($pathinfo);
+ } catch (ResourceNotFoundException $e) {
+ if ('/' === substr($pathinfo, -1)) {
+ throw $e;
+ }
+
+ // try with a / at the end
+ $this->trailingSlashTest = true;
+
+ return $this->match($pathinfo.'/');
+ }
+
+ if ($this->trailingSlashTest) {
+ $this->trailingSlashTest = false;
+
+ return $this->redirect($pathinfo, null);
+ }
+
+ return $parameters;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+/**
+ * RedirectableUrlMatcherInterface knows how to redirect the user.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface RedirectableUrlMatcherInterface
+{
+ /**
+ * Redirects the user to another URL.
+ *
+ * @param string $path The path info to redirect to.
+ * @param string $route The route that matched
+ * @param string $scheme The URL scheme (null to keep the current one)
+ *
+ * @return array An array of parameters
+ *
+ * @api
+ */
+ public function redirect($path, $route, $scheme = null);
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * UrlMatcher matches URL based on a set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class UrlMatcher implements UrlMatcherInterface
+{
+ protected $context;
+ protected $allow;
+
+ private $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes A RouteCollection instance
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function __construct(RouteCollection $routes, RequestContext $context)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ }
+
+ /**
+ * Sets the request context.
+ *
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * Gets the request context.
+ *
+ * @return RequestContext The context
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * Tries to match a URL with a set of routes.
+ *
+ * @param string $pathinfo The path info to be parsed
+ *
+ * @return array An array of parameters
+ *
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ *
+ * @api
+ */
+ public function match($pathinfo)
+ {
+ $this->allow = array();
+
+ if ($ret = $this->matchCollection($pathinfo, $this->routes)) {
+ return $ret;
+ }
+
+ throw 0 < count($this->allow)
+ ? new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow)))
+ : new ResourceNotFoundException();
+ }
+
+ protected function matchCollection($pathinfo, RouteCollection $routes)
+ {
+ $pathinfo = urldecode($pathinfo);
+
+ foreach ($routes as $name => $route) {
+ if ($route instanceof RouteCollection) {
+ if (false === strpos($route->getPrefix(), '{') && $route->getPrefix() !== substr($pathinfo, 0, strlen($route->getPrefix()))) {
+ continue;
+ }
+
+ if (!$ret = $this->matchCollection($pathinfo, $route)) {
+ continue;
+ }
+
+ return $ret;
+ }
+
+ $compiledRoute = $route->compile();
+
+ // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
+ if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($pathinfo, $compiledRoute->getStaticPrefix())) {
+ continue;
+ }
+
+ if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) {
+ continue;
+ }
+
+ // check HTTP method requirement
+ if ($req = $route->getRequirement('_method')) {
+ // HEAD and GET are equivalent as per RFC
+ if ('HEAD' === $method = $this->context->getMethod()) {
+ $method = 'GET';
+ }
+
+ if (!in_array($method, $req = explode('|', strtoupper($req)))) {
+ $this->allow = array_merge($this->allow, $req);
+
+ continue;
+ }
+ }
+
+ return array_merge($this->mergeDefaults($matches, $route->getDefaults()), array('_route' => $name));
+ }
+ }
+
+ protected function mergeDefaults($params, $defaults)
+ {
+ $parameters = $defaults;
+ foreach ($params as $key => $value) {
+ if (!is_int($key)) {
+ $parameters[$key] = rawurldecode($value);
+ }
+ }
+
+ return $parameters;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Matcher;
+
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * UrlMatcherInterface is the interface that all URL matcher classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface UrlMatcherInterface extends RequestContextAwareInterface
+{
+ /**
+ * Tries to match a URL with a set of routes.
+ *
+ * @param string $pathinfo The path info to be parsed
+ *
+ * @return array An array of parameters
+ *
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ *
+ * @api
+ */
+ public function match($pathinfo);
+}
--- /dev/null
+Routing Component
+=================
+
+Routing associates a request with the code that will convert it to a response.
+
+The example below demonstrates how you can set up a fully working routing
+system:
+
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Matcher\UrlMatcher;
+ use Symfony\Component\Routing\RequestContext;
+ use Symfony\Component\Routing\RouteCollection;
+ use Symfony\Component\Routing\Route;
+
+ $routes = new RouteCollection();
+ $routes->add('hello', new Route('/hello', array('controller' => 'foo')));
+
+ $context = new RequestContext();
+
+ // this is optional and can be done without a Request instance
+ $context->fromRequest(Request::createFromGlobals());
+
+ $matcher = new UrlMatcher($routes, $context);
+
+ $parameters = $matcher->match('/hello');
+
+Resources
+---------
+
+Unit tests:
+
+https://github.com/symfony/symfony/tree/master/tests/Symfony/Tests/Component/Routing
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * Holds information about the current request.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class RequestContext
+{
+ private $baseUrl;
+ private $method;
+ private $host;
+ private $scheme;
+ private $httpPort;
+ private $httpsPort;
+ private $parameters;
+
+ /**
+ * Constructor.
+ *
+ * @param string $baseUrl The base URL
+ * @param string $method The HTTP method
+ * @param string $host The HTTP host name
+ * @param string $scheme The HTTP scheme
+ * @param integer $httpPort The HTTP port
+ * @param integer $httpsPort The HTTPS port
+ *
+ * @api
+ */
+ public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443)
+ {
+ $this->baseUrl = $baseUrl;
+ $this->method = strtoupper($method);
+ $this->host = $host;
+ $this->scheme = strtolower($scheme);
+ $this->httpPort = $httpPort;
+ $this->httpsPort = $httpsPort;
+ $this->parameters = array();
+ }
+
+ /**
+ * Gets the base URL.
+ *
+ * @return string The base URL
+ */
+ public function getBaseUrl()
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * Sets the base URL.
+ *
+ * @param string $baseUrl The base URL
+ *
+ * @api
+ */
+ public function setBaseUrl($baseUrl)
+ {
+ $this->baseUrl = $baseUrl;
+ }
+
+ /**
+ * Gets the HTTP method.
+ *
+ * The method is always an uppercased string.
+ *
+ * @return string The HTTP method
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * Sets the HTTP method.
+ *
+ * @param string $method The HTTP method
+ *
+ * @api
+ */
+ public function setMethod($method)
+ {
+ $this->method = strtoupper($method);
+ }
+
+ /**
+ * Gets the HTTP host.
+ *
+ * @return string The HTTP host
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Sets the HTTP host.
+ *
+ * @param string $host The HTTP host
+ *
+ * @api
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+ }
+
+ /**
+ * Gets the HTTP scheme.
+ *
+ * @return string The HTTP scheme
+ */
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * Sets the HTTP scheme.
+ *
+ * @param string $scheme The HTTP scheme
+ *
+ * @api
+ */
+ public function setScheme($scheme)
+ {
+ $this->scheme = strtolower($scheme);
+ }
+
+ /**
+ * Gets the HTTP port.
+ *
+ * @return string The HTTP port
+ */
+ public function getHttpPort()
+ {
+ return $this->httpPort;
+ }
+
+ /**
+ * Sets the HTTP port.
+ *
+ * @param string $httpPort The HTTP port
+ *
+ * @api
+ */
+ public function setHttpPort($httpPort)
+ {
+ $this->httpPort = $httpPort;
+ }
+
+ /**
+ * Gets the HTTPS port.
+ *
+ * @return string The HTTPS port
+ */
+ public function getHttpsPort()
+ {
+ return $this->httpsPort;
+ }
+
+ /**
+ * Sets the HTTPS port.
+ *
+ * @param string $httpsPort The HTTPS port
+ *
+ * @api
+ */
+ public function setHttpsPort($httpsPort)
+ {
+ $this->httpsPort = $httpsPort;
+ }
+
+ /**
+ * Returns the parameters.
+ *
+ * @return array The parameters
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Sets the parameters.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $parameters The parameters
+ *
+ * @return Route The current Route instance
+ */
+ public function setParameters(array $parameters)
+ {
+ $this->parameters = $parameters;
+
+ return $this;
+ }
+
+ /**
+ * Gets a parameter value.
+ *
+ * @param string $name A parameter name
+ *
+ * @return mixed The parameter value
+ */
+ public function getParameter($name)
+ {
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+ }
+
+ /**
+ * Checks if a parameter value is set for the given parameter.
+ *
+ * @param string $name A parameter name
+ *
+ * @return Boolean true if the parameter value is set, false otherwise
+ */
+ public function hasParameter($name)
+ {
+ return array_key_exists($name, $this->parameters);
+ }
+
+ /**
+ * Sets a parameter value.
+ *
+ * @param string $name A parameter name
+ * @param mixed $parameter The parameter value
+ *
+ * @api
+ */
+ public function setParameter($name, $parameter)
+ {
+ $this->parameters[$name] = $parameter;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * @api
+ */
+interface RequestContextAwareInterface
+{
+ /**
+ * Sets the request context.
+ *
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function setContext(RequestContext $context);
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * A Route describes a route and its parameters.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class Route
+{
+ private $pattern;
+ private $defaults;
+ private $requirements;
+ private $options;
+ private $compiled;
+
+ private static $compilers = array();
+
+ /**
+ * Constructor.
+ *
+ * Available options:
+ *
+ * * compiler_class: A class name able to compile this route instance (RouteCompiler by default)
+ *
+ * @param string $pattern The pattern to match
+ * @param array $defaults An array of default parameter values
+ * @param array $requirements An array of requirements for parameters (regexes)
+ * @param array $options An array of options
+ *
+ * @api
+ */
+ public function __construct($pattern, array $defaults = array(), array $requirements = array(), array $options = array())
+ {
+ $this->setPattern($pattern);
+ $this->setDefaults($defaults);
+ $this->setRequirements($requirements);
+ $this->setOptions($options);
+ }
+
+ public function __clone()
+ {
+ $this->compiled = null;
+ }
+
+ /**
+ * Returns the pattern.
+ *
+ * @return string The pattern
+ */
+ public function getPattern()
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * Sets the pattern.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $pattern The pattern
+ *
+ * @return Route The current Route instance
+ */
+ public function setPattern($pattern)
+ {
+ $this->pattern = trim($pattern);
+
+ // a route must start with a slash
+ if (empty($this->pattern) || '/' !== $this->pattern[0]) {
+ $this->pattern = '/'.$this->pattern;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the options.
+ *
+ * @return array The options
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Sets the options.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $options The options
+ *
+ * @return Route The current Route instance
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = array_merge(array(
+ 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
+ ), $options);
+
+ return $this;
+ }
+
+ /**
+ * Sets an option value.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $name An option name
+ * @param mixed $value The option value
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setOption($name, $value)
+ {
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get an option value.
+ *
+ * @param string $name An option name
+ *
+ * @return mixed The option value
+ */
+ public function getOption($name)
+ {
+ return isset($this->options[$name]) ? $this->options[$name] : null;
+ }
+
+ /**
+ * Returns the defaults.
+ *
+ * @return array The defaults
+ */
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+
+ /**
+ * Sets the defaults.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $defaults The defaults
+ *
+ * @return Route The current Route instance
+ */
+ public function setDefaults(array $defaults)
+ {
+ $this->defaults = array();
+ foreach ($defaults as $name => $default) {
+ $this->defaults[(string) $name] = $default;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets a default value.
+ *
+ * @param string $name A variable name
+ *
+ * @return mixed The default value
+ */
+ public function getDefault($name)
+ {
+ return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
+ }
+
+ /**
+ * Checks if a default value is set for the given variable.
+ *
+ * @param string $name A variable name
+ *
+ * @return Boolean true if the default value is set, false otherwise
+ */
+ public function hasDefault($name)
+ {
+ return array_key_exists($name, $this->defaults);
+ }
+
+ /**
+ * Sets a default value.
+ *
+ * @param string $name A variable name
+ * @param mixed $default The default value
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setDefault($name, $default)
+ {
+ $this->defaults[(string) $name] = $default;
+
+ return $this;
+ }
+
+ /**
+ * Returns the requirements.
+ *
+ * @return array The requirements
+ */
+ public function getRequirements()
+ {
+ return $this->requirements;
+ }
+
+ /**
+ * Sets the requirements.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $requirements The requirements
+ *
+ * @return Route The current Route instance
+ */
+ public function setRequirements(array $requirements)
+ {
+ $this->requirements = array();
+ foreach ($requirements as $key => $regex) {
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the requirement for the given key.
+ *
+ * @param string $key The key
+ *
+ * @return string The regex
+ */
+ public function getRequirement($key)
+ {
+ return isset($this->requirements[$key]) ? $this->requirements[$key] : null;
+ }
+
+ /**
+ * Sets a requirement for the given key.
+ *
+ * @param string $key The key
+ * @param string $regex The regex
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setRequirement($key, $regex)
+ {
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+
+ return $this;
+ }
+
+ /**
+ * Compiles the route.
+ *
+ * @return CompiledRoute A CompiledRoute instance
+ */
+ public function compile()
+ {
+ if (null !== $this->compiled) {
+ return $this->compiled;
+ }
+
+ $class = $this->getOption('compiler_class');
+
+ if (!isset(self::$compilers[$class])) {
+ self::$compilers[$class] = new $class;
+ }
+
+ return $this->compiled = self::$compilers[$class]->compile($this);
+ }
+
+ private function sanitizeRequirement($key, $regex)
+ {
+ if (is_array($regex)) {
+ throw new \InvalidArgumentException(sprintf('Routing requirements must be a string, array given for "%s"', $key));
+ }
+
+ if ('^' == $regex[0]) {
+ $regex = substr($regex, 1);
+ }
+
+ if ('$' == substr($regex, -1)) {
+ $regex = substr($regex, 0, -1);
+ }
+
+ return $regex;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Config\Resource\ResourceInterface;
+
+/**
+ * A RouteCollection represents a set of Route instances.
+ *
+ * When adding a route, it overrides existing routes with the
+ * same name defined in the instance or its children and parents.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class RouteCollection implements \IteratorAggregate
+{
+ private $routes;
+ private $resources;
+ private $prefix;
+ private $parent;
+
+ /**
+ * Constructor.
+ *
+ * @api
+ */
+ public function __construct()
+ {
+ $this->routes = array();
+ $this->resources = array();
+ $this->prefix = '';
+ }
+
+ public function __clone()
+ {
+ foreach ($this->routes as $name => $route) {
+ $this->routes[$name] = clone $route;
+ if ($route instanceof RouteCollection) {
+ $this->routes[$name]->setParent($this);
+ }
+ }
+ }
+
+ /**
+ * Gets the parent RouteCollection.
+ *
+ * @return RouteCollection The parent RouteCollection
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Sets the parent RouteCollection.
+ *
+ * @param RouteCollection $parent The parent RouteCollection
+ */
+ public function setParent(RouteCollection $parent)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Gets the current RouteCollection as an Iterator.
+ *
+ * @return \ArrayIterator An \ArrayIterator interface
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->routes);
+ }
+
+ /**
+ * Adds a route.
+ *
+ * @param string $name The route name
+ * @param Route $route A Route instance
+ *
+ * @throws \InvalidArgumentException When route name contains non valid characters
+ *
+ * @api
+ */
+ public function add($name, Route $route)
+ {
+ if (!preg_match('/^[a-z0-9A-Z_.]+$/', $name)) {
+ throw new \InvalidArgumentException(sprintf('The provided route name "%s" contains non valid characters. A route name must only contain digits (0-9), letters (a-z and A-Z), underscores (_) and dots (.).', $name));
+ }
+
+ $parent = $this;
+ while ($parent->getParent()) {
+ $parent = $parent->getParent();
+ }
+
+ if ($parent) {
+ $parent->remove($name);
+ }
+
+ $this->routes[$name] = $route;
+ }
+
+ /**
+ * Returns the array of routes.
+ *
+ * @return array An array of routes
+ */
+ public function all()
+ {
+ $routes = array();
+ foreach ($this->routes as $name => $route) {
+ if ($route instanceof RouteCollection) {
+ $routes = array_merge($routes, $route->all());
+ } else {
+ $routes[$name] = $route;
+ }
+ }
+
+ return $routes;
+ }
+
+ /**
+ * Gets a route by name.
+ *
+ * @param string $name The route name
+ *
+ * @return Route $route A Route instance
+ */
+ public function get($name)
+ {
+ // get the latest defined route
+ foreach (array_reverse($this->routes) as $routes) {
+ if (!$routes instanceof RouteCollection) {
+ continue;
+ }
+
+ if (null !== $route = $routes->get($name)) {
+ return $route;
+ }
+ }
+
+ if (isset($this->routes[$name])) {
+ return $this->routes[$name];
+ }
+ }
+
+ /**
+ * Removes a route by name.
+ *
+ * @param string $name The route name
+ */
+ public function remove($name)
+ {
+ if (isset($this->routes[$name])) {
+ unset($this->routes[$name]);
+ }
+
+ foreach ($this->routes as $routes) {
+ if ($routes instanceof RouteCollection) {
+ $routes->remove($name);
+ }
+ }
+ }
+
+ /**
+ * Adds a route collection to the current set of routes (at the end of the current set).
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ * @param string $prefix An optional prefix to add before each pattern of the route collection
+ *
+ * @api
+ */
+ public function addCollection(RouteCollection $collection, $prefix = '')
+ {
+ $collection->setParent($this);
+ $collection->addPrefix($prefix);
+
+ // remove all routes with the same name in all existing collections
+ foreach (array_keys($collection->all()) as $name) {
+ $this->remove($name);
+ }
+
+ $this->routes[] = $collection;
+ }
+
+ /**
+ * Adds a prefix to all routes in the current set.
+ *
+ * @param string $prefix An optional prefix to add before each pattern of the route collection
+ *
+ * @api
+ */
+ public function addPrefix($prefix)
+ {
+ // a prefix must not end with a slash
+ $prefix = rtrim($prefix, '/');
+
+ if (!$prefix) {
+ return;
+ }
+
+ // a prefix must start with a slash
+ if ('/' !== $prefix[0]) {
+ $prefix = '/'.$prefix;
+ }
+
+ $this->prefix = $prefix.$this->prefix;
+
+ foreach ($this->routes as $name => $route) {
+ if ($route instanceof RouteCollection) {
+ $route->addPrefix($prefix);
+ } else {
+ $route->setPattern($prefix.$route->getPattern());
+ }
+ }
+ }
+
+ public function getPrefix()
+ {
+ return $this->prefix;
+ }
+
+ /**
+ * Returns an array of resources loaded to build this collection.
+ *
+ * @return ResourceInterface[] An array of resources
+ */
+ public function getResources()
+ {
+ $resources = $this->resources;
+ foreach ($this as $routes) {
+ if ($routes instanceof RouteCollection) {
+ $resources = array_merge($resources, $routes->getResources());
+ }
+ }
+
+ return array_unique($resources);
+ }
+
+ /**
+ * Adds a resource for this collection.
+ *
+ * @param ResourceInterface $resource A resource instance
+ */
+ public function addResource(ResourceInterface $resource)
+ {
+ $this->resources[] = $resource;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * RouteCompiler compiles Route instances to CompiledRoute instances.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class RouteCompiler implements RouteCompilerInterface
+{
+ /**
+ * Compiles the current route instance.
+ *
+ * @param Route $route A Route instance
+ *
+ * @return CompiledRoute A CompiledRoute instance
+ */
+ public function compile(Route $route)
+ {
+ $pattern = $route->getPattern();
+ $len = strlen($pattern);
+ $tokens = array();
+ $variables = array();
+ $pos = 0;
+ preg_match_all('#.\{([\w\d_]+)\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ if ($text = substr($pattern, $pos, $match[0][1] - $pos)) {
+ $tokens[] = array('text', $text);
+ }
+ $seps = array($pattern[$pos]);
+ $pos = $match[0][1] + strlen($match[0][0]);
+ $var = $match[1][0];
+
+ if ($req = $route->getRequirement($var)) {
+ $regexp = $req;
+ } else {
+ if ($pos !== $len) {
+ $seps[] = $pattern[$pos];
+ }
+ $regexp = sprintf('[^%s]+?', preg_quote(implode('', array_unique($seps)), '#'));
+ }
+
+ $tokens[] = array('variable', $match[0][0][0], $regexp, $var);
+
+ if (in_array($var, $variables)) {
+ throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $route->getPattern(), $var));
+ }
+
+ $variables[] = $var;
+ }
+
+ if ($pos < $len) {
+ $tokens[] = array('text', substr($pattern, $pos));
+ }
+
+ // find the first optional token
+ $firstOptional = INF;
+ for ($i = count($tokens) - 1; $i >= 0; $i--) {
+ $token = $tokens[$i];
+ if ('variable' === $token[0] && $route->hasDefault($token[3])) {
+ $firstOptional = $i;
+ } else {
+ break;
+ }
+ }
+
+ // compute the matching regexp
+ $regexp = '';
+ for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
+ $regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
+ }
+
+ return new CompiledRoute(
+ $route,
+ 'text' === $tokens[0][0] ? $tokens[0][1] : '',
+ sprintf("#^%s$#s", $regexp),
+ array_reverse($tokens),
+ $variables
+ );
+ }
+
+ /**
+ * Computes the regexp used to match the token.
+ *
+ * @param array $tokens The route tokens
+ * @param integer $index The index of the current token
+ * @param integer $firstOptional The index of the first optional token
+ *
+ * @return string The regexp
+ */
+ private function computeRegexp(array $tokens, $index, $firstOptional)
+ {
+ $token = $tokens[$index];
+ if ('text' === $token[0]) {
+ // Text tokens
+ return preg_quote($token[1], '#');
+ } else {
+ // Variable tokens
+ if (0 === $index && 0 === $firstOptional && 1 == count($tokens)) {
+ // When the only token is an optional variable token, the separator is required
+ return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], '#'), $token[3], $token[2]);
+ } else {
+ $nbTokens = count($tokens);
+ $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], '#'), $token[3], $token[2]);
+ if ($index >= $firstOptional) {
+ // Enclose each optional tokens in a subpattern to make it optional
+ $regexp = "(?:$regexp";
+ if ($nbTokens - 1 == $index) {
+ // Close the optional subpatterns
+ $regexp .= str_repeat(")?", $nbTokens - $firstOptional);
+ }
+ }
+
+ return $regexp;
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+/**
+ * RouteCompilerInterface is the interface that all RouteCompiler classes must implements.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RouteCompilerInterface
+{
+ /**
+ * Compiles the current route instance.
+ *
+ * @param Route $route A Route instance
+ *
+ * @return CompiledRoute A CompiledRoute instance
+ */
+ public function compile(Route $route);
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\ConfigCache;
+
+/**
+ * The Router class is an example of the integration of all pieces of the
+ * routing system for easier use.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Router implements RouterInterface
+{
+ protected $matcher;
+ protected $generator;
+ protected $defaults;
+ protected $context;
+ protected $loader;
+ protected $collection;
+ protected $resource;
+ protected $options;
+
+ /**
+ * Constructor.
+ *
+ * @param LoaderInterface $loader A LoaderInterface instance
+ * @param mixed $resource The main resource to load
+ * @param array $options An array of options
+ * @param RequestContext $context The context
+ * @param array $defaults The default values
+ */
+ public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, array $defaults = array())
+ {
+ $this->loader = $loader;
+ $this->resource = $resource;
+ $this->context = null === $context ? new RequestContext() : $context;
+ $this->defaults = $defaults;
+ $this->setOptions($options);
+ }
+
+ /**
+ * Sets options.
+ *
+ * Available options:
+ *
+ * * cache_dir: The cache directory (or null to disable caching)
+ * * debug: Whether to enable debugging or not (false by default)
+ * * resource_type: Type hint for the main resource (optional)
+ *
+ * @param array $options An array of options
+ *
+ * @throws \InvalidArgumentException When unsupported option is provided
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = array(
+ 'cache_dir' => null,
+ 'debug' => false,
+ 'generator_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ 'generator_base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ 'generator_dumper_class' => 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper',
+ 'generator_cache_class' => 'ProjectUrlGenerator',
+ 'matcher_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ 'matcher_base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ 'matcher_dumper_class' => 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper',
+ 'matcher_cache_class' => 'ProjectUrlMatcher',
+ 'resource_type' => null,
+ );
+
+ // check option names and live merge, if errors are encountered Exception will be thrown
+ $invalid = array();
+ $isInvalid = false;
+ foreach ($options as $key => $value) {
+ if (array_key_exists($key, $this->options)) {
+ $this->options[$key] = $value;
+ } else {
+ $isInvalid = true;
+ $invalid[] = $key;
+ }
+ }
+
+ if ($isInvalid) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('\', \'', $invalid)));
+ }
+ }
+
+ /**
+ * Sets an option.
+ *
+ * @param string $key The key
+ * @param mixed $value The value
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setOption($key, $value)
+ {
+ if (!array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * Gets an option value.
+ *
+ * @param string $key The key
+ *
+ * @return mixed The value
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getOption($key)
+ {
+ if (!array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ return $this->options[$key];
+ }
+
+ /**
+ * Gets the RouteCollection instance associated with this Router.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRouteCollection()
+ {
+ if (null === $this->collection) {
+ $this->collection = $this->loader->load($this->resource, $this->options['resource_type']);
+ }
+
+ return $this->collection;
+ }
+
+ /**
+ * Sets the request context.
+ *
+ * @param RequestContext $context The context
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+
+ $this->getMatcher()->setContext($context);
+ $this->getGenerator()->setContext($context);
+ }
+
+ /**
+ * Gets the request context.
+ *
+ * @return RequestContext The context
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * Generates a URL from the given parameters.
+ *
+ * @param string $name The name of the route
+ * @param mixed $parameters An array of parameters
+ * @param Boolean $absolute Whether to generate an absolute URL
+ *
+ * @return string The generated URL
+ */
+ public function generate($name, $parameters = array(), $absolute = false)
+ {
+ return $this->getGenerator()->generate($name, $parameters, $absolute);
+ }
+
+ /**
+ * Tries to match a URL with a set of routes.
+ *
+ * Returns false if no route matches the URL.
+ *
+ * @param string $url URL to be parsed
+ *
+ * @return array|false An array of parameters or false if no route matches
+ */
+ public function match($url)
+ {
+ return $this->getMatcher()->match($url);
+ }
+
+ /**
+ * Gets the UrlMatcher instance associated with this Router.
+ *
+ * @return UrlMatcherInterface A UrlMatcherInterface instance
+ */
+ public function getMatcher()
+ {
+ if (null !== $this->matcher) {
+ return $this->matcher;
+ }
+
+ if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) {
+ return $this->matcher = new $this->options['matcher_class']($this->getRouteCollection(), $this->context, $this->defaults);
+ }
+
+ $class = $this->options['matcher_cache_class'];
+ $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']);
+ if (!$cache->isFresh($class)) {
+ $dumper = new $this->options['matcher_dumper_class']($this->getRouteCollection());
+
+ $options = array(
+ 'class' => $class,
+ 'base_class' => $this->options['matcher_base_class'],
+ );
+
+ $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
+ }
+
+ require_once $cache;
+
+ return $this->matcher = new $class($this->context, $this->defaults);
+ }
+
+ /**
+ * Gets the UrlGenerator instance associated with this Router.
+ *
+ * @return UrlGeneratorInterface A UrlGeneratorInterface instance
+ */
+ public function getGenerator()
+ {
+ if (null !== $this->generator) {
+ return $this->generator;
+ }
+
+ if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) {
+ return $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->defaults);
+ }
+
+ $class = $this->options['generator_cache_class'];
+ $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']);
+ if (!$cache->isFresh($class)) {
+ $dumper = new $this->options['generator_dumper_class']($this->getRouteCollection());
+
+ $options = array(
+ 'class' => $class,
+ 'base_class' => $this->options['generator_base_class'],
+ );
+
+ $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
+ }
+
+ require_once $cache;
+
+ return $this->generator = new $class($this->context, $this->defaults);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * RouterInterface is the interface that all Router classes must implements.
+ *
+ * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface
+{
+}
--- /dev/null
+{
+ "name": "symfony/routing",
+ "type": "library",
+ "description": "Symfony Routing Component",
+ "keywords": [],
+ "homepage": "http://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "suggest": {
+ "symfony/config": "self.version",
+ "symfony/yaml": "self.version"
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Routing": "" }
+ },
+ "target-dir": "Symfony/Component/Routing"
+}
+++ /dev/null
-{
- "description": "ownCloud gives you universal access to your files/contacts/calendar through a web interface or WebDAV.",
- "homepage": "http://owncloud.org",
- "license": "AGPL-3.0+",
- "support": {
- "email": "owncloud@kde.org",
- "irc": "irc://irc.freenode.org/owncloud",
- "forum": "http://forum.owncloud.org/",
- "issues": "https://github.com/owncloud/core/issues"
- },
- "require": {
- "php": ">=5.3.2",
- "symfony/routing": "2.0.*"
- },
- "config": {
- "vendor-dir": "3rdparty"
- }
-}