diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/AnnotationAdapterInterface.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/AnnotationAdapterInterface.php new file mode 100644 index 0000000000000..53edd7f8f15ef --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/AnnotationAdapterInterface.php @@ -0,0 +1,33 @@ + + */ +interface AnnotationAdapterInterface +{ + /** + * The identifier for the annotation. + * + * @return string + */ + public function getIdentifier(); + + /** + * The actual annotation found. + * + * @return mixed + */ + public function getAnnotation(); + + /** + * Indicates whether the given annotation may be used multiple times on a given method or class. + * + * @return bool + */ + public function allowMultiple(); +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/ConfigurationAnnotationAdapter.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/ConfigurationAnnotationAdapter.php new file mode 100644 index 0000000000000..4dba98051b5e8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/ConfigurationAnnotationAdapter.php @@ -0,0 +1,52 @@ + + */ +final class ConfigurationAnnotationAdapter implements AnnotationAdapterInterface +{ + /** + * @var ConfigurationAnnotation + */ + private $annotation; + + /** + * @param ConfigurationAnnotation $annotation + */ + public function __construct(ConfigurationAnnotation $annotation) + { + $this->annotation = $annotation; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() + { + return $this->annotation->getAliasName(); + } + + /** + * {@inheritdoc} + * + * @returns ConfigurationAnnotation + */ + public function getAnnotation() + { + return $this->annotation; + } + + /** + * {@inheritdoc} + */ + public function allowMultiple() + { + return $this->annotation->allowArray(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/RouteAnnotationAdapter.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/RouteAnnotationAdapter.php new file mode 100644 index 0000000000000..1547865f69234 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/RouteAnnotationAdapter.php @@ -0,0 +1,54 @@ + + */ +final class RouteAnnotationAdapter implements AnnotationAdapterInterface +{ + /** + * @var Route + */ + private $annotation; + + /** + * @param Route $annotation + */ + public function __construct(Route $annotation) + { + $this->annotation = $annotation; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() + { + return $this->annotation->getName(); + } + + /** + * {@inheritdoc} + * + * @returns Route + */ + public function getAnnotation() + { + return $this->annotation; + } + + /** + * {@inheritdoc} + * + * @return bool always true + */ + public function allowMultiple() + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/ClassMetadata.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/ClassMetadata.php new file mode 100644 index 0000000000000..038f8aa102908 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/ClassMetadata.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\AnnotationAdapterInterface; + +/** + * Responsible for storing metadata of a controller. + * + * @author Iltar van der Berg + */ +final class ClassMetadata implements \Serializable +{ + /** + * @var string + */ + private $className; + + /** + * @var AnnotationAdapterInterface[] + */ + private $annotations; + + /** + * @var array + */ + private $methods; + + /** + * @param string $className + * @param MethodMetadata[] $methods + * @param AnnotationAdapterInterface[] $annotations + */ + public function __construct($className, array $methods = [], array $annotations = []) + { + $this->className = $className; + $this->methods = $methods; + $this->annotations = $annotations; + } + + public function getClassName() + { + return $this->className; + } + + public function getAnnotations() + { + return $this->annotations; + } + + public function serialize() + { + return serialize(array($this->className, $this->methods, $this->annotations)); + } + + public function unserialize($serialized) + { + list($this->className, $this->methods, $this->annotations) = unserialize($serialized); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Cache.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Cache.php new file mode 100644 index 0000000000000..97e2807268571 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Cache.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * The Cache class handles the Cache annotation parts. + * + * @author Fabien Potencier + * @Annotation + */ +class Cache extends ConfigurationAnnotation +{ + /** + * The expiration date as a valid date for the strtotime() function. + * + * @var string + */ + protected $expires; + + /** + * The number of seconds that the response is considered fresh by a private + * cache like a web browser. + * + * @var int + */ + protected $maxage; + + /** + * The number of seconds that the response is considered fresh by a public + * cache like a reverse proxy cache. + * + * @var int + */ + protected $smaxage; + + /** + * Whether the response is public or not. + * + * @var bool + */ + protected $public; + + /** + * Additional "Vary:"-headers. + * + * @var array + */ + protected $vary; + + /** + * An expression to compute the Last-Modified HTTP header. + * + * @var string + */ + protected $lastModified; + + /** + * An expression to compute the ETag HTTP header. + * + * @var string + */ + protected $etag; + + /** + * Returns the expiration date for the Expires header field. + * + * @return string + */ + public function getExpires() + { + return $this->expires; + } + + /** + * Sets the expiration date for the Expires header field. + * + * @param string $expires A valid php date + */ + public function setExpires($expires) + { + $this->expires = $expires; + } + + /** + * Sets the number of seconds for the max-age cache-control header field. + * + * @param int $maxage A number of seconds + */ + public function setMaxAge($maxage) + { + $this->maxage = $maxage; + } + + /** + * Returns the number of seconds the response is considered fresh by a + * private cache. + * + * @return int + */ + public function getMaxAge() + { + return $this->maxage; + } + + /** + * Sets the number of seconds for the s-maxage cache-control header field. + * + * @param int $smaxage A number of seconds + */ + public function setSMaxAge($smaxage) + { + $this->smaxage = $smaxage; + } + + /** + * Returns the number of seconds the response is considered fresh by a + * public cache. + * + * @return int + */ + public function getSMaxAge() + { + return $this->smaxage; + } + + /** + * Returns whether or not a response is public. + * + * @return bool + */ + public function isPublic() + { + return $this->public === true; + } + + /** + * Returns whether or not a response is private. + * + * @return bool + */ + public function isPrivate() + { + return $this->public === false; + } + + /** + * Sets a response public. + * + * @param bool $public A boolean value + */ + public function setPublic($public) + { + $this->public = (bool) $public; + } + + /** + * Returns the custom "Vary"-headers. + * + * @return array + */ + public function getVary() + { + return $this->vary; + } + + /** + * Add additional "Vary:"-headers. + * + * @param array $vary + */ + public function setVary($vary) + { + $this->vary = $vary; + } + + /** + * Sets the "Last-Modified"-header expression. + * + * @param string $expression + */ + public function setLastModified($expression) + { + $this->lastModified = $expression; + } + + /** + * Returns the "Last-Modified"-header expression. + * + * @return string + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets the "ETag"-header expression. + * + * @param string $expression + */ + public function setETag($expression) + { + $this->etag = $expression; + } + + /** + * Returns the "ETag"-header expression. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Returns the annotation alias name. + * + * @return string + * + * @see ConfigurationInterface + */ + public function getAliasName() + { + return 'cache'; + } + + /** + * Only one cache directive is allowed. + * + * @return bool + * + * @see ConfigurationInterface + */ + public function allowArray() + { + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationAnnotation.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationAnnotation.php new file mode 100644 index 0000000000000..b2edc0e743a3f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationAnnotation.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * Base configuration annotation. + * + * @author Johannes M. Schmitt + */ +abstract class ConfigurationAnnotation implements ConfigurationInterface +{ + public function __construct(array $values) + { + foreach ($values as $k => $v) { + if (!method_exists($this, $name = 'set'.$k)) { + throw new \RuntimeException(sprintf('Unknown key "%s" for annotation "@%s".', $k, get_class($this))); + } + + $this->$name($v); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationInterface.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationInterface.php new file mode 100644 index 0000000000000..cccf7bd9f5cf6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * ConfigurationInterface. + * + * @author Fabien Potencier + */ +interface ConfigurationInterface +{ + /** + * Returns the alias name for an annotated configuration. + * + * @return string + */ + public function getAliasName(); + + /** + * Returns whether multiple annotations of this type are allowed. + * + * @return bool + */ + public function allowArray(); +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Method.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Method.php new file mode 100644 index 0000000000000..3dbc2246d984b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Method.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * The Method class handles the Method annotation parts. + * + * @author Fabien Potencier + * @Annotation + */ +class Method extends ConfigurationAnnotation +{ + /** + * An array of restricted HTTP methods. + * + * @var array + */ + protected $methods = array(); + + /** + * Returns the array of HTTP methods. + * + * @return array + */ + public function getMethods() + { + return $this->methods; + } + + /** + * Sets the HTTP methods. + * + * @param array|string $methods An HTTP method or an array of HTTP methods + */ + public function setMethods($methods) + { + $this->methods = is_array($methods) ? $methods : array($methods); + } + + /** + * Sets the HTTP methods. + * + * @param array|string $methods An HTTP method or an array of HTTP methods + */ + public function setValue($methods) + { + $this->setMethods($methods); + } + + /** + * Returns the annotation alias name. + * + * @return string + * + * @see ConfigurationInterface + */ + public function getAliasName() + { + return 'method'; + } + + /** + * Only one method directive is allowed. + * + * @return bool + * + * @see ConfigurationInterface + */ + public function allowArray() + { + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ParamConverter.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ParamConverter.php new file mode 100644 index 0000000000000..ba343da145e2a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ParamConverter.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * The ParamConverter class handles the ParamConverter annotation parts. + * + * @author Fabien Potencier + * @Annotation + */ +class ParamConverter extends ConfigurationAnnotation +{ + /** + * The parameter name. + * + * @var string + */ + protected $name; + + /** + * The parameter class. + * + * @var string + */ + protected $class; + + /** + * An array of options. + * + * @var array + */ + protected $options = array(); + + /** + * Whether or not the parameter is optional. + * + * @var bool + */ + protected $optional = false; + + /** + * Use explicitly named converter instead of iterating by priorities. + * + * @var string + */ + protected $converter; + + /** + * Returns the parameter name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the parameter name. + * + * @param string $name The parameter name + */ + public function setValue($name) + { + $this->setName($name); + } + + /** + * Sets the parameter name. + * + * @param string $name The parameter name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the parameter class name. + * + * @return string $name + */ + public function getClass() + { + return $this->class; + } + + /** + * Sets the parameter class name. + * + * @param string $class The parameter class name + */ + public function setClass($class) + { + $this->class = $class; + } + + /** + * Returns an array of options. + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets an array of options. + * + * @param array $options An array of options + */ + public function setOptions($options) + { + $this->options = $options; + } + + /** + * Sets whether or not the parameter is optional. + * + * @param bool $optional Whether the parameter is optional + */ + public function setIsOptional($optional) + { + $this->optional = (bool) $optional; + } + + /** + * Returns whether or not the parameter is optional. + * + * @return bool + */ + public function isOptional() + { + return $this->optional; + } + + /** + * Get explicit converter name. + * + * @return string + */ + public function getConverter() + { + return $this->converter; + } + + /** + * Set explicit converter name. + * + * @param string $converter + */ + public function setConverter($converter) + { + $this->converter = $converter; + } + + /** + * Returns the annotation alias name. + * + * @return string + * + * @see ConfigurationInterface + */ + public function getAliasName() + { + return 'converters'; + } + + /** + * Multiple ParamConverters are allowed. + * + * @return bool + * + * @see ConfigurationInterface + */ + public function allowArray() + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Route.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Route.php new file mode 100644 index 0000000000000..be345c08bd9a5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Route.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +use Symfony\Component\Routing\Annotation\Route as BaseRoute; + +/** + * @author Kris Wallsmith + * @Annotation + */ +class Route extends BaseRoute +{ + protected $service; + + public function setService($service) + { + // avoid a BC notice in case of @Route(service="") with sf ^2.7 + if (null === $this->getPath()) { + $this->setPath(''); + } + $this->service = $service; + } + + public function getService() + { + return $this->service; + } + + /** + * Multiple route annotations are allowed. + * + * @return bool + * + * @see ConfigurationInterface + */ + public function allowArray() + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Security.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Security.php new file mode 100644 index 0000000000000..92fba963b4ecc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Security.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +/** + * The Security class handles the Security annotation. + * + * @author Fabien Potencier + * @Annotation + */ +class Security extends ConfigurationAnnotation +{ + protected $expression; + + public function getExpression() + { + return $this->expression; + } + + public function setExpression($expression) + { + $this->expression = $expression; + } + + public function setValue($expression) + { + $this->setExpression($expression); + } + + public function getAliasName() + { + return 'security'; + } + + public function allowArray() + { + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Template.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Template.php new file mode 100644 index 0000000000000..dad8d55637068 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Template.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration; + +use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; + +/** + * The Template class handles the Template annotation parts. + * + * @author Fabien Potencier + * @Annotation + */ +class Template extends ConfigurationAnnotation +{ + /** + * The template reference. + * + * @var TemplateReference|string + */ + protected $template; + + /** + * The template engine used when a specific template isn't specified. + * + * @var string + */ + protected $engine = 'twig'; + + /** + * The associative array of template variables. + * + * @var array + */ + protected $vars = array(); + + /** + * Should the template be streamed? + * + * @var bool + */ + protected $streamable = false; + + /** + * The controller (+action) this annotation is set to. + * + * @var array + */ + private $owner; + + /** + * Returns the array of templates variables. + * + * @return array + */ + public function getVars() + { + return $this->vars; + } + + /** + * @param bool $streamable + */ + public function setIsStreamable($streamable) + { + $this->streamable = $streamable; + } + + /** + * @return bool + */ + public function isStreamable() + { + return (bool) $this->streamable; + } + + /** + * Sets the template variables. + * + * @param array $vars The template variables + */ + public function setVars($vars) + { + $this->vars = $vars; + } + + /** + * Returns the engine used when guessing template names. + * + * @return string + */ + public function getEngine() + { + return $this->engine; + } + + /** + * Sets the engine used when guessing template names. + * + * @param string + */ + public function setEngine($engine) + { + $this->engine = $engine; + } + + /** + * Sets the template logic name. + * + * @param string $template The template logic name + */ + public function setValue($template) + { + $this->setTemplate($template); + } + + /** + * Returns the template reference. + * + * @return TemplateReference + */ + public function getTemplate() + { + return $this->template; + } + + /** + * Sets the template reference. + * + * @param TemplateReference|string $template The template reference + */ + public function setTemplate($template) + { + $this->template = $template; + } + + /** + * Returns the annotation alias name. + * + * @return string + * + * @see ConfigurationInterface + */ + public function getAliasName() + { + return 'template'; + } + + /** + * Only one template directive is allowed. + * + * @return bool + * + * @see ConfigurationInterface + */ + public function allowArray() + { + return false; + } + + /** + * @param array $owner + */ + public function setOwner(array $owner) + { + $this->owner = $owner; + } + + /** + * The controller (+action) this annotation is attached to. + * + * @return array + */ + public function getOwner() + { + return $this->owner; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/NoMatchingFactoryFoundException.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/NoMatchingFactoryFoundException.php new file mode 100644 index 0000000000000..37c77adf47004 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/NoMatchingFactoryFoundException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception; + +/** + * @author Iltar van der Berg + */ +class NoMatchingFactoryFoundException extends \InvalidArgumentException +{ + public function __construct($annotationClass) + { + parent::__construct(sprintf('No matching AnnotationAdapterFactory found for %s.', $annotationClass)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/UnsupportedAnnotationException.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/UnsupportedAnnotationException.php new file mode 100644 index 0000000000000..e66259f624d8d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/UnsupportedAnnotationException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception; + +/** + * @author Iltar van der Berg + */ +class UnsupportedAnnotationException extends \InvalidArgumentException +{ + public function __construct($factoryClass, $annotationClass) + { + parent::__construct(sprintf('%s only accepts %s annotations.', $factoryClass, $annotationClass)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/AnnotationAdapterFactoryInterface.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/AnnotationAdapterFactoryInterface.php new file mode 100644 index 0000000000000..a89351556e153 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/AnnotationAdapterFactoryInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Factory; + +use Doctrine\Common\Annotations\Annotation; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\AnnotationAdapterInterface; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception\UnsupportedAnnotationException; + +/** + * Responsible for adapter creation. + * + * @author Iltar van der Berg + */ +interface AnnotationAdapterFactoryInterface +{ + /** + * @param mixed $annotation + * @return AnnotationAdapterInterface + * + * @throws UnsupportedAnnotationException + */ + public function createForAnnotation($annotation); +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ChainAnnotationAdapterFactory.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ChainAnnotationAdapterFactory.php new file mode 100644 index 0000000000000..136332f8e4331 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ChainAnnotationAdapterFactory.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Factory; + +use Doctrine\Common\Annotations\Annotation; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\AnnotationAdapterInterface; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception\NoMatchingFactoryFoundException; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception\UnsupportedAnnotationException; + +/** + * Allows different factories to try and create adapters for annotations. + * + * @author Iltar van der Berg + */ +final class ChainAnnotationAdapterFactory implements AnnotationAdapterFactoryInterface +{ + /** + * @var AnnotationAdapterFactoryInterface[] + */ + private $factories; + + /** + * @param AnnotationAdapterFactoryInterface[] $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + /** + * Checks all registered annotation adapter factories until one is found that supports this annotation. + * + * @param mixed $annotation + * @return AnnotationAdapterInterface + * + * @throws NoMatchingFactoryFoundException + */ + public function createForAnnotation($annotation) + { + foreach ($this->factories as $factory) { + try { + return $factory->createForAnnotation($annotation); + } catch (UnsupportedAnnotationException $e) { + continue; + } + } + + throw new NoMatchingFactoryFoundException(get_class($annotation)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ConfigurationAnnotationAdapterFactory.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ConfigurationAnnotationAdapterFactory.php new file mode 100644 index 0000000000000..412b8e4d574ef --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ConfigurationAnnotationAdapterFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Factory; + +use Doctrine\Common\Annotations\Annotation; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\ConfigurationAnnotationAdapter; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\ConfigurationAnnotation; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception\UnsupportedAnnotationException; + +/** + * Responsible for adapter creation for the SensioFrameworkExtraBundle. + * + * @author Iltar van der Berg + */ +final class ConfigurationAnnotationAdapterFactory implements AnnotationAdapterFactoryInterface +{ + public function createForAnnotation($annotation) + { + if (!$annotation instanceof ConfigurationAnnotation) { + throw new UnsupportedAnnotationException(__CLASS__, get_class($annotation)); + } + + return new ConfigurationAnnotationAdapter($annotation); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/RouteAnnotationAdapterFactory.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/RouteAnnotationAdapterFactory.php new file mode 100644 index 0000000000000..a9af51671b8e5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/RouteAnnotationAdapterFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata\Factory; + +use Doctrine\Common\Annotations\Annotation; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\RouteAnnotationAdapter; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Exception\NoMatchingFactoryFoundException; +use Symfony\Component\Routing\Annotation\Route; + +/** + * Responsible for adapter creation of the Symfony Route annotation. + * + * @author Iltar van der Berg + */ +final class RouteAnnotationAdapterFactory implements AnnotationAdapterFactoryInterface +{ + public function createForAnnotation($annotation) + { + if (!$annotation instanceof Route) { + throw new NoMatchingFactoryFoundException(__CLASS__, Route::class); + } + + return new RouteAnnotationAdapter($annotation); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/MethodMetadata.php b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/MethodMetadata.php new file mode 100644 index 0000000000000..9400d7b13119b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/MethodMetadata.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Adapter\AnnotationAdapterInterface; + +/** + * Responsible for storing metadata of a controller method. + * + * @author Iltar van der Berg + */ +final class MethodMetadata implements \Serializable +{ + /** + * @var string + */ + private $methodName; + + /** + * @var AnnotationAdapterInterface[] + */ + private $annotations; + + /** + * @param string $methodName + * @param AnnotationAdapterInterface[] $annotations + */ + public function __construct($methodName, array $annotations = []) + { + $this->methodName = $methodName; + $this->annotations = $annotations; + } + + public function getMethodName() + { + return $this->methodName; + } + + public function getAnnotations() + { + return $this->annotations; + } + + public function serialize() + { + return serialize(array($this->methodName, $this->annotations)); + } + + public function unserialize($serialized) + { + list($this->methodName, $this->annotations) = unserialize($serialized); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/ClassMetadataTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/ClassMetadataTest.php new file mode 100644 index 0000000000000..8659439871212 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/ClassMetadataTest.php @@ -0,0 +1,242 @@ +getClassAnnotations($c); + $total += count($annotations[$controller]); + foreach ($c->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $annotations[$method->getName()] = $reader->getMethodAnnotations($method); + $total += count($annotations[$method->getName()]); + } + } + $data[] = $annotations; + } + + $after = microtime(true); + $time = round(($after - $before) * 1000); + $pr = round($time / $times, 2); + echo " * Executed $times times: $time ms\n - 5 controllers\n - $total annotations total\n - $pr ms/request\n\n"; + } + + public function testOldMany() + { + echo "\nRunning current situation with many controllers, annotations and reflection loaded each request\n"; + $times = 10; + $controllerCount = 100; + $before = microtime(true); + + AnnotationRegistry::registerLoader('class_exists'); + $reader = new AnnotationReader(); + + $controllers = [ + InvokableClassLevelController::class, + InvokableContainerController::class, + InvokableController::class, + MultipleActionsClassLevelTemplateController::class, + SimpleController::class, + ]; + + $total = 0; + + for ($j = 0; $j < $controllerCount; $j++) { + $data = []; + for ($i = 0; $i < $times; $i++) { + $annotations = []; + foreach ($controllers as $controller) { + $c = new \ReflectionClass($controller); + $annotations[$controller] = $reader->getClassAnnotations($c); + $total += count($annotations[$controller]); + foreach ($c->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $annotations[$method->getName()] = $reader->getMethodAnnotations($method); + $total += count($annotations[$method->getName()]); + } + } + $data[] = $annotations; + } + } + + $after = microtime(true); + $time = round(($after - $before) * 1000); + $c = count($controllers) * $controllerCount; + $pr = round($time / $times, 2); + $sr = round($pr / $c, 2); + echo " * Executed $times times: $time ms\n - $c controllers\n - $total annotations total\n - $pr ms/request\n - $sr ms/sub-request\n\n"; + } + + public function testBootstrapFewControllers() + { + echo "\nRunning new situation with a few controllers, annotations and reflection loaded during cache warmup\n"; + $before = microtime(true); + + $reader = new AnnotationReader(); + + $f1 = new ConfigurationAnnotationAdapterFactory(); + $f2 = new RouteAnnotationAdapterFactory([$f1]); + $f3 = new ChainAnnotationAdapterFactory([$f1, $f2]); + + $controllers = [ + InvokableClassLevelController::class, + InvokableContainerController::class, + InvokableController::class, + MultipleActionsClassLevelTemplateController::class, + SimpleController::class, + ]; + + $data = []; + $total = 0; + + foreach ($controllers as $controller) { + $classAnnotations = []; + $c = new \ReflectionClass($controller); + foreach ($reader->getClassAnnotations($c) as $annotation) { + $classAnnotations[] = $f3->createForAnnotation($annotation); + } + $total += count($classAnnotations); + $methods = []; + foreach ($c->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $methodAnnotations = []; + foreach ($reader->getMethodAnnotations($method) as $annotation) { + $methodAnnotations[] = $f3->createForAnnotation($annotation); + } + $total += count($methodAnnotations); + $methods[] = new MethodMetadata($method->getName(), $methodAnnotations); + } + $data[] = new ClassMetadata($c->getName(), $methods, $classAnnotations); + } + + file_put_contents(__DIR__.'/dump_few.serialized', serialize($data)); + + $after = microtime(true); + $time = round(($after - $before) * 1000); + echo " * bootstrap executed in $time ms\n - 4 controllers\n - $total annotations\n"; + } + + /** + * @depends testBootstrapFewControllers + */ + public function testNewFewControllers() + { + $times = 1000; + $before = microtime(true); + $data = []; + + for ($i = 0; $i < $times; $i++) { + $data = unserialize(file_get_contents(__DIR__.'/dump_few.serialized')); + } + + $after = microtime(true); + $time = round(($after - $before) * 1000); + $c = count($data); + $pr = round($time / $times, 2); + echo " * Executed $times times: $time ms\n - $c controllers\n - $pr ms/request\n"; + } + + + public function testBootstrapManyControllers() + { + echo "\n\nRunning new situation with many controllers, annotations and reflection loaded during cache warmup\n"; + $before = microtime(true); + + $reader = new AnnotationReader(); + + $f1 = new ConfigurationAnnotationAdapterFactory(); + $f2 = new RouteAnnotationAdapterFactory([$f1]); + $f3 = new ChainAnnotationAdapterFactory([$f1, $f2]); + + $controllers = [ + InvokableClassLevelController::class, + InvokableContainerController::class, + InvokableController::class, + MultipleActionsClassLevelTemplateController::class, + SimpleController::class, + ]; + + $data = []; + $total = 0; + + for ($i = 0; $i < 100; $i++) + foreach ($controllers as $controller) { + $classAnnotations = []; + $c = new \ReflectionClass($controller); + foreach ($reader->getClassAnnotations($c) as $annotation) { + $classAnnotations[] = $f3->createForAnnotation($annotation); + } + $total += count($classAnnotations); + $methods = []; + foreach ($c->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $methodAnnotations = []; + foreach ($reader->getMethodAnnotations($method) as $annotation) { + $methodAnnotations[] = $f3->createForAnnotation($annotation); + } + $total += count($methodAnnotations); + $methods[] = new MethodMetadata($method->getName(), $methodAnnotations); + } + $data[] = new ClassMetadata($c->getName(), $methods, $classAnnotations); + } + + + file_put_contents(__DIR__.'/dump_many.serialized', serialize($data)); + + $after = microtime(true); + $time = round(($after - $before) * 1000); + $c = $i * 5; + echo " * new bootstrap executed in $time ms\n - $c controllers\n - $total annotations\n"; + } + + /** + * @depends testBootstrapManyControllers + */ + public function testNewManyControllers() + { + $times = 1000; + $before = microtime(true); + + $data = []; + for ($i = 0; $i < $times; $i++) { + $data = unserialize(file_get_contents(__DIR__.'/dump_many.serialized')); + } + + $after = microtime(true); + $time = round(($after - $before) * 1000); + $c = count($data); + $pr = round($time / $times, 2); + echo " * New Executed $times times: $time ms\n - $c controllers\n - $pr ms/request\n"; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableClassLevelController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableClassLevelController.php new file mode 100644 index 0000000000000..ebc5e29bb316c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableClassLevelController.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Template; + +/** + * @Route(service="test.invokable_class_level.predefined") + * @Template("FooBundle:Invokable:predefined.html.twig") + */ +class InvokableClassLevelController +{ + /** + * @Route("/invokable/class-level/service/") + */ + public function __invoke() + { + return array( + 'foo' => 'bar', + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableContainerController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableContainerController.php new file mode 100644 index 0000000000000..3931d651af452 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableContainerController.php @@ -0,0 +1,61 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Template; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; + +class InvokableContainerController extends Controller +{ + /** + * @Route("/invokable/variable/container/{variable}/") + * @Template() + */ + public function variableAction($variable) + { + } + + /** + * @Route("/invokable/another-variable/container/{variable}/") + * @Template("FooBundle:InvokableContainer:variable.html.twig") + */ + public function anotherVariableAction($variable) + { + return array( + 'variable' => $variable, + ); + } + + /** + * @Route("/invokable/variable/container/{variable}/{another_variable}/") + * @Template("FooBundle:InvokableContainer:another_variable.html.twig") + */ + public function doubleVariableAction($variable, $another_variable) + { + return array( + 'variable' => $variable, + 'another_variable' => $another_variable, + ); + } + + /** + * @Route("/invokable/predefined/container/") + * @Template("FooBundle:Invokable:predefined.html.twig") + */ + public function __invoke() + { + return array( + 'foo' => 'bar', + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableController.php new file mode 100644 index 0000000000000..e869ccee15465 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableController.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Template; + +/** + * @Route(service="test.invokable.predefined") + */ +class InvokableController +{ + /** + * @Route("/invokable/predefined/service/") + * @Template("FooBundle:Invokable:predefined.html.twig") + */ + public function __invoke() + { + return array( + 'foo' => 'bar', + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/MultipleActionsClassLevelTemplateController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/MultipleActionsClassLevelTemplateController.php new file mode 100644 index 0000000000000..2420737638550 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/MultipleActionsClassLevelTemplateController.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Template; +use Symfony\Bundle\FrameworkBundle\Controller\Controller; + +/** + * @Template("FooBundle:Invokable:predefined.html.twig") + */ +class MultipleActionsClassLevelTemplateController extends Controller +{ + /** + * @Route("/multi/one-template/1/") + */ + public function firstAction() + { + return array( + 'foo' => 'bar', + ); + } + + /** + * @Route("/multi/one-template/2/") + * @Route("/multi/one-template/3/") + */ + public function secondAction() + { + return array( + 'foo' => 'bar', + ); + } + + /** + * @Route("/multi/one-template/4/") + * @Template("FooBundle::overwritten.html.twig") + */ + public function overwriteAction() + { + return array( + 'foo' => 'foo bar baz', + ); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/SimpleController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/SimpleController.php new file mode 100644 index 0000000000000..85807872e993f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/SimpleController.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. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\ControllerMetadata; + +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Route; +use Symfony\Bundle\FrameworkBundle\ControllerMetadata\Configuration\Template; +use Symfony\Component\HttpFoundation\Response; + +/** + * @Route(service="test.simple.multiple") + */ +class SimpleController +{ + /** + * @Route("/simple/multiple/", defaults={"a": "a", "b": "b"}) + * @Template() + */ + public function someAction($a, $b, $c = 'c') + { + } + + /** + * @Route("/simple/multiple/{a}/{b}/") + * @Template("FooBundle:Simple:some.html.twig") + */ + public function someMoreAction($a, $b, $c = 'c') + { + } + + /** + * @Route("/simple/multiple-with-vars/", defaults={"a": "a", "b": "b"}) + * @Template(vars={"a", "b"}) + */ + public function anotherAction($a, $b, $c = 'c') + { + } + + /** + * @Route("/no-listener/") + */ + public function noListenerAction() + { + return new Response('I did not get rendered via twig'); + } + + /** + * @Route("/streamed/") + * @Template(isStreamable=true) + */ + public function streamedAction() + { + return array( + 'foo' => 'foo', + 'bar' => 'bar', + ); + } +}