From 27d50bf2a12ac445826e225edc37f11ecfb9685c Mon Sep 17 00:00:00 2001 From: Iltar van der Berg Date: Fri, 26 Feb 2016 10:42:08 +0100 Subject: [PATCH] [PoC] Caching controller information for annotations --- .../Adapter/AnnotationAdapterInterface.php | 33 +++ .../ConfigurationAnnotationAdapter.php | 52 ++++ .../Adapter/RouteAnnotationAdapter.php | 54 ++++ .../ControllerMetadata/ClassMetadata.php | 69 +++++ .../Configuration/Cache.php | 248 ++++++++++++++++++ .../Configuration/ConfigurationAnnotation.php | 31 +++ .../Configuration/ConfigurationInterface.php | 34 +++ .../Configuration/Method.php | 82 ++++++ .../Configuration/ParamConverter.php | 190 ++++++++++++++ .../Configuration/Route.php | 49 ++++ .../Configuration/Security.php | 48 ++++ .../Configuration/Template.php | 186 +++++++++++++ .../NoMatchingFactoryFoundException.php | 23 ++ .../UnsupportedAnnotationException.php | 23 ++ .../AnnotationAdapterFactoryInterface.php | 32 +++ .../Factory/ChainAnnotationAdapterFactory.php | 59 +++++ .../ConfigurationAnnotationAdapterFactory.php | 34 +++ .../Factory/RouteAnnotationAdapterFactory.php | 34 +++ .../ControllerMetadata/MethodMetadata.php | 62 +++++ .../ControllerMetadata/ClassMetadataTest.php | 242 +++++++++++++++++ .../InvokableClassLevelController.php | 32 +++ .../InvokableContainerController.php | 61 +++++ .../InvokableController.php | 32 +++ ...pleActionsClassLevelTemplateController.php | 54 ++++ .../ControllerMetadata/SimpleController.php | 66 +++++ 25 files changed, 1830 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/AnnotationAdapterInterface.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/ConfigurationAnnotationAdapter.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Adapter/RouteAnnotationAdapter.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/ClassMetadata.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Cache.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationAnnotation.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ConfigurationInterface.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Method.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/ParamConverter.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Route.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Security.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Configuration/Template.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/NoMatchingFactoryFoundException.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Exception/UnsupportedAnnotationException.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/AnnotationAdapterFactoryInterface.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ChainAnnotationAdapterFactory.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/ConfigurationAnnotationAdapterFactory.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/Factory/RouteAnnotationAdapterFactory.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/ControllerMetadata/MethodMetadata.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/ClassMetadataTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableClassLevelController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableContainerController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/InvokableController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/MultipleActionsClassLevelTemplateController.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/ControllerMetadata/SimpleController.php 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', + ); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy