diff --git a/src/Symfony/Component/Routing/Annotation/Route.php b/src/Symfony/Component/Routing/Annotation/Route.php new file mode 100644 index 0000000000..8964fa888a --- /dev/null +++ b/src/Symfony/Component/Routing/Annotation/Route.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Annotation class for @Route(). + * + * @author Fabien Potencier + */ +class Route +{ + protected $pattern; + protected $name; + protected $requirements; + protected $options; + + /** + * Constructor. + * + * @param array $data An array of key/value parameters. + */ + public function __construct(array $data) + { + $this->requirements = array(); + $this->options = 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; + } +} diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php new file mode 100644 index 0000000000..cc93d34978 --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php @@ -0,0 +1,162 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * 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 + */ +abstract class AnnotationClassLoader implements LoaderInterface +{ + protected $reader; + + /** + * Constructor. + */ + public function __construct(AnnotationReader $reader) + { + $this->reader = $reader; + } + + /** + * Loads from annotations from a class. + * + * @param string $class A class name + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load($class) + { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + $class = new \ReflectionClass($class); + class_exists($annotClass = 'Symfony\\Component\\Routing\\Annotation\\Route'); + + $globals = array( + 'pattern' => '', + 'requirements' => array(), + 'options' => array(), + ); + + if ($annot = $this->reader->getClassAnnotation($class, $annotClass)) { + if (null !== $annot->getPattern()) { + $globals['pattern'] = $annot->getPattern(); + } + + if (null !== $annot->getRequirements()) { + $globals['requirements'] = $annot->getRequirements(); + } + + if (null !== $annot->getOptions()) { + $globals['options'] = $annot->getOptions(); + } + } + + $this->reader->setDefaultAnnotationNamespace('Symfony\\Component\\Routing\\Annotation\\'); + $collection = new RouteCollection(); + foreach ($class->getMethods() as $method) { + if ($annot = $this->reader->getMethodAnnotation($method, $annotClass)) { + if (null === $annot->getName()) { + $annot->setName($this->getDefaultRouteName($class, $method)); + } + + $defaults = $this->getRouteDefaults($class, $method, $annot); + $requirements = array_merge($globals['requirements'], $annot->getRequirements()); + $options = array_merge($globals['options'], $annot->getOptions()); + + $route = new Route($globals['pattern'].$annot->getPattern(), $defaults, $requirements, $options); + $collection->addRoute($annot->getName(), $route); + } + } + + return $collection; + } + + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + return strtolower(str_replace('\\', '_', $class->getName()).'_'.$method->getName()); + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * + * @return Boolean true if this class supports the given resource, false otherwise + */ + public function supports($resource) + { + return is_string($resource) && class_exists($resource); + } + + /** + * 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() + { + } + + abstract protected function getRouteDefaults(\ReflectionClass $class, \ReflectionMethod $method, RouteAnnotation $annot); +} diff --git a/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php new file mode 100644 index 0000000000..89822d76a7 --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php @@ -0,0 +1,65 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * AnnotationDirectoryLoader loads routing information from annotations set + * on PHP classes and methods. + * + * @author Fabien Potencier + */ +class AnnotationDirectoryLoader extends AnnotationFileLoader +{ + /** + * Loads from annotations from a directory. + * + * @param string $resource A directory prefixed with annotations: + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load($resource) + { + $dir = $this->getAbsolutePath(substr($resource, 12)); + if (!file_exists($dir)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist (in: %s).', $dir, implode(', ', $this->paths))); + } + + $collection = new RouteCollection(); + 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)) { + $collection->addCollection($this->loader->load($class)); + } + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * + * @return Boolean true if this class supports the given resource, false otherwise + */ + public function supports($resource) + { + return is_string($resource) && 0 === strpos($resource, 'annotations:') && is_dir($this->getAbsolutePath(substr($resource, 12))); + } +} diff --git a/src/Symfony/Component/Routing/Loader/AnnotationFileLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationFileLoader.php new file mode 100644 index 0000000000..3235c9522f --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/AnnotationFileLoader.php @@ -0,0 +1,113 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * AnnotationFileLoader loads routing information from annotations set + * on a PHP class and its methods. + * + * @author Fabien Potencier + */ +class AnnotationFileLoader extends FileLoader +{ + protected $loader; + + /** + * Constructor. + * + * @param string|array $paths A path or an array of paths where to look for resources + */ + public function __construct(AnnotationClassLoader $loader, $paths = array()) + { + if (!function_exists('token_get_all')) { + throw new \RuntimeException('The Tokenizer extension is needed for the routing annotation loaders.'); + } + + parent::__construct($paths); + + $this->loader = $loader; + } + + /** + * Loads from annotations from a file. + * + * @param string $resource A directory prefixed with annotations: + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load($resource) + { + $path = $this->getAbsolutePath(substr($resource, 12)); + if (!file_exists($path)) { + throw new \InvalidArgumentException(sprintf('The file "%s" cannot be found (in: %s).', $resource, implode(', ', $this->paths))); + } + + $collection = new RouteCollection(); + if ($class = $this->findClass($path)) { + $collection->addResource(new FileResource($path)); + $collection->addCollection($this->loader->load($class)); + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * + * @return Boolean true if this class supports the given resource, false otherwise + */ + public function supports($resource) + { + return 0 === strpos($resource, 'annotations:') && is_file($this->getAbsolutePath(substr($resource, 12))); + } + + protected function findClass($file) + { + $class = false; + $namespace = false; + $tokens = token_get_all(file_get_contents($file)); + while ($token = array_shift($tokens)) { + 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 = array_shift($tokens); + } while ($tokens && 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; + } +} diff --git a/src/Symfony/Component/Routing/Loader/AnnotationGlobLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationGlobLoader.php new file mode 100644 index 0000000000..ce10551454 --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/AnnotationGlobLoader.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * AnnotationGlobLoader loads routing information from annotations set + * on PHP classes and methods. + * + * @author Fabien Potencier + */ +class AnnotationGlobLoader extends AnnotationDirectoryLoader +{ + /** + * Loads from annotations from an array of directories. + * + * @param array $resource An array of directories prefixed with annotations: + * + * @return RouteCollection A RouteCollection instance + * + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load($resource) + { + $collection = new RouteCollection(); + foreach ($this->getAbsolutePaths(substr($resource, 12)) as $resource) { + $collection->addCollection(parent::load('annotations:'.$resource)); + } + + return $collection; + } + + /** + * Returns true if this class supports the given resource. + * + * @param mixed $resource A resource + * + * @return Boolean true if this class supports the given resource, false otherwise + */ + public function supports($resource) + { + return is_string($resource) && 0 === strpos($resource, 'annotations:') && false !== strpos($resource, '*'); + } + + protected function getAbsolutePaths($glob) + { + $dirs = array(); + foreach ($this->paths as $path) { + if (false !== $d = glob($path.DIRECTORY_SEPARATOR.$glob, GLOB_ONLYDIR | GLOB_BRACE)) { + $dirs = array_merge($dirs, $d); + } + } + + return $dirs; + } +}