├── .gitattributes ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .php_cs.dist ├── .symfony.bundle.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Configuration ├── Cache.php ├── ConfigurationAnnotation.php ├── ConfigurationInterface.php ├── Entity.php ├── IsGranted.php ├── Method.php ├── ParamConverter.php ├── Route.php ├── Security.php └── Template.php ├── DependencyInjection ├── Compiler │ ├── AddExpressionLanguageProvidersPass.php │ ├── AddParamConverterPass.php │ └── OptimizerPass.php ├── Configuration.php └── SensioFrameworkExtraExtension.php ├── EventListener ├── ControllerListener.php ├── HttpCacheListener.php ├── IsGrantedListener.php ├── ParamConverterListener.php ├── SecurityListener.php └── TemplateListener.php ├── Request ├── ArgumentNameConverter.php └── ParamConverter │ ├── DateTimeParamConverter.php │ ├── DoctrineParamConverter.php │ ├── ParamConverterInterface.php │ └── ParamConverterManager.php ├── Resources ├── config │ ├── annotations.xml │ ├── cache.xml │ ├── converters.xml │ ├── routing-4.4.xml │ ├── routing.xml │ ├── security.xml │ └── view.xml └── doc │ ├── annotations │ ├── cache.rst │ ├── converters.rst │ ├── routing.rst │ ├── security.rst │ └── view.rst │ └── index.rst ├── Routing └── AnnotatedRouteControllerLoader.php ├── Security └── ExpressionLanguage.php ├── SensioFrameworkExtraBundle.php └── Templating └── TemplateGuesser.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /phpunit.xml.dist export-ignore 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: null 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: 'Ubuntu-20.04' 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['7.2', '7.3', '7.4', '8.0'] 14 | include: 15 | - php: '7.2' 16 | dependencies: 'lowest' 17 | composer-options: '--prefer-stable' 18 | 19 | name: PHP ${{ matrix.php }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: '${{ matrix.php }}' 25 | coverage: none 26 | ini-values: 'memory_limit=-1' 27 | - uses: "ramsey/composer-install@v1" 28 | with: 29 | dependency-versions: '${{ matrix.dependencies }}' 30 | composer-options: '${{ matrix.composer-options }}' 31 | - name: Install PHPUnit 32 | run: vendor/bin/simple-phpunit install 33 | - name: Run Unit tests 34 | run: vendor/bin/simple-phpunit 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | coverage 3 | /tests/Fixtures/var 4 | phpunit.xml 5 | composer.lock 6 | /.php_cs.cache 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 9 | '@Symfony' => true, 10 | '@Symfony:risky' => true, 11 | '@PHPUnit75Migration:risky' => true, 12 | 'php_unit_dedicate_assert' => ['target' => '5.6'], 13 | 'phpdoc_no_empty_return' => false, // triggers almost always false positive 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'fopen_flags' => false, 16 | 'ordered_imports' => true, 17 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 18 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 19 | 'protected_to_private' => false, 20 | // Part of @Symfony:risky in PHP-CS-Fixer 2.13.0. To be removed from the config file once upgrading 21 | 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced', 'strict' => true], 22 | // Part of future @Symfony ruleset in PHP-CS-Fixer To be removed from the config file once upgrading 23 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 24 | 'combine_nested_dirname' => true, 25 | ]) 26 | ->setRiskyAllowed(true) 27 | ->setFinder( 28 | PhpCsFixer\Finder::create() 29 | ->in(__DIR__.'/{src,tests}') 30 | ->append([__FILE__]) 31 | ) 32 | ; 33 | -------------------------------------------------------------------------------- /.symfony.bundle.yaml: -------------------------------------------------------------------------------- 1 | branches: ["master"] 2 | maintained_branches: ["master"] 3 | current_branch: "master" 4 | dev_branch: "master" 5 | dev_branch_alias: "5.0" 6 | doc_dir: "src/Resources/doc/" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 6.2 5 | --- 6 | 7 | * Add support for Symfony 6 8 | 9 | 6.1 10 | --- 11 | 12 | * Add support for PHP 8 attributes 13 | 14 | 6.0 15 | --- 16 | 17 | * Remove PSR-7 support 18 | 19 | 5.6 20 | --- 21 | 22 | * Bump min dependency versions 23 | 24 | 5.2 25 | --- 26 | 27 | * Deprecated routing annotations as this is included in symfony/framework-bundle. 28 | Disable the feature with 29 | 30 | ``` 31 | sensio_framework_extra: 32 | router: 33 | annotations: false 34 | ``` 35 | 36 | Also replace the annotations `Sensio\Bundle\FrameworkExtraBundle\Configuration\Route` 37 | and `Sensio\Bundle\FrameworkExtraBundle\Configuration\Method` with `Symfony\Component\Routing\Annotation\Route` 38 | 39 | 5.1 40 | --- 41 | 42 | * Added autoconfigure for `ParamConverterInterface` (#516). 43 | 44 | * Renamed service ids back to traditional service ids instead 45 | of class names (#530). 46 | 47 | 5.0 48 | --- 49 | 50 | * Changed the `@Security` annotation to use arguments from argument 51 | resolvers as expression variables. 52 | 53 | * The `@IsGranted` annotation now also supports using arguments from the 54 | argument resolvers as the subject. 55 | 56 | 4.0 57 | --- 58 | 59 | * added @IsGranted() annotation 60 | * allowed to disable some converters 61 | * allowed to customize the @security message and status code 62 | * [BC BREAK] changed template name generation from camelCase to under_score for both files and directories 63 | * removed support for bundle inheritance 64 | * a RuntimeException is now thrown when a reserved variable is used in a security expression 65 | * added cache-control max-stale support 66 | * renamed setETag to setEtag for consistency with Symfony core (use Etag in @Cache now instead of ETag) 67 | * added must-revalidate support for @Cache annotation 68 | * Response cache headers set in controllers now take precedence over the ones defined with the @Cache annotation 69 | * removed HHVM support 70 | * moved most services as private 71 | * renamed services to use their FQCN 72 | * allowed using multiple @Security annotations (class and method) 73 | * removed support for the Templating component (only plain Twig is supported) 74 | * removed unneeded phpdocs, converted protected to private properties 75 | * bumped Symfony minimum version to 3.0 76 | * bumped PHP minimum version to 5.5.9 77 | * removed class parameters in container definitions 78 | * [BC break] DateTimeParamConverter strictly validates the input date when using with 'format' option 79 | 80 | 3.0 81 | --- 82 | 83 | * fixed the Doctrine param converter that sent 500 when an entity was not found under some circumstances 84 | * ParamConverterInterface now uses ParamConverter as a type hint instead of ConfigurationInterface 85 | * added support for @Security 86 | * added support for HTTP validation cache in @Cache (ETag and LastModified) 87 | 88 | 2.2 89 | --- 90 | 91 | * added the possibility to configure the repository method to use for the 92 | Doctrine converter via the repository_method option. 93 | * [BC break] When defining multiple @Cache, @Method or @Template annotations on 94 | a controller class or method, the latter now overrules the former 95 | 96 | 2.1 97 | --- 98 | 99 | * added the possibility to configure the id name for the Doctrine converter via the id option 100 | * [BC break] The ParamConverterInterface::apply() method now must return a 101 | Boolean value indicating if a conversion was done. 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2020 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SensioFrameworkExtraBundle 2 | ========================== 3 | 4 | **WARNING**: SensioFrameworkExtraBundle is not maintained anymore. 5 | Please move to native PHP attribute support as added in Symfony core. 6 | For full support, use [Symfony 6.2](https://symfony.com/blog/new-in-symfony-6-2-built-in-cache-security-template-and-doctrine-attributes). 7 | 8 | This bundle provides a way to configure your controllers with annotations. 9 | 10 | Read about it on its [official homepage](http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html). 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sensio/framework-extra-bundle", 3 | "description": "This bundle provides a way to configure your controllers with annotations", 4 | "keywords": ["annotations","controllers"], 5 | "type": "symfony-bundle", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Fabien Potencier", 10 | "email": "fabien@symfony.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.2.5", 15 | "doctrine/annotations": "^1.0|^2.0", 16 | "symfony/config": "^4.4|^5.0|^6.0", 17 | "symfony/dependency-injection": "^4.4|^5.0|^6.0", 18 | "symfony/framework-bundle": "^4.4|^5.0|^6.0", 19 | "symfony/http-kernel": "^4.4|^5.0|^6.0" 20 | }, 21 | "require-dev": { 22 | "doctrine/dbal": "^2.10|^3.0", 23 | "doctrine/doctrine-bundle": "^1.11|^2.0", 24 | "doctrine/orm": "^2.5", 25 | "symfony/browser-kit": "^4.4|^5.0|^6.0", 26 | "symfony/doctrine-bridge": "^4.4|^5.0|^6.0", 27 | "symfony/dom-crawler": "^4.4|^5.0|^6.0", 28 | "symfony/expression-language": "^4.4|^5.0|^6.0", 29 | "symfony/finder": "^4.4|^5.0|^6.0", 30 | "symfony/monolog-bundle": "^3.2", 31 | "symfony/monolog-bridge": "^4.0|^5.0|^6.0", 32 | "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", 33 | "symfony/security-bundle": "^4.4|^5.0|^6.0", 34 | "symfony/twig-bundle": "^4.4|^5.0|^6.0", 35 | "symfony/yaml": "^4.4|^5.0|^6.0", 36 | "twig/twig": "^1.34|^2.4|^3.0" 37 | }, 38 | "conflict": { 39 | "doctrine/doctrine-cache-bundle": "<1.3.1", 40 | "doctrine/persistence": "<1.3" 41 | }, 42 | "minimum-stability": "dev", 43 | "autoload": { 44 | "psr-4": { 45 | "Sensio\\Bundle\\FrameworkExtraBundle\\": "src/" 46 | }, 47 | "exclude-from-classmap": [ 48 | "/tests/" 49 | ] 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Sensio\\Bundle\\FrameworkExtraBundle\\Tests\\": "tests/", 54 | "Tests\\Fixtures\\": "tests/Fixtures/" 55 | } 56 | }, 57 | "extra": { 58 | "branch-alias": { 59 | "dev-master": "6.1.x-dev" 60 | } 61 | }, 62 | "config": { 63 | "allow-plugins": { 64 | "composer/package-versions-deprecated": true 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Configuration/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * The Cache class handles the Cache annotation parts. 16 | * 17 | * @author Fabien Potencier 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 21 | class Cache extends ConfigurationAnnotation 22 | { 23 | /** 24 | * The expiration date as a valid date for the strtotime() function. 25 | * 26 | * @var string 27 | */ 28 | private $expires; 29 | 30 | /** 31 | * The number of seconds that the response is considered fresh by a private 32 | * cache like a web browser. 33 | * 34 | * @var int|string|null 35 | */ 36 | private $maxage; 37 | 38 | /** 39 | * The number of seconds that the response is considered fresh by a public 40 | * cache like a reverse proxy cache. 41 | * 42 | * @var int|string|null 43 | */ 44 | private $smaxage; 45 | 46 | /** 47 | * Whether the response is public or not. 48 | * 49 | * @var bool 50 | */ 51 | private $public; 52 | 53 | /** 54 | * Whether or not the response must be revalidated. 55 | * 56 | * @var bool 57 | */ 58 | private $mustRevalidate; 59 | 60 | /** 61 | * Additional "Vary:"-headers. 62 | * 63 | * @var array 64 | */ 65 | private $vary; 66 | 67 | /** 68 | * An expression to compute the Last-Modified HTTP header. 69 | * 70 | * @var string 71 | */ 72 | private $lastModified; 73 | 74 | /** 75 | * An expression to compute the ETag HTTP header. 76 | * 77 | * @var string 78 | */ 79 | private $etag; 80 | 81 | /** 82 | * max-stale Cache-Control header 83 | * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). 84 | * 85 | * @var int|string 86 | */ 87 | private $maxStale; 88 | 89 | /** 90 | * stale-while-revalidate Cache-Control header 91 | * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). 92 | * 93 | * @var int|string 94 | */ 95 | private $staleWhileRevalidate; 96 | 97 | /** 98 | * stale-if-error Cache-Control header 99 | * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). 100 | * 101 | * @var int|string 102 | */ 103 | private $staleIfError; 104 | 105 | /** 106 | * @param int|string|null $maxage 107 | * @param int|string|null $smaxage 108 | * @param int|string|null $maxstale 109 | * @param int|string|null $staleWhileRevalidate 110 | * @param int|string|null $staleIfError 111 | */ 112 | public function __construct( 113 | array $values = [], 114 | string $expires = null, 115 | $maxage = null, 116 | $smaxage = null, 117 | bool $public = null, 118 | bool $mustRevalidate = null, 119 | array $vary = null, 120 | string $lastModified = null, 121 | string $etag = null, 122 | $maxstale = null, 123 | $staleWhileRevalidate = null, 124 | $staleIfError = null 125 | ) { 126 | $values['expires'] = $values['expires'] ?? $expires; 127 | $values['maxage'] = $values['maxage'] ?? $maxage; 128 | $values['smaxage'] = $values['smaxage'] ?? $smaxage; 129 | $values['public'] = $values['public'] ?? $public; 130 | $values['mustRevalidate'] = $values['mustRevalidate'] ?? $mustRevalidate; 131 | $values['vary'] = $values['vary'] ?? $vary; 132 | $values['lastModified'] = $values['lastModified'] ?? $lastModified; 133 | $values['Etag'] = $values['Etag'] ?? $etag; 134 | $values['maxstale'] = $values['maxstale'] ?? $maxstale; 135 | $values['staleWhileRevalidate'] = $values['staleWhileRevalidate'] ?? $staleWhileRevalidate; 136 | $values['staleIfError'] = $values['staleIfError'] ?? $staleIfError; 137 | 138 | $values = array_filter($values, function ($v) { 139 | return null !== $v; 140 | }); 141 | parent::__construct($values); 142 | } 143 | 144 | /** 145 | * Returns the expiration date for the Expires header field. 146 | * 147 | * @return string 148 | */ 149 | public function getExpires() 150 | { 151 | return $this->expires; 152 | } 153 | 154 | /** 155 | * Sets the expiration date for the Expires header field. 156 | * 157 | * @param string $expires A valid php date 158 | */ 159 | public function setExpires($expires) 160 | { 161 | $this->expires = $expires; 162 | } 163 | 164 | /** 165 | * Sets the number of seconds for the max-age cache-control header field. 166 | * 167 | * @param int $maxage A number of seconds 168 | */ 169 | public function setMaxAge($maxage) 170 | { 171 | $this->maxage = $maxage; 172 | } 173 | 174 | /** 175 | * Returns the number of seconds the response is considered fresh by a 176 | * private cache. 177 | * 178 | * @return int 179 | */ 180 | public function getMaxAge() 181 | { 182 | return $this->maxage; 183 | } 184 | 185 | /** 186 | * Sets the number of seconds for the s-maxage cache-control header field. 187 | * 188 | * @param int $smaxage A number of seconds 189 | */ 190 | public function setSMaxAge($smaxage) 191 | { 192 | $this->smaxage = $smaxage; 193 | } 194 | 195 | /** 196 | * Returns the number of seconds the response is considered fresh by a 197 | * public cache. 198 | * 199 | * @return int 200 | */ 201 | public function getSMaxAge() 202 | { 203 | return $this->smaxage; 204 | } 205 | 206 | /** 207 | * Returns whether or not a response is public. 208 | * 209 | * @return bool 210 | */ 211 | public function isPublic() 212 | { 213 | return true === $this->public; 214 | } 215 | 216 | /** 217 | * @return bool 218 | */ 219 | public function mustRevalidate() 220 | { 221 | return true === $this->mustRevalidate; 222 | } 223 | 224 | /** 225 | * Forces a response to be revalidated. 226 | * 227 | * @param bool $mustRevalidate 228 | */ 229 | public function setMustRevalidate($mustRevalidate) 230 | { 231 | $this->mustRevalidate = (bool) $mustRevalidate; 232 | } 233 | 234 | /** 235 | * Returns whether or not a response is private. 236 | * 237 | * @return bool 238 | */ 239 | public function isPrivate() 240 | { 241 | return false === $this->public; 242 | } 243 | 244 | /** 245 | * Sets a response public. 246 | * 247 | * @param bool $public A boolean value 248 | */ 249 | public function setPublic($public) 250 | { 251 | $this->public = (bool) $public; 252 | } 253 | 254 | /** 255 | * Returns the custom "Vary"-headers. 256 | * 257 | * @return array 258 | */ 259 | public function getVary() 260 | { 261 | return $this->vary; 262 | } 263 | 264 | /** 265 | * Add additional "Vary:"-headers. 266 | * 267 | * @param array $vary 268 | */ 269 | public function setVary($vary) 270 | { 271 | $this->vary = $vary; 272 | } 273 | 274 | /** 275 | * Sets the "Last-Modified"-header expression. 276 | * 277 | * @param string $expression 278 | */ 279 | public function setLastModified($expression) 280 | { 281 | $this->lastModified = $expression; 282 | } 283 | 284 | /** 285 | * Returns the "Last-Modified"-header expression. 286 | * 287 | * @return string 288 | */ 289 | public function getLastModified() 290 | { 291 | return $this->lastModified; 292 | } 293 | 294 | /** 295 | * Sets the "ETag"-header expression. 296 | * 297 | * @param string $expression 298 | */ 299 | public function setEtag($expression) 300 | { 301 | $this->etag = $expression; 302 | } 303 | 304 | /** 305 | * Returns the "ETag"-header expression. 306 | * 307 | * @return string 308 | */ 309 | public function getEtag() 310 | { 311 | return $this->etag; 312 | } 313 | 314 | /** 315 | * @return int|string 316 | */ 317 | public function getMaxStale() 318 | { 319 | return $this->maxStale; 320 | } 321 | 322 | /** 323 | * Sets the number of seconds for the max-stale cache-control header field. 324 | * 325 | * @param int|string $maxStale A number of seconds 326 | */ 327 | public function setMaxStale($maxStale) 328 | { 329 | $this->maxStale = $maxStale; 330 | } 331 | 332 | /** 333 | * @return int|string 334 | */ 335 | public function getStaleWhileRevalidate() 336 | { 337 | return $this->staleWhileRevalidate; 338 | } 339 | 340 | /** 341 | * @param int|string $staleWhileRevalidate 342 | * 343 | * @return self 344 | */ 345 | public function setStaleWhileRevalidate($staleWhileRevalidate) 346 | { 347 | $this->staleWhileRevalidate = $staleWhileRevalidate; 348 | 349 | return $this; 350 | } 351 | 352 | /** 353 | * @return int|string 354 | */ 355 | public function getStaleIfError() 356 | { 357 | return $this->staleIfError; 358 | } 359 | 360 | /** 361 | * @param int|string $staleIfError 362 | * 363 | * @return self 364 | */ 365 | public function setStaleIfError($staleIfError) 366 | { 367 | $this->staleIfError = $staleIfError; 368 | 369 | return $this; 370 | } 371 | 372 | /** 373 | * Returns the annotation alias name. 374 | * 375 | * @return string 376 | * 377 | * @see ConfigurationInterface 378 | */ 379 | public function getAliasName() 380 | { 381 | return 'cache'; 382 | } 383 | 384 | /** 385 | * Only one cache directive is allowed. 386 | * 387 | * @return bool 388 | * 389 | * @see ConfigurationInterface 390 | */ 391 | public function allowArray() 392 | { 393 | return false; 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/Configuration/ConfigurationAnnotation.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * Base configuration annotation. 16 | * 17 | * @author Johannes M. Schmitt 18 | */ 19 | abstract class ConfigurationAnnotation implements ConfigurationInterface 20 | { 21 | public function __construct(array $values) 22 | { 23 | foreach ($values as $k => $v) { 24 | if (!method_exists($this, $name = 'set'.$k)) { 25 | throw new \RuntimeException(sprintf('Unknown key "%s" for annotation "@%s".', $k, static::class)); 26 | } 27 | 28 | $this->$name($v); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Configuration/ConfigurationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * ConfigurationInterface. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | interface ConfigurationInterface 20 | { 21 | /** 22 | * Returns the alias name for an annotated configuration. 23 | * 24 | * @return string 25 | */ 26 | public function getAliasName(); 27 | 28 | /** 29 | * Returns whether multiple annotations of this type are allowed. 30 | * 31 | * @return bool 32 | */ 33 | public function allowArray(); 34 | } 35 | -------------------------------------------------------------------------------- /src/Configuration/Entity.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * Doctrine-specific ParamConverter with an easier syntax. 16 | * 17 | * @author Ryan Weaver 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 21 | class Entity extends ParamConverter 22 | { 23 | public function setExpr($expr) 24 | { 25 | $options = $this->getOptions(); 26 | $options['expr'] = $expr; 27 | 28 | $this->setOptions($options); 29 | } 30 | 31 | /** 32 | * @param array|string $data 33 | */ 34 | public function __construct( 35 | $data = [], 36 | string $expr = null, 37 | string $class = null, 38 | array $options = [], 39 | bool $isOptional = false, 40 | string $converter = null 41 | ) { 42 | $values = []; 43 | if (\is_string($data)) { 44 | $values['value'] = $data; 45 | } else { 46 | $values = $data; 47 | } 48 | 49 | $values['expr'] = $values['expr'] ?? $expr; 50 | 51 | parent::__construct($values, $class, $options, $isOptional, $converter); 52 | 53 | $this->setExpr($values['expr']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Configuration/IsGranted.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * The Security class handles the Security annotation. 16 | * 17 | * @author Ryan Weaver 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 21 | class IsGranted extends ConfigurationAnnotation 22 | { 23 | /** 24 | * Sets the first argument that will be passed to isGranted(). 25 | * 26 | * @var mixed 27 | */ 28 | private $attributes; 29 | 30 | /** 31 | * Sets the second argument passed to isGranted(). 32 | * 33 | * @var mixed 34 | */ 35 | private $subject; 36 | 37 | /** 38 | * The message of the exception - has a nice default if not set. 39 | * 40 | * @var string 41 | */ 42 | private $message; 43 | 44 | /** 45 | * If set, will throw Symfony\Component\HttpKernel\Exception\HttpException 46 | * with the given $statusCode. 47 | * If null, Symfony\Component\Security\Core\Exception\AccessDeniedException. 48 | * will be used. 49 | * 50 | * @var int|null 51 | */ 52 | private $statusCode; 53 | 54 | /** 55 | * @param mixed $subject 56 | * @param array|string $data 57 | */ 58 | public function __construct( 59 | $data = [], 60 | $subject = null, 61 | string $message = null, 62 | ?int $statusCode = null 63 | ) { 64 | $values = []; 65 | if (\is_string($data)) { 66 | $values['attributes'] = $data; 67 | } else { 68 | $values = $data; 69 | } 70 | 71 | $values['subject'] = $values['subject'] ?? $subject; 72 | $values['message'] = $values['message'] ?? $message; 73 | $values['statusCode'] = $values['statusCode'] ?? $statusCode; 74 | parent::__construct($values); 75 | } 76 | 77 | public function setAttributes($attributes) 78 | { 79 | $this->attributes = $attributes; 80 | } 81 | 82 | public function getAttributes() 83 | { 84 | return $this->attributes; 85 | } 86 | 87 | public function setSubject($subject) 88 | { 89 | $this->subject = $subject; 90 | } 91 | 92 | public function getSubject() 93 | { 94 | return $this->subject; 95 | } 96 | 97 | public function getMessage() 98 | { 99 | return $this->message; 100 | } 101 | 102 | public function setMessage($message) 103 | { 104 | $this->message = $message; 105 | } 106 | 107 | public function getStatusCode() 108 | { 109 | return $this->statusCode; 110 | } 111 | 112 | public function setStatusCode($statusCode) 113 | { 114 | $this->statusCode = $statusCode; 115 | } 116 | 117 | public function setValue($value) 118 | { 119 | $this->setAttributes($value); 120 | } 121 | 122 | public function getAliasName() 123 | { 124 | return 'is_granted'; 125 | } 126 | 127 | public function allowArray() 128 | { 129 | return true; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Configuration/Method.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | @trigger_error(sprintf('The "%s" annotation is deprecated since version 5.2. Use "%s" instead.', Method::class, \Symfony\Component\Routing\Annotation\Route::class), \E_USER_DEPRECATED); 15 | 16 | /** 17 | * The Method class handles the Method annotation parts. 18 | * 19 | * @author Fabien Potencier 20 | * @Annotation 21 | * 22 | * @deprecated since version 5.2 23 | */ 24 | class Method extends ConfigurationAnnotation 25 | { 26 | /** 27 | * An array of restricted HTTP methods. 28 | * 29 | * @var array 30 | */ 31 | private $methods = []; 32 | 33 | /** 34 | * Returns the array of HTTP methods. 35 | * 36 | * @return array 37 | */ 38 | public function getMethods() 39 | { 40 | return $this->methods; 41 | } 42 | 43 | /** 44 | * Sets the HTTP methods. 45 | * 46 | * @param array|string $methods An HTTP method or an array of HTTP methods 47 | */ 48 | public function setMethods($methods) 49 | { 50 | $this->methods = \is_array($methods) ? $methods : [$methods]; 51 | } 52 | 53 | /** 54 | * Sets the HTTP methods. 55 | * 56 | * @param array|string $methods An HTTP method or an array of HTTP methods 57 | */ 58 | public function setValue($methods) 59 | { 60 | $this->setMethods($methods); 61 | } 62 | 63 | /** 64 | * Returns the annotation alias name. 65 | * 66 | * @return string 67 | * 68 | * @see ConfigurationInterface 69 | */ 70 | public function getAliasName() 71 | { 72 | return 'method'; 73 | } 74 | 75 | /** 76 | * Only one method directive is allowed. 77 | * 78 | * @return bool 79 | * 80 | * @see ConfigurationInterface 81 | */ 82 | public function allowArray() 83 | { 84 | return false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Configuration/ParamConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * The ParamConverter class handles the ParamConverter annotation parts. 16 | * 17 | * @author Fabien Potencier 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 21 | class ParamConverter extends ConfigurationAnnotation 22 | { 23 | /** 24 | * The parameter name. 25 | * 26 | * @var string 27 | */ 28 | private $name; 29 | 30 | /** 31 | * The parameter class. 32 | * 33 | * @var string 34 | */ 35 | private $class; 36 | 37 | /** 38 | * An array of options. 39 | * 40 | * @var array 41 | */ 42 | private $options = []; 43 | 44 | /** 45 | * Whether or not the parameter is optional. 46 | * 47 | * @var bool 48 | */ 49 | private $isOptional = false; 50 | 51 | /** 52 | * Use explicitly named converter instead of iterating by priorities. 53 | * 54 | * @var string 55 | */ 56 | private $converter; 57 | 58 | /** 59 | * @param array|string $data 60 | */ 61 | public function __construct( 62 | $data = [], 63 | string $class = null, 64 | array $options = [], 65 | bool $isOptional = false, 66 | string $converter = null 67 | ) { 68 | $values = []; 69 | if (\is_string($data)) { 70 | $values['value'] = $data; 71 | } else { 72 | $values = $data; 73 | } 74 | $values['class'] = $values['class'] ?? $class; 75 | $values['options'] = $values['options'] ?? $options; 76 | $values['isOptional'] = $values['isOptional'] ?? $isOptional; 77 | $values['converter'] = $values['converter'] ?? $converter; 78 | parent::__construct($values); 79 | } 80 | 81 | /** 82 | * Returns the parameter name. 83 | * 84 | * @return string 85 | */ 86 | public function getName() 87 | { 88 | return $this->name; 89 | } 90 | 91 | /** 92 | * Sets the parameter name. 93 | * 94 | * @param string $name The parameter name 95 | */ 96 | public function setValue($name) 97 | { 98 | $this->setName($name); 99 | } 100 | 101 | /** 102 | * Sets the parameter name. 103 | * 104 | * @param string $name The parameter name 105 | */ 106 | public function setName($name) 107 | { 108 | $this->name = $name; 109 | } 110 | 111 | /** 112 | * Returns the parameter class name. 113 | * 114 | * @return string 115 | */ 116 | public function getClass() 117 | { 118 | return $this->class; 119 | } 120 | 121 | /** 122 | * Sets the parameter class name. 123 | * 124 | * @param string $class The parameter class name 125 | */ 126 | public function setClass($class) 127 | { 128 | $this->class = $class; 129 | } 130 | 131 | /** 132 | * Returns an array of options. 133 | * 134 | * @return array 135 | */ 136 | public function getOptions() 137 | { 138 | return $this->options; 139 | } 140 | 141 | /** 142 | * Sets an array of options. 143 | * 144 | * @param array $options An array of options 145 | */ 146 | public function setOptions($options) 147 | { 148 | $this->options = $options; 149 | } 150 | 151 | /** 152 | * Sets whether or not the parameter is optional. 153 | * 154 | * @param bool $optional Whether the parameter is optional 155 | */ 156 | public function setIsOptional($optional) 157 | { 158 | $this->isOptional = (bool) $optional; 159 | } 160 | 161 | /** 162 | * Returns whether or not the parameter is optional. 163 | * 164 | * @return bool 165 | */ 166 | public function isOptional() 167 | { 168 | return $this->isOptional; 169 | } 170 | 171 | /** 172 | * Get explicit converter name. 173 | * 174 | * @return string 175 | */ 176 | public function getConverter() 177 | { 178 | return $this->converter; 179 | } 180 | 181 | /** 182 | * Set explicit converter name. 183 | * 184 | * @param string $converter 185 | */ 186 | public function setConverter($converter) 187 | { 188 | $this->converter = $converter; 189 | } 190 | 191 | /** 192 | * Returns the annotation alias name. 193 | * 194 | * @return string 195 | * 196 | * @see ConfigurationInterface 197 | */ 198 | public function getAliasName() 199 | { 200 | return 'converters'; 201 | } 202 | 203 | /** 204 | * Multiple ParamConverters are allowed. 205 | * 206 | * @return bool 207 | * 208 | * @see ConfigurationInterface 209 | */ 210 | public function allowArray() 211 | { 212 | return true; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Configuration/Route.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | use Symfony\Component\Routing\Annotation\Route as BaseRoute; 15 | 16 | @trigger_error(sprintf('The "%s" annotation is deprecated since version 5.2. Use "%s" instead.', Route::class, BaseRoute::class), \E_USER_DEPRECATED); 17 | 18 | /** 19 | * @author Kris Wallsmith 20 | * @Annotation 21 | * 22 | * @deprecated since version 5.2 23 | */ 24 | class Route extends BaseRoute 25 | { 26 | private $service; 27 | 28 | public function setService($service) 29 | { 30 | // avoid a BC notice in case of @Route(service="") with sf ^2.7 31 | if (null === $this->getPath()) { 32 | $this->setPath(''); 33 | } 34 | $this->service = $service; 35 | } 36 | 37 | public function getService() 38 | { 39 | return $this->service; 40 | } 41 | 42 | public function setLocalizedPaths(array $localizedPaths) 43 | { 44 | if (isset($localizedPaths['service'])) { 45 | $this->setService($localizedPaths['service']); 46 | unset($localizedPaths['service']); 47 | } 48 | 49 | parent::setLocalizedPaths($localizedPaths); 50 | } 51 | 52 | /** 53 | * Multiple route annotations are allowed. 54 | * 55 | * @return bool 56 | * 57 | * @see ConfigurationInterface 58 | */ 59 | public function allowArray() 60 | { 61 | return true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Configuration/Security.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * The Security class handles the Security annotation. 16 | * 17 | * @author Fabien Potencier 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] 21 | class Security extends ConfigurationAnnotation 22 | { 23 | /** 24 | * The expression evaluated to allow or deny access. 25 | * 26 | * @var string 27 | */ 28 | private $expression; 29 | 30 | /** 31 | * If set, will throw Symfony\Component\HttpKernel\Exception\HttpException 32 | * with the given $statusCode. 33 | * If null, Symfony\Component\Security\Core\Exception\AccessDeniedException. 34 | * will be used. 35 | * 36 | * @var int|null 37 | */ 38 | protected $statusCode; 39 | 40 | /** 41 | * The message of the exception. 42 | * 43 | * @var string 44 | */ 45 | protected $message = 'Access denied.'; 46 | 47 | /** 48 | * @param array|string $data 49 | */ 50 | public function __construct( 51 | $data = [], 52 | string $message = null, 53 | ?int $statusCode = null 54 | ) { 55 | $values = []; 56 | if (\is_string($data)) { 57 | $values['expression'] = $data; 58 | } else { 59 | $values = $data; 60 | } 61 | 62 | $values['message'] = $values['message'] ?? $message ?? $this->message; 63 | $values['statusCode'] = $values['statusCode'] ?? $statusCode; 64 | 65 | parent::__construct($values); 66 | } 67 | 68 | public function getExpression() 69 | { 70 | return $this->expression; 71 | } 72 | 73 | public function setExpression($expression) 74 | { 75 | $this->expression = $expression; 76 | } 77 | 78 | public function getStatusCode() 79 | { 80 | return $this->statusCode; 81 | } 82 | 83 | public function setStatusCode($statusCode) 84 | { 85 | $this->statusCode = $statusCode; 86 | } 87 | 88 | public function getMessage() 89 | { 90 | return $this->message; 91 | } 92 | 93 | public function setMessage($message) 94 | { 95 | $this->message = $message; 96 | } 97 | 98 | public function setValue($expression) 99 | { 100 | $this->setExpression($expression); 101 | } 102 | 103 | public function getAliasName() 104 | { 105 | return 'security'; 106 | } 107 | 108 | public function allowArray() 109 | { 110 | return true; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Configuration/Template.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | 14 | /** 15 | * The Template class handles the Template annotation parts. 16 | * 17 | * @author Fabien Potencier 18 | * @Annotation 19 | */ 20 | #[\Attribute(\Attribute::TARGET_METHOD)] 21 | class Template extends ConfigurationAnnotation 22 | { 23 | /** 24 | * The template. 25 | * 26 | * @var string 27 | */ 28 | protected $template; 29 | 30 | /** 31 | * The associative array of template variables. 32 | * 33 | * @var array 34 | */ 35 | private $vars = []; 36 | 37 | /** 38 | * Should the template be streamed? 39 | * 40 | * @var bool 41 | */ 42 | private $streamable = false; 43 | 44 | /** 45 | * The controller (+action) this annotation is set to. 46 | * 47 | * @var array 48 | */ 49 | private $owner = []; 50 | 51 | /** 52 | * @param array|string $data 53 | */ 54 | public function __construct( 55 | $data = [], 56 | array $vars = [], 57 | bool $isStreamable = false, 58 | array $owner = [] 59 | ) { 60 | $values = []; 61 | if (\is_string($data)) { 62 | $values['template'] = $data; 63 | } else { 64 | $values = $data; 65 | } 66 | 67 | $values['isStreamable'] = $values['isStreamable'] ?? $isStreamable; 68 | $values['vars'] = $values['vars'] ?? $vars; 69 | $values['owner'] = $values['owner'] ?? $owner; 70 | 71 | parent::__construct($values); 72 | } 73 | 74 | /** 75 | * Returns the array of templates variables. 76 | * 77 | * @return array 78 | */ 79 | public function getVars() 80 | { 81 | return $this->vars; 82 | } 83 | 84 | /** 85 | * @param bool $streamable 86 | */ 87 | public function setIsStreamable($streamable) 88 | { 89 | $this->streamable = $streamable; 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | public function isStreamable() 96 | { 97 | return (bool) $this->streamable; 98 | } 99 | 100 | /** 101 | * Sets the template variables. 102 | * 103 | * @param array $vars The template variables 104 | */ 105 | public function setVars($vars) 106 | { 107 | $this->vars = $vars; 108 | } 109 | 110 | /** 111 | * Sets the template logic name. 112 | * 113 | * @param string $template The template logic name 114 | */ 115 | public function setValue($template) 116 | { 117 | $this->setTemplate($template); 118 | } 119 | 120 | /** 121 | * Returns the template. 122 | * 123 | * @return string 124 | */ 125 | public function getTemplate() 126 | { 127 | return $this->template; 128 | } 129 | 130 | /** 131 | * Sets the template. 132 | * 133 | * @param string $template The template 134 | */ 135 | public function setTemplate($template) 136 | { 137 | $this->template = $template; 138 | } 139 | 140 | /** 141 | * Returns the annotation alias name. 142 | * 143 | * @return string 144 | * 145 | * @see ConfigurationInterface 146 | */ 147 | public function getAliasName() 148 | { 149 | return 'template'; 150 | } 151 | 152 | /** 153 | * Only one template directive is allowed. 154 | * 155 | * @return bool 156 | * 157 | * @see ConfigurationInterface 158 | */ 159 | public function allowArray() 160 | { 161 | return false; 162 | } 163 | 164 | public function setOwner(array $owner) 165 | { 166 | $this->owner = $owner; 167 | } 168 | 169 | /** 170 | * The controller (+action) this annotation is attached to. 171 | * 172 | * @return array 173 | */ 174 | public function getOwner() 175 | { 176 | return $this->owner; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | class AddExpressionLanguageProvidersPass implements CompilerPassInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function process(ContainerBuilder $container) 24 | { 25 | if ($container->hasDefinition('sensio_framework_extra.security.expression_language.default')) { 26 | $definition = $container->findDefinition('sensio_framework_extra.security.expression_language.default'); 27 | foreach ($container->findTaggedServiceIds('security.expression_language_provider') as $id => $attributes) { 28 | $definition->addMethodCall('registerProvider', [new Reference($id)]); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/AddParamConverterPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | /** 19 | * Adds tagged request.param_converter services to converter.manager service. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | class AddParamConverterPass implements CompilerPassInterface 24 | { 25 | public function process(ContainerBuilder $container) 26 | { 27 | if (false === $container->hasDefinition('sensio_framework_extra.converter.manager')) { 28 | return; 29 | } 30 | 31 | $definition = $container->getDefinition('sensio_framework_extra.converter.manager'); 32 | $disabled = $container->getParameter('sensio_framework_extra.disabled_converters'); 33 | $container->getParameterBag()->remove('sensio_framework_extra.disabled_converters'); 34 | 35 | foreach ($container->findTaggedServiceIds('request.param_converter') as $id => $converters) { 36 | foreach ($converters as $converter) { 37 | $name = isset($converter['converter']) ? $converter['converter'] : null; 38 | 39 | if (null !== $name && \in_array($name, $disabled)) { 40 | continue; 41 | } 42 | 43 | $priority = isset($converter['priority']) ? $converter['priority'] : 0; 44 | 45 | if ('false' === $priority || false === $priority) { 46 | $priority = null; 47 | } 48 | 49 | $definition->addMethodCall('add', [new Reference($id), $priority, $name]); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/OptimizerPass.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | /** 18 | * Optimizes the container by removing unneeded listeners. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class OptimizerPass implements CompilerPassInterface 23 | { 24 | public function process(ContainerBuilder $container) 25 | { 26 | if (!$container->has('security.token_storage')) { 27 | $container->removeDefinition('sensio_framework_extra.security.listener'); 28 | } 29 | 30 | if (!$container->hasDefinition('twig')) { 31 | $container->removeDefinition('sensio_framework_extra.view.listener'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | use Symfony\Component\Config\Definition\NodeInterface; 17 | 18 | /** 19 | * FrameworkExtraBundle configuration structure. 20 | * 21 | * @author Henrik Bjornskov 22 | */ 23 | class Configuration implements ConfigurationInterface 24 | { 25 | /** 26 | * Generates the configuration tree. 27 | * 28 | * @return NodeInterface 29 | */ 30 | public function getConfigTreeBuilder() 31 | { 32 | $treeBuilder = new TreeBuilder('sensio_framework_extra'); 33 | 34 | if (method_exists($treeBuilder, 'getRootNode')) { 35 | $rootNode = $treeBuilder->getRootNode(); 36 | } else { 37 | // BC layer for symfony/config 4.1 and older 38 | $rootNode = $treeBuilder->root('sensio_framework_extra'); 39 | } 40 | 41 | $rootNode 42 | ->children() 43 | ->arrayNode('router') 44 | ->addDefaultsIfNotSet() 45 | ->children() 46 | ->booleanNode('annotations')->defaultTrue()->end() 47 | ->end() 48 | ->end() 49 | ->arrayNode('request') 50 | ->addDefaultsIfNotSet() 51 | ->children() 52 | ->booleanNode('converters')->defaultTrue()->end() 53 | ->booleanNode('auto_convert')->defaultTrue()->end() 54 | ->arrayNode('disable')->prototype('scalar')->end()->end() 55 | ->end() 56 | ->end() 57 | ->arrayNode('view') 58 | ->addDefaultsIfNotSet() 59 | ->children() 60 | ->booleanNode('annotations')->defaultTrue()->end() 61 | ->end() 62 | ->end() 63 | ->arrayNode('cache') 64 | ->addDefaultsIfNotSet() 65 | ->children() 66 | ->booleanNode('annotations')->defaultTrue()->end() 67 | ->end() 68 | ->end() 69 | ->arrayNode('security') 70 | ->addDefaultsIfNotSet() 71 | ->children() 72 | ->booleanNode('annotations')->defaultTrue()->end() 73 | ->scalarNode('expression_language')->defaultValue('sensio_framework_extra.security.expression_language.default')->end() 74 | ->end() 75 | ->end() 76 | ->arrayNode('templating') 77 | ->fixXmlConfig('controller_pattern') 78 | ->children() 79 | ->arrayNode('controller_patterns') 80 | ->prototype('scalar') 81 | ->end() 82 | ->end() 83 | ->end() 84 | ->end() 85 | ; 86 | 87 | return $treeBuilder; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/DependencyInjection/SensioFrameworkExtraExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\DependencyInjection; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; 15 | use Symfony\Component\Config\FileLocator; 16 | use Symfony\Component\Config\Resource\ClassExistenceResource; 17 | use Symfony\Component\DependencyInjection\Alias; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 20 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 21 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 22 | use Symfony\Component\HttpKernel\Kernel; 23 | use Symfony\Component\Security\Core\Authorization\ExpressionLanguage as SecurityExpressionLanguage; 24 | 25 | /** 26 | * @author Fabien Potencier 27 | */ 28 | class SensioFrameworkExtraExtension extends Extension 29 | { 30 | public function load(array $configs, ContainerBuilder $container) 31 | { 32 | $configuration = $this->getConfiguration($configs, $container); 33 | $config = $this->processConfiguration($configuration, $configs); 34 | 35 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 36 | 37 | $annotationsToLoad = []; 38 | $definitionsToRemove = []; 39 | 40 | if ($config['router']['annotations']) { 41 | @trigger_error(sprintf('Enabling the "sensio_framework_extra.router.annotations" configuration is deprecated since version 5.2. Set it to false and use the "%s" annotation from Symfony itself.', \Symfony\Component\Routing\Annotation\Route::class), \E_USER_DEPRECATED); 42 | 43 | if (Kernel::MAJOR_VERSION < 5) { 44 | $annotationsToLoad[] = 'routing-4.4.xml'; 45 | } else { 46 | $annotationsToLoad[] = 'routing.xml'; 47 | } 48 | } 49 | 50 | if ($config['request']['converters']) { 51 | $annotationsToLoad[] = 'converters.xml'; 52 | 53 | $container->registerForAutoconfiguration(ParamConverterInterface::class) 54 | ->addTag('request.param_converter'); 55 | 56 | $container->setParameter('sensio_framework_extra.disabled_converters', \is_string($config['request']['disable']) ? implode(',', $config['request']['disable']) : $config['request']['disable']); 57 | 58 | $container->addResource(new ClassExistenceResource(ExpressionLanguage::class)); 59 | if (class_exists(ExpressionLanguage::class)) { 60 | $container->setAlias('sensio_framework_extra.converter.doctrine.orm.expression_language', new Alias('sensio_framework_extra.converter.doctrine.orm.expression_language.default', false)); 61 | } else { 62 | $definitionsToRemove[] = 'sensio_framework_extra.converter.doctrine.orm.expression_language.default'; 63 | } 64 | } 65 | 66 | if ($config['view']['annotations']) { 67 | $annotationsToLoad[] = 'view.xml'; 68 | } 69 | 70 | if ($config['cache']['annotations']) { 71 | $annotationsToLoad[] = 'cache.xml'; 72 | } 73 | 74 | if ($config['security']['annotations']) { 75 | $annotationsToLoad[] = 'security.xml'; 76 | 77 | $container->addResource(new ClassExistenceResource(ExpressionLanguage::class)); 78 | if (class_exists(ExpressionLanguage::class)) { 79 | // this resource can only be added if ExpressionLanguage exists (to avoid a fatal error) 80 | $container->addResource(new ClassExistenceResource(SecurityExpressionLanguage::class)); 81 | if (class_exists(SecurityExpressionLanguage::class)) { 82 | $container->setAlias('sensio_framework_extra.security.expression_language', new Alias($config['security']['expression_language'], false)); 83 | } else { 84 | $definitionsToRemove[] = 'sensio_framework_extra.security.expression_language.default'; 85 | } 86 | } else { 87 | $definitionsToRemove[] = 'sensio_framework_extra.security.expression_language.default'; 88 | } 89 | } 90 | 91 | if ($annotationsToLoad) { 92 | // must be first 93 | $loader->load('annotations.xml'); 94 | 95 | foreach ($annotationsToLoad as $configFile) { 96 | $loader->load($configFile); 97 | } 98 | 99 | if ($config['request']['converters']) { 100 | $container->getDefinition('sensio_framework_extra.converter.listener')->replaceArgument(1, $config['request']['auto_convert']); 101 | } 102 | } 103 | 104 | if (!empty($config['templating']['controller_patterns'])) { 105 | $container 106 | ->getDefinition('sensio_framework_extra.view.guesser') 107 | ->addArgument($config['templating']['controller_patterns']); 108 | } 109 | 110 | foreach ($definitionsToRemove as $definition) { 111 | $container->removeDefinition($definition); 112 | } 113 | } 114 | 115 | /** 116 | * Returns the base path for the XSD files. 117 | * 118 | * @return string The XSD base path 119 | */ 120 | public function getXsdValidationBasePath() 121 | { 122 | return __DIR__.'/../Resources/config/schema'; 123 | } 124 | 125 | /** 126 | * @return string 127 | */ 128 | public function getNamespace() 129 | { 130 | return 'http://symfony.com/schema/dic/symfony_extra'; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/EventListener/ControllerListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Doctrine\Common\Annotations\Reader; 15 | use Doctrine\Persistence\Proxy; 16 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\HttpKernel\Event\KernelEvent; 19 | use Symfony\Component\HttpKernel\KernelEvents; 20 | 21 | /** 22 | * The ControllerListener class parses annotation blocks located in 23 | * controller classes. 24 | * 25 | * @author Fabien Potencier 26 | */ 27 | class ControllerListener implements EventSubscriberInterface 28 | { 29 | /** 30 | * @var Reader 31 | */ 32 | private $reader; 33 | 34 | public function __construct(Reader $reader) 35 | { 36 | $this->reader = $reader; 37 | } 38 | 39 | /** 40 | * Modifies the Request object to apply configuration information found in 41 | * controllers annotations like the template to render or HTTP caching 42 | * configuration. 43 | */ 44 | public function onKernelController(KernelEvent $event) 45 | { 46 | $controller = $event->getController(); 47 | 48 | if (!\is_array($controller) && method_exists($controller, '__invoke')) { 49 | $controller = [$controller, '__invoke']; 50 | } 51 | 52 | if (!\is_array($controller)) { 53 | return; 54 | } 55 | 56 | $className = $this->getRealClass(\get_class($controller[0])); 57 | $object = new \ReflectionClass($className); 58 | $method = $object->getMethod($controller[1]); 59 | 60 | $classConfigurations = $this->getConfigurations($this->reader->getClassAnnotations($object)); 61 | $methodConfigurations = $this->getConfigurations($this->reader->getMethodAnnotations($method)); 62 | 63 | if (80000 <= \PHP_VERSION_ID) { 64 | $classAttributes = array_map( 65 | function (\ReflectionAttribute $attribute) { 66 | return $attribute->newInstance(); 67 | }, 68 | $object->getAttributes(ConfigurationInterface::class, \ReflectionAttribute::IS_INSTANCEOF) 69 | ); 70 | $classConfigurations = array_merge($classConfigurations, $this->getConfigurations($classAttributes)); 71 | 72 | $methodAttributes = array_map( 73 | function (\ReflectionAttribute $attribute) { 74 | return $attribute->newInstance(); 75 | }, 76 | $method->getAttributes(ConfigurationInterface::class, \ReflectionAttribute::IS_INSTANCEOF) 77 | ); 78 | $methodConfigurations = array_merge($methodConfigurations, $this->getConfigurations($methodAttributes)); 79 | } 80 | 81 | $configurations = []; 82 | foreach (array_merge(array_keys($classConfigurations), array_keys($methodConfigurations)) as $key) { 83 | if (!\array_key_exists($key, $classConfigurations)) { 84 | $configurations[$key] = $methodConfigurations[$key]; 85 | } elseif (!\array_key_exists($key, $methodConfigurations)) { 86 | $configurations[$key] = $classConfigurations[$key]; 87 | } else { 88 | if (\is_array($classConfigurations[$key])) { 89 | if (!\is_array($methodConfigurations[$key])) { 90 | throw new \UnexpectedValueException('Configurations should both be an array or both not be an array.'); 91 | } 92 | $configurations[$key] = array_merge($classConfigurations[$key], $methodConfigurations[$key]); 93 | } else { 94 | // method configuration overrides class configuration 95 | $configurations[$key] = $methodConfigurations[$key]; 96 | } 97 | } 98 | } 99 | 100 | $request = $event->getRequest(); 101 | foreach ($configurations as $key => $attributes) { 102 | $request->attributes->set($key, $attributes); 103 | } 104 | } 105 | 106 | private function getConfigurations(array $annotations) 107 | { 108 | $configurations = []; 109 | foreach ($annotations as $configuration) { 110 | if ($configuration instanceof ConfigurationInterface) { 111 | if ($configuration->allowArray()) { 112 | $configurations['_'.$configuration->getAliasName()][] = $configuration; 113 | } elseif (!isset($configurations['_'.$configuration->getAliasName()])) { 114 | $configurations['_'.$configuration->getAliasName()] = $configuration; 115 | } else { 116 | throw new \LogicException(sprintf('Multiple "%s" annotations are not allowed.', $configuration->getAliasName())); 117 | } 118 | } 119 | } 120 | 121 | return $configurations; 122 | } 123 | 124 | /** 125 | * @return array 126 | */ 127 | public static function getSubscribedEvents() 128 | { 129 | return [ 130 | KernelEvents::CONTROLLER => 'onKernelController', 131 | ]; 132 | } 133 | 134 | private static function getRealClass(string $class): string 135 | { 136 | if (class_exists(Proxy::class)) { 137 | if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) { 138 | return $class; 139 | } 140 | 141 | return substr($class, $pos + Proxy::MARKER_LENGTH + 2); 142 | } 143 | 144 | return $class; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/EventListener/HttpCacheListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 15 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 16 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 17 | use Symfony\Component\HttpFoundation\Response; 18 | use Symfony\Component\HttpKernel\Event\KernelEvent; 19 | use Symfony\Component\HttpKernel\KernelEvents; 20 | 21 | /** 22 | * HttpCacheListener handles HTTP cache headers. 23 | * 24 | * It can be configured via the Cache annotation. 25 | * 26 | * @author Fabien Potencier 27 | */ 28 | class HttpCacheListener implements EventSubscriberInterface 29 | { 30 | private $lastModifiedDates; 31 | private $etags; 32 | private $expressionLanguage; 33 | 34 | public function __construct() 35 | { 36 | $this->lastModifiedDates = new \SplObjectStorage(); 37 | $this->etags = new \SplObjectStorage(); 38 | } 39 | 40 | /** 41 | * Handles HTTP validation headers. 42 | */ 43 | public function onKernelController(KernelEvent $event) 44 | { 45 | $request = $event->getRequest(); 46 | $configuration = $request->attributes->get('_cache'); 47 | if (!$configuration instanceof Cache) { 48 | return; 49 | } 50 | 51 | $response = new Response(); 52 | 53 | $lastModifiedDate = ''; 54 | if ($configuration->getLastModified()) { 55 | $lastModifiedDate = $this->getExpressionLanguage()->evaluate($configuration->getLastModified(), $request->attributes->all()); 56 | $response->setLastModified($lastModifiedDate); 57 | } 58 | 59 | $etag = ''; 60 | if ($configuration->getEtag()) { 61 | $etag = hash('sha256', $this->getExpressionLanguage()->evaluate($configuration->getEtag(), $request->attributes->all())); 62 | $response->setEtag($etag); 63 | } 64 | 65 | if ($response->isNotModified($request)) { 66 | $event->setController(function () use ($response) { 67 | return $response; 68 | }); 69 | $event->stopPropagation(); 70 | } else { 71 | if ($etag) { 72 | $this->etags[$request] = $etag; 73 | } 74 | if ($lastModifiedDate) { 75 | $this->lastModifiedDates[$request] = $lastModifiedDate; 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Modifies the response to apply HTTP cache headers when needed. 82 | */ 83 | public function onKernelResponse(KernelEvent $event) 84 | { 85 | $request = $event->getRequest(); 86 | $configuration = $request->attributes->get('_cache'); 87 | 88 | if (!$configuration instanceof Cache) { 89 | return; 90 | } 91 | 92 | $response = $event->getResponse(); 93 | 94 | // http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1 95 | if (!\in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 304, 404, 410])) { 96 | return; 97 | } 98 | 99 | if (!$response->headers->hasCacheControlDirective('s-maxage') && null !== $age = $configuration->getSMaxAge()) { 100 | $age = $this->convertToSecondsIfNeeded($age); 101 | 102 | $response->setSharedMaxAge($age); 103 | } 104 | 105 | if ($configuration->mustRevalidate()) { 106 | $response->headers->addCacheControlDirective('must-revalidate'); 107 | } 108 | 109 | if (!$response->headers->hasCacheControlDirective('max-age') && null !== $age = $configuration->getMaxAge()) { 110 | $age = $this->convertToSecondsIfNeeded($age); 111 | 112 | $response->setMaxAge($age); 113 | } 114 | 115 | if (!$response->headers->hasCacheControlDirective('max-stale') && null !== $stale = $configuration->getMaxStale()) { 116 | $stale = $this->convertToSecondsIfNeeded($stale); 117 | 118 | $response->headers->addCacheControlDirective('max-stale', $stale); 119 | } 120 | 121 | if (!$response->headers->hasCacheControlDirective('stale-while-revalidate') && null !== $staleWhileRevalidate = $configuration->getStaleWhileRevalidate()) { 122 | $staleWhileRevalidate = $this->convertToSecondsIfNeeded($staleWhileRevalidate); 123 | 124 | $response->headers->addCacheControlDirective('stale-while-revalidate', $staleWhileRevalidate); 125 | } 126 | 127 | if (!$response->headers->hasCacheControlDirective('stale-if-error') && null !== $staleIfError = $configuration->getStaleIfError()) { 128 | $staleIfError = $this->convertToSecondsIfNeeded($staleIfError); 129 | 130 | $response->headers->addCacheControlDirective('stale-if-error', $staleIfError); 131 | } 132 | 133 | if (!$response->headers->has('Expires') && null !== $configuration->getExpires()) { 134 | $date = \DateTime::createFromFormat('U', strtotime($configuration->getExpires()), new \DateTimeZone('UTC')); 135 | $response->setExpires($date); 136 | } 137 | 138 | if (!$response->headers->has('Vary') && null !== $configuration->getVary()) { 139 | $response->setVary($configuration->getVary()); 140 | } 141 | 142 | if ($configuration->isPublic()) { 143 | $response->setPublic(); 144 | } 145 | 146 | if ($configuration->isPrivate()) { 147 | $response->setPrivate(); 148 | } 149 | 150 | if (!$response->headers->has('Last-Modified') && isset($this->lastModifiedDates[$request])) { 151 | $response->setLastModified($this->lastModifiedDates[$request]); 152 | 153 | unset($this->lastModifiedDates[$request]); 154 | } 155 | 156 | if (!$response->headers->has('Etag') && isset($this->etags[$request])) { 157 | $response->setEtag($this->etags[$request]); 158 | 159 | unset($this->etags[$request]); 160 | } 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public static function getSubscribedEvents() 167 | { 168 | return [ 169 | KernelEvents::CONTROLLER => 'onKernelController', 170 | KernelEvents::RESPONSE => 'onKernelResponse', 171 | ]; 172 | } 173 | 174 | private function getExpressionLanguage() 175 | { 176 | if (null === $this->expressionLanguage) { 177 | if (!class_exists(ExpressionLanguage::class)) { 178 | throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); 179 | } 180 | $this->expressionLanguage = new ExpressionLanguage(); 181 | } 182 | 183 | return $this->expressionLanguage; 184 | } 185 | 186 | /** 187 | * @param int|string $time Time that can be either expressed in seconds or with relative time format (1 day, 2 weeks, ...) 188 | * 189 | * @return int 190 | */ 191 | private function convertToSecondsIfNeeded($time) 192 | { 193 | if (!is_numeric($time)) { 194 | $now = microtime(true); 195 | 196 | $time = ceil(strtotime($time, (int) $now) - $now); 197 | } 198 | 199 | return $time; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/EventListener/IsGrantedListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 15 | use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\HttpKernel\Event\KernelEvent; 18 | use Symfony\Component\HttpKernel\Exception\HttpException; 19 | use Symfony\Component\HttpKernel\KernelEvents; 20 | use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; 21 | use Symfony\Component\Security\Core\Exception\AccessDeniedException; 22 | 23 | /** 24 | * Handles the IsGranted annotation on controllers. 25 | * 26 | * @author Ryan Weaver 27 | */ 28 | class IsGrantedListener implements EventSubscriberInterface 29 | { 30 | private $argumentNameConverter; 31 | private $authChecker; 32 | 33 | public function __construct(ArgumentNameConverter $argumentNameConverter, AuthorizationCheckerInterface $authChecker = null) 34 | { 35 | $this->argumentNameConverter = $argumentNameConverter; 36 | $this->authChecker = $authChecker; 37 | } 38 | 39 | public function onKernelControllerArguments(KernelEvent $event) 40 | { 41 | $request = $event->getRequest(); 42 | 43 | /** @var $configurations IsGranted[] */ 44 | if (!$configurations = $request->attributes->get('_is_granted')) { 45 | return; 46 | } 47 | 48 | if (null === $this->authChecker) { 49 | throw new \LogicException('To use the @IsGranted tag, you need to install symfony/security-bundle and configure your security system.'); 50 | } 51 | 52 | $arguments = $this->argumentNameConverter->getControllerArguments($event); 53 | 54 | foreach ($configurations as $configuration) { 55 | $subjectRef = $configuration->getSubject(); 56 | $subject = null; 57 | 58 | if ($subjectRef) { 59 | if (\is_array($subjectRef)) { 60 | foreach ($subjectRef as $ref) { 61 | if (!\array_key_exists($ref, $arguments)) { 62 | throw $this->createMissingSubjectException($ref); 63 | } 64 | 65 | $subject[$ref] = $arguments[$ref]; 66 | } 67 | } else { 68 | if (!\array_key_exists($subjectRef, $arguments)) { 69 | throw $this->createMissingSubjectException($subjectRef); 70 | } 71 | 72 | $subject = $arguments[$subjectRef]; 73 | } 74 | } 75 | 76 | if (!$this->authChecker->isGranted($configuration->getAttributes(), $subject)) { 77 | $argsString = $this->getIsGrantedString($configuration); 78 | 79 | $message = $configuration->getMessage() ?: sprintf('Access Denied by controller annotation @IsGranted(%s)', $argsString); 80 | 81 | if ($statusCode = $configuration->getStatusCode()) { 82 | throw new HttpException($statusCode, $message); 83 | } 84 | 85 | $accessDeniedException = new AccessDeniedException($message); 86 | $accessDeniedException->setAttributes($configuration->getAttributes()); 87 | $accessDeniedException->setSubject($subject); 88 | 89 | throw $accessDeniedException; 90 | } 91 | } 92 | } 93 | 94 | private function createMissingSubjectException(string $subject) 95 | { 96 | return new \RuntimeException(sprintf('Could not find the subject "%s" for the @IsGranted annotation. Try adding a "$%s" argument to your controller method.', $subject, $subject)); 97 | } 98 | 99 | private function getIsGrantedString(IsGranted $isGranted) 100 | { 101 | $attributes = array_map(function ($attribute) { 102 | return sprintf('"%s"', $attribute); 103 | }, (array) $isGranted->getAttributes()); 104 | if (1 === \count($attributes)) { 105 | $argsString = reset($attributes); 106 | } else { 107 | $argsString = sprintf('[%s]', implode(', ', $attributes)); 108 | } 109 | 110 | if (null !== $isGranted->getSubject()) { 111 | $argsString = sprintf('%s, %s', $argsString, $isGranted->getSubject()); 112 | } 113 | 114 | return $argsString; 115 | } 116 | 117 | /** 118 | * @return array 119 | */ 120 | public static function getSubscribedEvents() 121 | { 122 | return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments']; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/EventListener/ParamConverterListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 15 | use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterManager; 16 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 17 | use Symfony\Component\HttpFoundation\Request; 18 | use Symfony\Component\HttpKernel\Event\KernelEvent; 19 | use Symfony\Component\HttpKernel\KernelEvents; 20 | 21 | /** 22 | * The ParamConverterListener handles the ParamConverter annotation. 23 | * 24 | * @author Fabien Potencier 25 | */ 26 | class ParamConverterListener implements EventSubscriberInterface 27 | { 28 | /** 29 | * @var ParamConverterManager 30 | */ 31 | private $manager; 32 | 33 | private $autoConvert; 34 | 35 | /** 36 | * @param bool $autoConvert Auto convert non-configured objects 37 | */ 38 | public function __construct(ParamConverterManager $manager, $autoConvert = true) 39 | { 40 | $this->manager = $manager; 41 | $this->autoConvert = $autoConvert; 42 | } 43 | 44 | /** 45 | * Modifies the ParamConverterManager instance. 46 | */ 47 | public function onKernelController(KernelEvent $event) 48 | { 49 | $controller = $event->getController(); 50 | $request = $event->getRequest(); 51 | $configurations = []; 52 | 53 | if ($configuration = $request->attributes->get('_converters')) { 54 | foreach (\is_array($configuration) ? $configuration : [$configuration] as $configuration) { 55 | $configurations[$configuration->getName()] = $configuration; 56 | } 57 | } 58 | 59 | // automatically apply conversion for non-configured objects 60 | if ($this->autoConvert) { 61 | if (\is_array($controller)) { 62 | $r = new \ReflectionMethod($controller[0], $controller[1]); 63 | } elseif (\is_object($controller) && \is_callable([$controller, '__invoke'])) { 64 | $r = new \ReflectionMethod($controller, '__invoke'); 65 | } else { 66 | $r = new \ReflectionFunction($controller); 67 | } 68 | 69 | $configurations = $this->autoConfigure($r, $request, $configurations); 70 | } 71 | 72 | $this->manager->apply($request, $configurations); 73 | } 74 | 75 | private function autoConfigure(\ReflectionFunctionAbstract $r, Request $request, $configurations) 76 | { 77 | foreach ($r->getParameters() as $param) { 78 | $type = $param->getType(); 79 | $class = $this->getParamClassByType($type); 80 | if (null !== $class && $request instanceof $class) { 81 | continue; 82 | } 83 | 84 | $name = $param->getName(); 85 | 86 | if ($type) { 87 | if (!isset($configurations[$name])) { 88 | $configuration = new ParamConverter([]); 89 | $configuration->setName($name); 90 | 91 | $configurations[$name] = $configuration; 92 | } 93 | 94 | if (null !== $class && null === $configurations[$name]->getClass()) { 95 | $configurations[$name]->setClass($class); 96 | } 97 | } 98 | 99 | if (isset($configurations[$name])) { 100 | $configurations[$name]->setIsOptional($param->isOptional() || $param->isDefaultValueAvailable() || ($type && $type->allowsNull())); 101 | } 102 | } 103 | 104 | return $configurations; 105 | } 106 | 107 | private function getParamClassByType(?\ReflectionType $type): ?string 108 | { 109 | if (null === $type) { 110 | return null; 111 | } 112 | 113 | foreach ($type instanceof \ReflectionUnionType ? $type->getTypes() : [$type] as $type) { 114 | if (!$type->isBuiltin()) { 115 | return $type->getName(); 116 | } 117 | } 118 | 119 | return null; 120 | } 121 | 122 | /** 123 | * @return array 124 | */ 125 | public static function getSubscribedEvents() 126 | { 127 | return [ 128 | KernelEvents::CONTROLLER => 'onKernelController', 129 | ]; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/EventListener/SecurityListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Sensio\Bundle\FrameworkExtraBundle\Request\ArgumentNameConverter; 16 | use Sensio\Bundle\FrameworkExtraBundle\Security\ExpressionLanguage; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\HttpKernel\Event\KernelEvent; 19 | use Symfony\Component\HttpKernel\Exception\HttpException; 20 | use Symfony\Component\HttpKernel\KernelEvents; 21 | use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; 22 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 23 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 24 | use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; 25 | use Symfony\Component\Security\Core\Exception\AccessDeniedException; 26 | use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; 27 | 28 | /** 29 | * SecurityListener handles security restrictions on controllers. 30 | * 31 | * @author Fabien Potencier 32 | */ 33 | class SecurityListener implements EventSubscriberInterface 34 | { 35 | private $argumentNameConverter; 36 | private $tokenStorage; 37 | private $authChecker; 38 | private $language; 39 | private $trustResolver; 40 | private $roleHierarchy; 41 | private $logger; 42 | 43 | public function __construct(ArgumentNameConverter $argumentNameConverter, ExpressionLanguage $language = null, AuthenticationTrustResolverInterface $trustResolver = null, RoleHierarchyInterface $roleHierarchy = null, TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authChecker = null, LoggerInterface $logger = null) 44 | { 45 | $this->argumentNameConverter = $argumentNameConverter; 46 | $this->tokenStorage = $tokenStorage; 47 | $this->authChecker = $authChecker; 48 | $this->language = $language; 49 | $this->trustResolver = $trustResolver; 50 | $this->roleHierarchy = $roleHierarchy; 51 | $this->logger = $logger; 52 | } 53 | 54 | public function onKernelControllerArguments(KernelEvent $event) 55 | { 56 | $request = $event->getRequest(); 57 | if (!$configurations = $request->attributes->get('_security')) { 58 | return; 59 | } 60 | 61 | if (null === $this->tokenStorage || null === $this->trustResolver) { 62 | throw new \LogicException('To use the @Security tag, you need to install the Symfony Security bundle.'); 63 | } 64 | 65 | if (null === $this->tokenStorage->getToken()) { 66 | throw new AccessDeniedException('No user token or you forgot to put your controller behind a firewall while using a @Security tag.'); 67 | } 68 | 69 | if (null === $this->language) { 70 | throw new \LogicException('To use the @Security tag, you need to use the Security component 2.4 or newer and install the ExpressionLanguage component.'); 71 | } 72 | 73 | foreach ($configurations as $configuration) { 74 | if (!$this->language->evaluate($configuration->getExpression(), $this->getVariables($event))) { 75 | if ($statusCode = $configuration->getStatusCode()) { 76 | throw new HttpException($statusCode, $configuration->getMessage()); 77 | } 78 | 79 | throw new AccessDeniedException($configuration->getMessage() ?: sprintf('Expression "%s" denied access.', $configuration->getExpression())); 80 | } 81 | } 82 | } 83 | 84 | // code should be sync with Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter 85 | private function getVariables(KernelEvent $event) 86 | { 87 | $request = $event->getRequest(); 88 | $token = $this->tokenStorage->getToken(); 89 | $variables = [ 90 | 'token' => $token, 91 | 'user' => $token->getUser(), 92 | 'object' => $request, 93 | 'subject' => $request, 94 | 'request' => $request, 95 | 'roles' => $this->getRoles($token), 96 | 'trust_resolver' => $this->trustResolver, 97 | // needed for the is_granted expression function 98 | 'auth_checker' => $this->authChecker, 99 | ]; 100 | 101 | $controllerArguments = $this->argumentNameConverter->getControllerArguments($event); 102 | 103 | if ($diff = array_intersect(array_keys($variables), array_keys($controllerArguments))) { 104 | foreach ($diff as $key => $variableName) { 105 | if ($variables[$variableName] === $controllerArguments[$variableName]) { 106 | unset($diff[$key]); 107 | } 108 | } 109 | 110 | if ($diff) { 111 | $singular = 1 === \count($diff); 112 | if (null !== $this->logger) { 113 | $this->logger->warning(sprintf('Controller argument%s "%s" collided with the built-in security expression variables. The built-in value%s are being used for the @Security expression.', $singular ? '' : 's', implode('", "', $diff), $singular ? 's' : '')); 114 | } 115 | } 116 | } 117 | 118 | // controller variables should also be accessible 119 | return array_merge($controllerArguments, $variables); 120 | } 121 | 122 | private function getRoles(TokenInterface $token): array 123 | { 124 | if (method_exists($this->roleHierarchy, 'getReachableRoleNames')) { 125 | if (null !== $this->roleHierarchy) { 126 | $roles = $this->roleHierarchy->getReachableRoleNames($token->getRoleNames()); 127 | } else { 128 | $roles = $token->getRoleNames(); 129 | } 130 | } else { 131 | if (null !== $this->roleHierarchy) { 132 | $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); 133 | } else { 134 | $roles = $token->getRoles(); 135 | } 136 | 137 | $roles = array_map(function ($role) { 138 | return $role->getRole(); 139 | }, $roles); 140 | } 141 | 142 | return $roles; 143 | } 144 | 145 | /** 146 | * @return array 147 | */ 148 | public static function getSubscribedEvents() 149 | { 150 | return [KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments']; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/EventListener/TemplateListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\EventListener; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 16 | use Sensio\Bundle\FrameworkExtraBundle\Templating\TemplateGuesser; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpFoundation\Response; 20 | use Symfony\Component\HttpFoundation\StreamedResponse; 21 | use Symfony\Component\HttpKernel\Event\KernelEvent; 22 | use Symfony\Component\HttpKernel\KernelEvents; 23 | use Symfony\Contracts\Service\ServiceSubscriberInterface; 24 | use Twig\Environment; 25 | 26 | /** 27 | * Handles the Template annotation for actions. 28 | * 29 | * Depends on pre-processing of the ControllerListener. 30 | * 31 | * @author Fabien Potencier 32 | */ 33 | class TemplateListener implements EventSubscriberInterface, ServiceSubscriberInterface 34 | { 35 | private $templateGuesser; 36 | private $twig; 37 | private $container; 38 | 39 | public function __construct(TemplateGuesser $templateGuesser, Environment $twig = null) 40 | { 41 | $this->templateGuesser = $templateGuesser; 42 | $this->twig = $twig; 43 | } 44 | 45 | public function setContainer(ContainerInterface $container): void 46 | { 47 | $this->container = $container; 48 | } 49 | 50 | /** 51 | * Guesses the template name to render and its variables and adds them to 52 | * the request object. 53 | */ 54 | public function onKernelController(KernelEvent $event) 55 | { 56 | $request = $event->getRequest(); 57 | $template = $request->attributes->get('_template'); 58 | 59 | if (!$template instanceof Template) { 60 | return; 61 | } 62 | 63 | $controller = $event->getController(); 64 | if (!\is_array($controller) && method_exists($controller, '__invoke')) { 65 | $controller = [$controller, '__invoke']; 66 | } 67 | $template->setOwner($controller); 68 | 69 | // when no template has been given, try to resolve it based on the controller 70 | if (null === $template->getTemplate()) { 71 | $template->setTemplate($this->templateGuesser->guessTemplateName($controller, $request)); 72 | } 73 | } 74 | 75 | /** 76 | * Renders the template and initializes a new response object with the 77 | * rendered template content. 78 | */ 79 | public function onKernelView(KernelEvent $event) 80 | { 81 | /* @var Template $template */ 82 | $request = $event->getRequest(); 83 | $template = $request->attributes->get('_template'); 84 | 85 | if (!$template instanceof Template) { 86 | return; 87 | } 88 | 89 | if (null === $this->twig) { 90 | if (!$this->container || !$this->container->has('twig')) { 91 | throw new \LogicException('You can not use the "@Template" annotation if the Twig Bundle is not available.'); 92 | } 93 | 94 | $this->twig = $this->container->get('twig'); 95 | } 96 | 97 | $parameters = $event->getControllerResult(); 98 | $owner = $template->getOwner(); 99 | list($controller, $action) = $owner; 100 | 101 | // when the annotation declares no default vars and the action returns 102 | // null, all action method arguments are used as default vars 103 | if (null === $parameters) { 104 | $parameters = $this->resolveDefaultParameters($request, $template, $controller, $action); 105 | } 106 | 107 | // attempt to render the actual response 108 | if ($template->isStreamable()) { 109 | $callback = function () use ($template, $parameters) { 110 | $this->twig->display($template->getTemplate(), $parameters); 111 | }; 112 | 113 | $event->setResponse(new StreamedResponse($callback)); 114 | } else { 115 | $event->setResponse(new Response($this->twig->render($template->getTemplate(), $parameters))); 116 | } 117 | 118 | // make sure the owner (controller+dependencies) is not cached or stored elsewhere 119 | $template->setOwner([]); 120 | } 121 | 122 | /** 123 | * @return array 124 | */ 125 | public static function getSubscribedEvents() 126 | { 127 | return [ 128 | KernelEvents::CONTROLLER => ['onKernelController', -128], 129 | KernelEvents::VIEW => 'onKernelView', 130 | ]; 131 | } 132 | 133 | public static function getSubscribedServices(): array 134 | { 135 | return ['twig' => '?'.Environment::class]; 136 | } 137 | 138 | private function resolveDefaultParameters(Request $request, Template $template, $controller, $action) 139 | { 140 | $parameters = []; 141 | $arguments = $template->getVars(); 142 | 143 | if (0 === \count($arguments)) { 144 | $r = new \ReflectionObject($controller); 145 | 146 | $arguments = []; 147 | foreach ($r->getMethod($action)->getParameters() as $param) { 148 | $arguments[] = $param; 149 | } 150 | } 151 | 152 | // fetch the arguments of @Template.vars or everything if desired 153 | // and assign them to the designated template 154 | foreach ($arguments as $argument) { 155 | if ($argument instanceof \ReflectionParameter) { 156 | $parameters[$name = $argument->getName()] = !$request->attributes->has($name) && $argument->isDefaultValueAvailable() ? $argument->getDefaultValue() : $request->attributes->get($name); 157 | } else { 158 | $parameters[$argument] = $request->attributes->get($argument); 159 | } 160 | } 161 | 162 | return $parameters; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Request/ArgumentNameConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Request; 13 | 14 | use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; 15 | use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; 16 | 17 | /** 18 | * @author Ryan Weaver 19 | */ 20 | class ArgumentNameConverter 21 | { 22 | private $argumentMetadataFactory; 23 | 24 | public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory) 25 | { 26 | $this->argumentMetadataFactory = $argumentMetadataFactory; 27 | } 28 | 29 | /** 30 | * Returns an associative array of the controller arguments for the event. 31 | * 32 | * @return array 33 | */ 34 | public function getControllerArguments(ControllerArgumentsEvent $event) 35 | { 36 | $namedArguments = $event->getRequest()->attributes->all(); 37 | $argumentMetadatas = $this->argumentMetadataFactory->createArgumentMetadata($event->getController()); 38 | $controllerArguments = $event->getArguments(); 39 | 40 | foreach ($argumentMetadatas as $index => $argumentMetadata) { 41 | if ($argumentMetadata->isVariadic()) { 42 | // set the rest of the arguments as this arg's value 43 | $namedArguments[$argumentMetadata->getName()] = \array_slice($controllerArguments, $index); 44 | 45 | break; 46 | } 47 | 48 | if (!\array_key_exists($index, $controllerArguments)) { 49 | throw new \LogicException(sprintf('Could not find an argument value for argument %d of the controller.', $index)); 50 | } 51 | 52 | $namedArguments[$argumentMetadata->getName()] = $controllerArguments[$index]; 53 | } 54 | 55 | return $namedArguments; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Request/ParamConverter/DateTimeParamConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 17 | 18 | /** 19 | * Convert DateTime instances from request attribute variable. 20 | * 21 | * @author Benjamin Eberlei 22 | */ 23 | class DateTimeParamConverter implements ParamConverterInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | * 28 | * @throws NotFoundHttpException When invalid date given 29 | */ 30 | public function apply(Request $request, ParamConverter $configuration) 31 | { 32 | $param = $configuration->getName(); 33 | 34 | if (!$request->attributes->has($param)) { 35 | return false; 36 | } 37 | 38 | $options = $configuration->getOptions(); 39 | $value = $request->attributes->get($param); 40 | 41 | if (!$value && $configuration->isOptional()) { 42 | $request->attributes->set($param, null); 43 | 44 | return true; 45 | } 46 | 47 | $class = $configuration->getClass(); 48 | 49 | if (isset($options['format'])) { 50 | $date = $class::createFromFormat($options['format'], $value); 51 | 52 | $errors = \DateTime::getLastErrors() ?: ['warning_count' => 0]; 53 | 54 | if (0 < $errors['warning_count']) { 55 | $date = false; 56 | } 57 | 58 | if (!$date) { 59 | throw new NotFoundHttpException(sprintf('Invalid date given for parameter "%s".', $param)); 60 | } 61 | } else { 62 | $valueIsInt = filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]); 63 | if (false !== $valueIsInt) { 64 | $date = (new $class())->setTimestamp($value); 65 | } else { 66 | if (false === strtotime($value)) { 67 | throw new NotFoundHttpException(sprintf('Invalid date given for parameter "%s".', $param)); 68 | } 69 | 70 | $date = new $class($value); 71 | } 72 | } 73 | 74 | $request->attributes->set($param, $date); 75 | 76 | return true; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function supports(ParamConverter $configuration) 83 | { 84 | if (null === $configuration->getClass()) { 85 | return false; 86 | } 87 | 88 | return is_subclass_of($configuration->getClass(), \DateTimeInterface::class); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Request/ParamConverter/DoctrineParamConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; 13 | 14 | use Doctrine\DBAL\Types\ConversionException; 15 | use Doctrine\ORM\EntityManagerInterface; 16 | use Doctrine\ORM\NoResultException; 17 | use Doctrine\Persistence\ManagerRegistry; 18 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 19 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 20 | use Symfony\Component\ExpressionLanguage\SyntaxError; 21 | use Symfony\Component\HttpFoundation\Request; 22 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 23 | 24 | /** 25 | * DoctrineParamConverter. 26 | * 27 | * @author Fabien Potencier 28 | */ 29 | class DoctrineParamConverter implements ParamConverterInterface 30 | { 31 | /** 32 | * @var ManagerRegistry 33 | */ 34 | private $registry; 35 | 36 | /** 37 | * @var ExpressionLanguage 38 | */ 39 | private $language; 40 | 41 | /** 42 | * @var array 43 | */ 44 | private $defaultOptions; 45 | 46 | public function __construct(ManagerRegistry $registry = null, ExpressionLanguage $expressionLanguage = null, array $options = []) 47 | { 48 | $this->registry = $registry; 49 | $this->language = $expressionLanguage; 50 | 51 | $defaultValues = [ 52 | 'entity_manager' => null, 53 | 'exclude' => [], 54 | 'mapping' => [], 55 | 'strip_null' => false, 56 | 'expr' => null, 57 | 'id' => null, 58 | 'repository_method' => null, 59 | 'map_method_signature' => false, 60 | 'evict_cache' => false, 61 | ]; 62 | 63 | $this->defaultOptions = array_merge($defaultValues, $options); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | * 69 | * @throws \LogicException When unable to guess how to get a Doctrine instance from the request information 70 | * @throws NotFoundHttpException When object not found 71 | */ 72 | public function apply(Request $request, ParamConverter $configuration) 73 | { 74 | $name = $configuration->getName(); 75 | $class = $configuration->getClass(); 76 | $options = $this->getOptions($configuration); 77 | 78 | if (null === $request->attributes->get($name, false)) { 79 | $configuration->setIsOptional(true); 80 | } 81 | 82 | $errorMessage = null; 83 | if ($expr = $options['expr']) { 84 | $object = $this->findViaExpression($class, $request, $expr, $options, $configuration); 85 | 86 | if (null === $object) { 87 | $errorMessage = sprintf('The expression "%s" returned null', $expr); 88 | } 89 | 90 | // find by identifier? 91 | } elseif (false === $object = $this->find($class, $request, $options, $name)) { 92 | // find by criteria 93 | if (false === $object = $this->findOneBy($class, $request, $options)) { 94 | if ($configuration->isOptional()) { 95 | $object = null; 96 | } else { 97 | throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name)); 98 | } 99 | } 100 | } 101 | 102 | if (null === $object && false === $configuration->isOptional()) { 103 | $message = sprintf('%s object not found by the @%s annotation.', $class, $this->getAnnotationName($configuration)); 104 | if ($errorMessage) { 105 | $message .= ' '.$errorMessage; 106 | } 107 | throw new NotFoundHttpException($message); 108 | } 109 | 110 | $request->attributes->set($name, $object); 111 | 112 | return true; 113 | } 114 | 115 | private function find($class, Request $request, $options, $name) 116 | { 117 | if ($options['mapping'] || $options['exclude']) { 118 | return false; 119 | } 120 | 121 | $id = $this->getIdentifier($request, $options, $name); 122 | 123 | if (false === $id || null === $id) { 124 | return false; 125 | } 126 | 127 | if ($options['repository_method']) { 128 | $method = $options['repository_method']; 129 | } else { 130 | $method = 'find'; 131 | } 132 | 133 | $om = $this->getManager($options['entity_manager'], $class); 134 | if ($options['evict_cache'] && $om instanceof EntityManagerInterface) { 135 | $cacheProvider = $om->getCache(); 136 | if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) { 137 | $cacheProvider->evictEntity($class, $id); 138 | } 139 | } 140 | 141 | try { 142 | return $om->getRepository($class)->$method($id); 143 | } catch (NoResultException $e) { 144 | return; 145 | } catch (ConversionException $e) { 146 | return; 147 | } 148 | } 149 | 150 | private function getIdentifier(Request $request, $options, $name) 151 | { 152 | if (null !== $options['id']) { 153 | if (!\is_array($options['id'])) { 154 | $name = $options['id']; 155 | } elseif (\is_array($options['id'])) { 156 | $id = []; 157 | foreach ($options['id'] as $field) { 158 | if (false !== strstr($field, '%s')) { 159 | // Convert "%s_uuid" to "foobar_uuid" 160 | $field = sprintf($field, $name); 161 | } 162 | $id[$field] = $request->attributes->get($field); 163 | } 164 | 165 | return $id; 166 | } 167 | } 168 | 169 | if ($request->attributes->has($name)) { 170 | return $request->attributes->get($name); 171 | } 172 | 173 | if ($request->attributes->has('id') && !$options['id']) { 174 | return $request->attributes->get('id'); 175 | } 176 | 177 | return false; 178 | } 179 | 180 | private function findOneBy($class, Request $request, $options) 181 | { 182 | if (!$options['mapping']) { 183 | $keys = $request->attributes->keys(); 184 | $options['mapping'] = $keys ? array_combine($keys, $keys) : []; 185 | } 186 | 187 | foreach ($options['exclude'] as $exclude) { 188 | unset($options['mapping'][$exclude]); 189 | } 190 | 191 | if (!$options['mapping']) { 192 | return false; 193 | } 194 | 195 | // if a specific id has been defined in the options and there is no corresponding attribute 196 | // return false in order to avoid a fallback to the id which might be of another object 197 | if ($options['id'] && null === $request->attributes->get($options['id'])) { 198 | return false; 199 | } 200 | 201 | $criteria = []; 202 | $em = $this->getManager($options['entity_manager'], $class); 203 | $metadata = $em->getClassMetadata($class); 204 | 205 | $mapMethodSignature = $options['repository_method'] 206 | && $options['map_method_signature'] 207 | && true === $options['map_method_signature']; 208 | 209 | foreach ($options['mapping'] as $attribute => $field) { 210 | if ($metadata->hasField($field) 211 | || ($metadata->hasAssociation($field) && $metadata->isSingleValuedAssociation($field)) 212 | || $mapMethodSignature) { 213 | $criteria[$field] = $request->attributes->get($attribute); 214 | } 215 | } 216 | 217 | if ($options['strip_null']) { 218 | $criteria = array_filter($criteria, function ($value) { 219 | return null !== $value; 220 | }); 221 | } 222 | 223 | if (!$criteria) { 224 | return false; 225 | } 226 | 227 | if ($options['repository_method']) { 228 | $repositoryMethod = $options['repository_method']; 229 | } else { 230 | $repositoryMethod = 'findOneBy'; 231 | } 232 | 233 | try { 234 | if ($mapMethodSignature) { 235 | return $this->findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria); 236 | } 237 | 238 | return $em->getRepository($class)->$repositoryMethod($criteria); 239 | } catch (NoResultException $e) { 240 | return; 241 | } catch (ConversionException $e) { 242 | return; 243 | } 244 | } 245 | 246 | private function findDataByMapMethodSignature($em, $class, $repositoryMethod, $criteria) 247 | { 248 | $arguments = []; 249 | $repository = $em->getRepository($class); 250 | $ref = new \ReflectionMethod($repository, $repositoryMethod); 251 | foreach ($ref->getParameters() as $parameter) { 252 | if (\array_key_exists($parameter->name, $criteria)) { 253 | $arguments[] = $criteria[$parameter->name]; 254 | } elseif ($parameter->isDefaultValueAvailable()) { 255 | $arguments[] = $parameter->getDefaultValue(); 256 | } else { 257 | throw new \InvalidArgumentException(sprintf('Repository method "%s::%s" requires that you provide a value for the "$%s" argument.', \get_class($repository), $repositoryMethod, $parameter->name)); 258 | } 259 | } 260 | 261 | return $ref->invokeArgs($repository, $arguments); 262 | } 263 | 264 | private function findViaExpression($class, Request $request, $expression, $options, ParamConverter $configuration) 265 | { 266 | if (null === $this->language) { 267 | throw new \LogicException(sprintf('To use the @%s tag with the "expr" option, you need to install the ExpressionLanguage component.', $this->getAnnotationName($configuration))); 268 | } 269 | 270 | $repository = $this->getManager($options['entity_manager'], $class)->getRepository($class); 271 | $variables = array_merge($request->attributes->all(), ['repository' => $repository]); 272 | 273 | try { 274 | return $this->language->evaluate($expression, $variables); 275 | } catch (NoResultException $e) { 276 | return; 277 | } catch (ConversionException $e) { 278 | return; 279 | } catch (SyntaxError $e) { 280 | throw new \LogicException(sprintf('Error parsing expression -- "%s" -- (%s).', $expression, $e->getMessage()), 0, $e); 281 | } 282 | } 283 | 284 | /** 285 | * {@inheritdoc} 286 | */ 287 | public function supports(ParamConverter $configuration) 288 | { 289 | // if there is no manager, this means that only Doctrine DBAL is configured 290 | if (null === $this->registry || !\count($this->registry->getManagerNames())) { 291 | return false; 292 | } 293 | 294 | if (null === $configuration->getClass()) { 295 | return false; 296 | } 297 | 298 | $options = $this->getOptions($configuration, false); 299 | 300 | // Doctrine Entity? 301 | $em = $this->getManager($options['entity_manager'], $configuration->getClass()); 302 | if (null === $em) { 303 | return false; 304 | } 305 | 306 | return !$em->getMetadataFactory()->isTransient($configuration->getClass()); 307 | } 308 | 309 | private function getOptions(ParamConverter $configuration, $strict = true) 310 | { 311 | $passedOptions = $configuration->getOptions(); 312 | 313 | if (isset($passedOptions['repository_method'])) { 314 | @trigger_error('The repository_method option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', \E_USER_DEPRECATED); 315 | } 316 | 317 | if (isset($passedOptions['map_method_signature'])) { 318 | @trigger_error('The map_method_signature option of @ParamConverter is deprecated and will be removed in 6.0. Use the expr option or @Entity.', \E_USER_DEPRECATED); 319 | } 320 | 321 | $extraKeys = array_diff(array_keys($passedOptions), array_keys($this->defaultOptions)); 322 | if ($extraKeys && $strict) { 323 | throw new \InvalidArgumentException(sprintf('Invalid option(s) passed to @%s: "%s".', $this->getAnnotationName($configuration), implode(', ', $extraKeys))); 324 | } 325 | 326 | return array_replace($this->defaultOptions, $passedOptions); 327 | } 328 | 329 | private function getManager($name, $class) 330 | { 331 | if (null === $name) { 332 | return $this->registry->getManagerForClass($class); 333 | } 334 | 335 | return $this->registry->getManager($name); 336 | } 337 | 338 | private function getAnnotationName(ParamConverter $configuration) 339 | { 340 | $r = new \ReflectionClass($configuration); 341 | 342 | return $r->getShortName(); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/Request/ParamConverter/ParamConverterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 15 | use Symfony\Component\HttpFoundation\Request; 16 | 17 | /** 18 | * Converts request parameters to objects and stores them as request 19 | * attributes, so they can be injected as controller method arguments. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | interface ParamConverterInterface 24 | { 25 | /** 26 | * Stores the object in the request. 27 | * 28 | * @param ParamConverter $configuration Contains the name, class and options of the object 29 | * 30 | * @return bool True if the object has been successfully set, else false 31 | */ 32 | public function apply(Request $request, ParamConverter $configuration); 33 | 34 | /** 35 | * Checks if the object is supported. 36 | * 37 | * @return bool True if the object is supported, else false 38 | */ 39 | public function supports(ParamConverter $configuration); 40 | } 41 | -------------------------------------------------------------------------------- /src/Request/ParamConverter/ParamConverterManager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 15 | use Symfony\Component\HttpFoundation\Request; 16 | 17 | /** 18 | * Managers converters. 19 | * 20 | * @author Fabien Potencier 21 | * @author Henrik Bjornskov 22 | */ 23 | class ParamConverterManager 24 | { 25 | /** 26 | * @var array 27 | */ 28 | private $converters = []; 29 | 30 | /** 31 | * @var array 32 | */ 33 | private $namedConverters = []; 34 | 35 | /** 36 | * Applies all converters to the passed configurations and stops when a 37 | * converter is applied it will move on to the next configuration and so on. 38 | * 39 | * @param array|object $configurations 40 | */ 41 | public function apply(Request $request, $configurations) 42 | { 43 | if (\is_object($configurations)) { 44 | $configurations = [$configurations]; 45 | } 46 | 47 | foreach ($configurations as $configuration) { 48 | $this->applyConverter($request, $configuration); 49 | } 50 | } 51 | 52 | /** 53 | * Applies converter on request based on the given configuration. 54 | */ 55 | private function applyConverter(Request $request, ParamConverter $configuration) 56 | { 57 | $value = $request->attributes->get($configuration->getName()); 58 | $className = $configuration->getClass(); 59 | 60 | // If the value is already an instance of the class we are trying to convert it into 61 | // we should continue as no conversion is required 62 | if (\is_object($value) && $value instanceof $className) { 63 | return; 64 | } 65 | 66 | if ($converterName = $configuration->getConverter()) { 67 | if (!isset($this->namedConverters[$converterName])) { 68 | throw new \RuntimeException(sprintf("No converter named '%s' found for conversion of parameter '%s'.", $converterName, $configuration->getName())); 69 | } 70 | 71 | $converter = $this->namedConverters[$converterName]; 72 | 73 | if (!$converter->supports($configuration)) { 74 | throw new \RuntimeException(sprintf("Converter '%s' does not support conversion of parameter '%s'.", $converterName, $configuration->getName())); 75 | } 76 | 77 | $converter->apply($request, $configuration); 78 | 79 | return; 80 | } 81 | 82 | foreach ($this->all() as $converter) { 83 | if ($converter->supports($configuration)) { 84 | if ($converter->apply($request, $configuration)) { 85 | return; 86 | } 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Adds a parameter converter. 93 | * 94 | * Converters match either explicitly via $name or by iteration over all 95 | * converters with a $priority. If you pass a $priority = null then the 96 | * added converter will not be part of the iteration chain and can only 97 | * be invoked explicitly. 98 | * 99 | * @param int $priority the priority (between -10 and 10) 100 | * @param string $name name of the converter 101 | */ 102 | public function add(ParamConverterInterface $converter, $priority = 0, $name = null) 103 | { 104 | if (null !== $priority) { 105 | if (!isset($this->converters[$priority])) { 106 | $this->converters[$priority] = []; 107 | } 108 | 109 | $this->converters[$priority][] = $converter; 110 | } 111 | 112 | if (null !== $name) { 113 | $this->namedConverters[$name] = $converter; 114 | } 115 | } 116 | 117 | /** 118 | * Returns all registered param converters. 119 | * 120 | * @return array An array of param converters 121 | */ 122 | public function all() 123 | { 124 | krsort($this->converters); 125 | 126 | $converters = []; 127 | foreach ($this->converters as $all) { 128 | $converters = array_merge($converters, $all); 129 | } 130 | 131 | return $converters; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Resources/config/annotations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Resources/config/cache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Resources/config/converters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/routing-4.4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | The "%service_id%" service is deprecated since version 5.2 12 | 13 | 14 | 15 | 16 | 17 | 18 | The "%service_id%" service is deprecated since version 5.2 19 | 20 | 21 | 22 | 23 | 24 | 25 | The "%service_id%" service is deprecated since version 5.2 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | The "%service_id%" service is deprecated since version 5.2 12 | 13 | 14 | 15 | 16 | 17 | 18 | The "%service_id%" service is deprecated since version 5.2 19 | 20 | 21 | 22 | 23 | 24 | 25 | The "%service_id%" service is deprecated since version 5.2 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/security.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Resources/config/view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Resources/doc/annotations/cache.rst: -------------------------------------------------------------------------------- 1 | @Cache 2 | ====== 3 | 4 | The ``@Cache`` annotation allows to define HTTP caching headers for expiration 5 | and validation. 6 | 7 | HTTP Expiration Strategies 8 | -------------------------- 9 | 10 | The ``@Cache`` annotation allows to define HTTP caching: 11 | 12 | .. configuration-block:: 13 | 14 | .. code-block:: php-annotations 15 | 16 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 17 | 18 | /** 19 | * @Cache(expires="tomorrow", public=true) 20 | */ 21 | public function index() 22 | { 23 | } 24 | 25 | .. code-block:: php-attributes 26 | 27 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 28 | 29 | #[Cache(expires: 'tomorrow', public: true)] 30 | public function index() 31 | { 32 | } 33 | 34 | You can also use the annotation on a class to define caching for all actions 35 | of a controller: 36 | 37 | .. configuration-block:: 38 | 39 | .. code-block:: php-annotations 40 | 41 | /** 42 | * @Cache(expires="tomorrow", public=true) 43 | */ 44 | class BlogController extends Controller 45 | { 46 | } 47 | 48 | .. code-block:: php-attributes 49 | 50 | #[Cache(expires: 'tomorrow', public: true)] 51 | class BlogController extends Controller 52 | { 53 | } 54 | 55 | When there is a conflict between the class configuration and the method 56 | configuration, the latter overrides the former: 57 | 58 | .. configuration-block:: 59 | 60 | .. code-block:: php-annotations 61 | 62 | /** 63 | * @Cache(expires="tomorrow") 64 | */ 65 | class BlogController extends Controller 66 | { 67 | /** 68 | * @Cache(expires="+2 days") 69 | */ 70 | public function index() 71 | { 72 | } 73 | } 74 | 75 | .. code-block:: php-attributes 76 | 77 | #[Cache(expires: 'tomorrow')] 78 | class BlogController extends Controller 79 | { 80 | #[Cache(expires: '+2 days')] 81 | public function index() 82 | { 83 | } 84 | } 85 | 86 | 87 | 88 | .. note:: 89 | 90 | The ``expires`` attribute takes any valid date understood by the PHP 91 | ``strtotime()`` function. 92 | 93 | HTTP Validation Strategies 94 | -------------------------- 95 | 96 | The ``lastModified`` and ``Etag`` attributes manage the HTTP validation cache 97 | headers. ``lastModified`` adds a ``Last-Modified`` header to Responses and 98 | ``Etag`` adds an ``Etag`` header. 99 | 100 | Both automatically trigger the logic to return a 304 response when the 101 | response is not modified (in this case, the controller is **not** called): 102 | 103 | .. configuration-block:: 104 | 105 | .. code-block:: php-annotations 106 | 107 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 108 | 109 | /** 110 | * @Cache(lastModified="post.getUpdatedAt()", Etag="'Post' ~ post.getId() ~ post.getUpdatedAt().getTimestamp()") 111 | */ 112 | public function index(Post $post) 113 | { 114 | // your code 115 | // won't be called in case of a 304 116 | } 117 | 118 | .. code-block:: php-attributes 119 | 120 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 121 | 122 | #[Cache(lastModified: 'post.getUpdatedAt()', etag: "'Post' ~ post.getId() ~ post.getUpdatedAt().getTimestamp()")] 123 | public function index(Post $post) 124 | { 125 | // your code 126 | // won't be called in case of a 304 127 | } 128 | 129 | It's roughly doing the same as the following code:: 130 | 131 | public function my(Request $request, Post $post) 132 | { 133 | $response = new Response(); 134 | $response->setLastModified($post->getUpdatedAt()); 135 | if ($response->isNotModified($request)) { 136 | return $response; 137 | } 138 | 139 | // your code 140 | } 141 | 142 | .. note:: 143 | 144 | The Etag HTTP header value is the result of the expression hashed with the 145 | ``sha256`` algorithm. 146 | 147 | Attributes 148 | ---------- 149 | 150 | Here is a list of accepted attributes and their HTTP header equivalent: 151 | 152 | ======================================================================= =================================================================== 153 | Annotation Response Method 154 | ======================================================================= =================================================================== 155 | ``@Cache(expires="tomorrow")`` ``$response->setExpires()`` 156 | ``@Cache(smaxage="15")`` ``$response->setSharedMaxAge()`` 157 | ``@Cache(maxage="15")`` ``$response->setMaxAge()`` 158 | ``@Cache(maxstale="15")`` ``$response->headers->addCacheControlDirective('max-stale', 15)`` 159 | ``@Cache(staleWhileRevalidate="15")`` ``$response->headers->addCacheControlDirective('stale-while-revalidate', 15)`` 160 | ``@Cache(staleIfError="15")`` ``$response->headers->addCacheControlDirective('stale-if-error', 15)`` 161 | ``@Cache(vary={"Cookie"})`` ``$response->setVary()`` 162 | ``@Cache(public=true)`` ``$response->setPublic()`` 163 | ``@Cache(lastModified="post.getUpdatedAt()")`` ``$response->setLastModified()`` 164 | ``@Cache(Etag="post.getId() ~ post.getUpdatedAt().getTimestamp()")`` ``$response->setEtag()`` 165 | ``@Cache(mustRevalidate=true)`` ``$response->headers->addCacheControlDirective('must-revalidate')`` 166 | ======================================================================= =================================================================== 167 | 168 | .. note:: 169 | 170 | ``smaxage``, ``maxage`` and ``maxstale`` attributes can also get a string 171 | with relative time format (``1 day``, ``2 weeks``, ...). 172 | -------------------------------------------------------------------------------- /src/Resources/doc/annotations/converters.rst: -------------------------------------------------------------------------------- 1 | @ParamConverter 2 | =============== 3 | 4 | Usage 5 | ----- 6 | 7 | The ``@ParamConverter`` annotation calls *converters* to convert request 8 | parameters to objects. These objects are stored as request attributes and so 9 | they can be injected as controller method arguments: 10 | 11 | .. configuration-block:: 12 | 13 | .. code-block:: php-annotations 14 | 15 | use Symfony\Component\Routing\Annotation\Route; 16 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 17 | 18 | /** 19 | * @Route("/blog/{id}") 20 | * @ParamConverter("post", class="SensioBlogBundle:Post") 21 | */ 22 | public function show(Post $post) 23 | { 24 | } 25 | 26 | .. code-block:: php-attributes 27 | 28 | use Symfony\Component\Routing\Annotation\Route; 29 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 30 | 31 | #[Route('/blog/{id}')] 32 | #[ParamConverter('post', class: 'SensioBlogBundle:Post')] 33 | public function show(Post $post) 34 | { 35 | } 36 | 37 | Several things happen under the hood: 38 | 39 | * The converter tries to get a ``SensioBlogBundle:Post`` object from the 40 | request attributes (request attributes comes from route placeholders -- here 41 | ``id``); 42 | 43 | * If no ``Post`` object is found, a ``404`` Response is generated; 44 | 45 | * If a ``Post`` object is found, a new ``post`` request attribute is defined 46 | (accessible via ``$request->attributes->get('post')``); 47 | 48 | * As for other request attributes, it is automatically injected in the 49 | controller when present in the method signature. 50 | 51 | If you use type hinting as in the example above, you can even omit the 52 | ``@ParamConverter`` annotation:: 53 | 54 | // automatic with method signature 55 | public function show(Post $post) 56 | { 57 | } 58 | 59 | You can disable the auto-conversion of type-hinted method arguments feature 60 | by setting the ``auto_convert`` flag to ``false``: 61 | 62 | .. code-block:: yaml 63 | 64 | # config/packages/sensio_framework_extra.yaml 65 | sensio_framework_extra: 66 | request: 67 | converters: true 68 | auto_convert: false 69 | 70 | You can also explicitly disable some converters by name: 71 | 72 | .. code-block:: yaml 73 | 74 | # config/packages/sensio_framework_extra.yaml 75 | sensio_framework_extra: 76 | request: 77 | converters: true 78 | disable: ['doctrine.orm', 'datetime'] 79 | 80 | To detect which converters are run on a parameter, the following process is 81 | run: 82 | 83 | * If an explicit converter choice was made with 84 | ``@ParamConverter(converter="name")`` the converter with the given name is 85 | chosen. 86 | 87 | * Otherwise all registered parameter converters are iterated by priority. The 88 | ``supports()`` method is invoked to check if a param converter can convert 89 | the request into the required parameter. If it returns ``true`` the param 90 | converter is invoked. 91 | 92 | Built-in Converters 93 | ------------------- 94 | 95 | The bundle has two built-in converters, the Doctrine one and a DateTime 96 | converter. 97 | 98 | Doctrine Converter 99 | ~~~~~~~~~~~~~~~~~~ 100 | 101 | Converter Name: ``doctrine.orm`` 102 | 103 | The Doctrine Converter attempts to convert request attributes to Doctrine 104 | entities fetched from the database. Several different approaches are possible: 105 | 106 | 1) Fetch Automatically 107 | ...................... 108 | 109 | If your route wildcards match properties on your entity, then the converter 110 | will automatically fetch them: 111 | 112 | .. configuration-block:: 113 | 114 | .. code-block:: php-annotations 115 | 116 | /** 117 | * Fetch via primary key because {id} is in the route. 118 | * 119 | * @Route("/blog/{id}") 120 | */ 121 | public function showByPk(Post $post) 122 | { 123 | } 124 | 125 | /** 126 | * Perform a findOneBy() where the slug property matches {slug}. 127 | * 128 | * @Route("/blog/{slug}") 129 | */ 130 | public function show(Post $post) 131 | { 132 | } 133 | 134 | .. code-block:: php-attributes 135 | 136 | /** 137 | * Fetch via primary key because {id} is in the route. 138 | */ 139 | #[Route('/blog/{id}')] 140 | public function showByPk(Post $post) 141 | { 142 | } 143 | 144 | /** 145 | * Perform a findOneBy() where the slug property matches {slug}. 146 | */ 147 | #[Route('/blog/{slug}')] 148 | public function show(Post $post) 149 | { 150 | } 151 | 152 | Automatic fetching works in these situations: 153 | 154 | * If ``{id}`` is in your route, then this is used to fetch by 155 | primary key via the ``find()`` method. 156 | 157 | * The converter will attempt to do a ``findOneBy()`` fetch by using 158 | *all* of the wildcards in your route that are actually properties 159 | on your entity (non-properties are ignored). 160 | 161 | You can control this behavior by actually *adding* the ``@ParamConverter`` 162 | annotation and using the `@ParamConverter options`_. 163 | 164 | 2) Fetch via an Expression 165 | .......................... 166 | 167 | If automatic fetching doesn't work, use an expression: 168 | 169 | .. configuration-block:: 170 | 171 | .. code-block:: php-annotations 172 | 173 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; 174 | 175 | /** 176 | * @Route("/blog/{post_id}") 177 | * @Entity("post", expr="repository.find(post_id)") 178 | */ 179 | public function show(Post $post) 180 | { 181 | } 182 | 183 | .. code-block:: php-attributes 184 | 185 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity; 186 | 187 | #[Route('/blog/{post_id}')] 188 | #[Entity('post', expr: 'repository.find(post_id)')] 189 | public function show(Post $post) 190 | { 191 | } 192 | 193 | Use the special ``@Entity`` annotation with an ``expr`` option to 194 | fetch the object by calling a method on your repository. The 195 | ``repository`` method will be your entity's Repository class and 196 | any route wildcards - like ``{post_id}`` are available as variables. 197 | 198 | .. tip:: 199 | 200 | The ``@Entity`` annotation is a shortcut for using ``expr`` 201 | and has all the same options as ``@ParamConverter``. 202 | 203 | This can also be used to help resolve multiple arguments: 204 | 205 | .. configuration-block:: 206 | 207 | .. code-block:: php-annotations 208 | 209 | /** 210 | * @Route("/blog/{id}/comments/{comment_id}") 211 | * @Entity("comment", expr="repository.find(comment_id)") 212 | */ 213 | public function show(Post $post, Comment $comment) 214 | { 215 | } 216 | 217 | .. code-block:: php-attributes 218 | 219 | #[Route('/blog/{id}/comments/{comment_id}')] 220 | #[Entity('comment', expr: 'repository.find(comment_id)')] 221 | public function show(Post $post, Comment $comment) 222 | { 223 | } 224 | 225 | In the example above, the ``$post`` parameter is handled automatically, but ``$comment`` 226 | is configured with the annotation since they cannot both follow the default convention. 227 | 228 | .. _`@ParamConverter options`: 229 | 230 | DoctrineConverter Options 231 | ......................... 232 | 233 | A number of ``options`` are available on the ``@ParamConverter`` or 234 | (``@Entity``) annotation to control behavior: 235 | 236 | * ``id``: If an ``id`` option is configured and matches a route parameter, then the 237 | converter will find by the primary key: 238 | 239 | .. configuration-block:: 240 | 241 | .. code-block:: php-annotations 242 | 243 | /** 244 | * @Route("/blog/{post_id}") 245 | * @ParamConverter("post", options={"id" = "post_id"}) 246 | */ 247 | public function showPost(Post $post) 248 | { 249 | } 250 | 251 | .. code-block:: php-attributes 252 | 253 | #[Route('/blog/{post_id}')] 254 | #[Entity('post', options: ['id' => 'post_id'])] 255 | public function showPost(Post $post) 256 | { 257 | } 258 | 259 | * ``mapping``: Configures the properties and values to use with the ``findOneBy()`` 260 | method: the key is the route placeholder name and the value is the Doctrine property 261 | name: 262 | 263 | .. configuration-block:: 264 | 265 | .. code-block:: php-annotations 266 | 267 | /** 268 | * @Route("/blog/{date}/{slug}/comments/{comment_slug}") 269 | * @ParamConverter("post", options={"mapping": {"date": "date", "slug": "slug"}}) 270 | * @ParamConverter("comment", options={"mapping": {"comment_slug": "slug"}}) 271 | */ 272 | public function showComment(Post $post, Comment $comment) 273 | { 274 | } 275 | 276 | .. code-block:: php-attributes 277 | 278 | #[Route('/blog/{date}/{slug}/comments/{comment_slug}')] 279 | #[ParamConverter('post', options: ['mapping' => ['date' => 'date', 'slug' => 'slug']])] 280 | #[ParamConverter('comment', options: ['mapping' => ['comment_slug' => 'slug']])] 281 | public function showComment(Post $post, Comment $comment) 282 | { 283 | } 284 | 285 | * ``exclude`` Configures the properties that should be used in the ``findOneBy()`` 286 | method by *excluding* one or more properties so that not *all* are used: 287 | 288 | .. configuration-block:: 289 | 290 | .. code-block:: php-annotations 291 | 292 | /** 293 | * @Route("/blog/{date}/{slug}") 294 | * @ParamConverter("post", options={"exclude": {"date"}}) 295 | */ 296 | public function show(Post $post, \DateTime $date) 297 | { 298 | } 299 | 300 | .. code-block:: php-attributes 301 | 302 | #[Route('/blog/{date}/{slug}')] 303 | #[ParamConverter('post', options: ['exclude' => ['date']])] 304 | public function show(Post $post, \DateTime $date) 305 | { 306 | } 307 | 308 | * ``strip_null`` If true, then when ``findOneBy()`` is used, any values that are 309 | ``null`` will not be used for the query. 310 | 311 | * ``entity_manager`` By default, the Doctrine converter uses the *default* entity 312 | manager, but you can configure this: 313 | 314 | .. configuration-block:: 315 | 316 | .. code-block:: php-annotations 317 | 318 | /** 319 | * @Route("/blog/{id}") 320 | * @ParamConverter("post", options={"entity_manager" = "foo"}) 321 | */ 322 | public function show(Post $post) 323 | { 324 | } 325 | 326 | .. code-block:: php-attributes 327 | 328 | #[Route('/blog/{id}')] 329 | #[ParamConverter('post', options: ['entity_manager' => 'foo'])] 330 | public function show(Post $post) 331 | { 332 | } 333 | 334 | * ``evict_cache`` If true, forces Doctrine to always fetch the entity from the database instead of cache. 335 | 336 | DateTime Converter 337 | ~~~~~~~~~~~~~~~~~~ 338 | 339 | Converter Name: ``datetime`` 340 | 341 | The datetime converter converts any route or request attribute into a datetime 342 | instance: 343 | 344 | .. configuration-block:: 345 | 346 | .. code-block:: php-annotations 347 | 348 | /** 349 | * @Route("/blog/archive/{start}/{end}") 350 | */ 351 | public function archive(\DateTime $start, \DateTime $end) 352 | { 353 | } 354 | 355 | .. code-block:: php-attributes 356 | 357 | #[Route('/blog/archive/{start}/{end}')] 358 | public function archive(\DateTime $start, \DateTime $end) 359 | { 360 | } 361 | 362 | By default, any date format that can be parsed by the ``DateTime`` constructor 363 | or a unix timestamp is accepted. You can be stricter with input given through the options: 364 | 365 | .. configuration-block:: 366 | 367 | .. code-block:: php-annotations 368 | 369 | /** 370 | * @Route("/blog/archive/{start}/{end}") 371 | * @ParamConverter("start", options={"format": "!Y-m-d"}) 372 | * @ParamConverter("end", options={"format": "!Y-m-d"}) 373 | */ 374 | public function archive(\DateTime $start, \DateTime $end) 375 | { 376 | } 377 | 378 | .. code-block:: php-attributes 379 | 380 | #[Route('/blog/archive/{start}/{end}')] 381 | #[ParamConverter('start', options: ['format' => '!Y-m-d'])] 382 | #[ParamConverter('end', options: ['format' => '!Y-m-d'])] 383 | public function archive(\DateTime $start, \DateTime $end) 384 | { 385 | } 386 | 387 | A date in a wrong format like ``2017-21-22`` will return a 404. 388 | 389 | Creating a Converter 390 | -------------------- 391 | 392 | All converters must implement the ``ParamConverterInterface``:: 393 | 394 | namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; 395 | 396 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 397 | use Symfony\Component\HttpFoundation\Request; 398 | 399 | interface ParamConverterInterface 400 | { 401 | function apply(Request $request, ParamConverter $configuration); 402 | 403 | function supports(ParamConverter $configuration); 404 | } 405 | 406 | The ``supports()`` method must return ``true`` when it is able to convert the 407 | given configuration (a ``ParamConverter`` instance). 408 | 409 | The ``ParamConverter`` instance has three pieces of information about the annotation: 410 | 411 | * ``name``: The attribute name; 412 | * ``class``: The attribute class name (can be any string representing a class 413 | name); 414 | * ``options``: An array of options. 415 | 416 | The ``apply()`` method is called whenever a configuration is supported. Based 417 | on the request attributes, it should set an attribute named 418 | ``$configuration->getName()``, which stores an object of class 419 | ``$configuration->getClass()``. 420 | 421 | If you're using service `auto-registration and autoconfiguration`_, 422 | you're done! Your converter will automatically be used. 423 | 424 | You can register a converter by priority, by name (attribute "converter"), or 425 | both. If you don't specify a priority or a name, the converter will be added to 426 | the converter stack with a priority of ``0``. To explicitly disable the 427 | registration by priority you have to set ``priority="false"`` in your tag 428 | definition. 429 | 430 | .. tip:: 431 | 432 | If you would like to inject services or additional arguments into a custom 433 | param converter, the priority shouldn't be higher than ``1``. Otherwise, the 434 | service wouldn't be loaded. 435 | 436 | .. tip:: 437 | 438 | Use the ``DoctrineParamConverter`` class as a template for your own converters. 439 | 440 | .. _auto-registration and autoconfiguration: http://symfony.com/doc/current/service_container/3.3-di-changes.html 441 | -------------------------------------------------------------------------------- /src/Resources/doc/annotations/routing.rst: -------------------------------------------------------------------------------- 1 | @Route and @Method 2 | ================== 3 | 4 | **Routing annotations of the SensioFrameworkExtraBundle are deprecated** since 5 | version 5.2 because they are now a core feature of Symfony. 6 | 7 | How to Update your Applications 8 | ------------------------------- 9 | 10 | ``@Route`` Annotation 11 | ~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | The Symfony ``@Route`` annotation is similar to the SensioFrameworkExtraBundle 14 | annotation, so you only have to update the annotation class namespace: 15 | 16 | .. code-block:: diff 17 | 18 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 19 | +use Symfony\Component\Routing\Annotation\Route; 20 | 21 | class DefaultController extends Controller 22 | { 23 | /** 24 | * @Route("/") 25 | */ 26 | public function index() 27 | { 28 | // ... 29 | } 30 | } 31 | 32 | Alternatively, it can be done with the help of the PHP 8 attribute: 33 | 34 | .. code-block:: diff 35 | 36 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 37 | +use Symfony\Component\Routing\Annotation\Route; 38 | 39 | class DefaultController extends Controller 40 | { 41 | - /** 42 | - * @Route("/") 43 | - */ 44 | + #[Route('/')] 45 | public function index() 46 | { 47 | // ... 48 | } 49 | } 50 | 51 | The main difference is that Symfony's annotation no longer defines the 52 | ``service`` option, which was used to instantiate the controller by fetching the 53 | given service from the container. In modern Symfony applications, all 54 | `controllers are services by default`_ and their service IDs are their fully- 55 | qualified class names, so this option is no longer needed. 56 | 57 | ``@Method`` Annotation 58 | ~~~~~~~~~~~~~~~~~~~~~~ 59 | 60 | The ``@Method`` annotation from SensioFrameworkExtraBundle has been removed. 61 | Instead, the Symfony ``@Route`` annotation defines a ``methods`` option to 62 | restrict the HTTP methods of the route: 63 | 64 | .. code-block:: diff 65 | 66 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 67 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 68 | +use Symfony\Component\Routing\Annotation\Route; 69 | 70 | class DefaultController extends Controller 71 | { 72 | /** 73 | - * @Route("/show/{id}") 74 | - * @Method({"GET", "HEAD"}) 75 | + * @Route("/show/{id}", methods={"GET","HEAD"}) 76 | */ 77 | public function show($id) 78 | { 79 | // ... 80 | } 81 | } 82 | 83 | Alternatively, it can be done with the help of the PHP 8 attribute: 84 | 85 | .. code-block:: diff 86 | 87 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 88 | -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 89 | +use Symfony\Component\Routing\Annotation\Route; 90 | 91 | class DefaultController extends Controller 92 | { 93 | - /** 94 | - * @Route("/show/{id}") 95 | - * @Method({"GET", "HEAD"}) 96 | - */ 97 | + #[Route('/show/{id}', methods: ['GET','HEAD'])] 98 | public function show($id) 99 | { 100 | // ... 101 | } 102 | } 103 | 104 | 105 | Read the `chapter about Routing`_ in the Symfony Documentation to learn 106 | everything about these and the other annotations available. 107 | 108 | .. _`controllers are services by default`: https://symfony.com/doc/current/controller/service.html 109 | .. _`chapter about Routing`: https://symfony.com/doc/current/routing.html 110 | -------------------------------------------------------------------------------- /src/Resources/doc/annotations/security.rst: -------------------------------------------------------------------------------- 1 | @Security & @IsGranted 2 | ====================== 3 | 4 | Usage 5 | ----- 6 | 7 | The ``@Security`` and ``@IsGranted`` annotations restrict access on controllers: 8 | 9 | .. configuration-block:: 10 | 11 | .. code-block:: php-annotations 12 | 13 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 15 | 16 | class PostController extends Controller 17 | { 18 | /** 19 | * @IsGranted("ROLE_ADMIN") 20 | * 21 | * or use @Security for more flexibility: 22 | * 23 | * @Security("is_granted('ROLE_ADMIN') and is_granted('ROLE_FRIENDLY_USER')") 24 | */ 25 | public function index() 26 | { 27 | // ... 28 | } 29 | } 30 | 31 | .. code-block:: php-attributes 32 | 33 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 34 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 35 | 36 | class PostController extends Controller 37 | { 38 | #[IsGranted('ROLE_ADMIN')] 39 | /** or use Security attribute */ 40 | #[Security("is_granted('ROLE_ADMIN') and is_granted('ROLE_FRIENDLY_USER')")] 41 | public function index() 42 | { 43 | // ... 44 | } 45 | } 46 | 47 | 48 | @IsGranted 49 | ---------- 50 | 51 | The ``@IsGranted()`` annotation is the simplest way to restrict access. 52 | Use it to restrict by roles, or use custom voters to restrict access based 53 | on variables passed to the controller: 54 | 55 | .. configuration-block:: 56 | 57 | .. code-block:: php-annotations 58 | 59 | /** 60 | * @Route("/posts/{id}") 61 | * 62 | * @IsGranted("ROLE_ADMIN") 63 | * @IsGranted("POST_SHOW", subject="post") 64 | */ 65 | public function show(Post $post) 66 | { 67 | } 68 | 69 | .. code-block:: php-attributes 70 | 71 | #[Route('/posts/{id}')] 72 | #[IsGranted('ROLE_ADMIN')] 73 | #[IsGranted('POST_SHOW', subject: 'post')] 74 | public function show(Post $post) 75 | { 76 | } 77 | 78 | Each ``IsGranted()`` must grant access for the user to have access to the controller. 79 | 80 | .. tip:: 81 | 82 | The ``@IsGranted("POST_SHOW", subject="post")`` is an example of using 83 | a custom security voter. For more details, see `the Security Voters page`_. 84 | 85 | You can also control the message and status code: 86 | 87 | .. configuration-block:: 88 | 89 | .. code-block:: php-annotations 90 | 91 | /** 92 | * Will throw a normal AccessDeniedException: 93 | * 94 | * @IsGranted("ROLE_ADMIN", message="No access! Get out!") 95 | * 96 | * Will throw an HttpException with a 404 status code: 97 | * 98 | * @IsGranted("ROLE_ADMIN", statusCode=404, message="Post not found") 99 | */ 100 | public function show(Post $post) 101 | { 102 | } 103 | 104 | .. code-block:: php-attributes 105 | 106 | /** Will throw a normal AccessDeniedException */ 107 | #[IsGranted('ROLE_ADMIN', message: 'No access! Get out!')] 108 | /** Will throw an HttpException with a 404 status code */ 109 | #[IsGranted('ROLE_ADMIN', statusCode: 404, message: 'Post not found')] 110 | public function show(Post $post) 111 | { 112 | } 113 | 114 | @Security 115 | --------- 116 | 117 | The ``@Security`` annotation is more flexible than ``@IsGranted``: it 118 | allows you to pass an *expression* that can contain custom logic: 119 | 120 | .. configuration-block:: 121 | 122 | .. code-block:: php-annotations 123 | 124 | /** 125 | * @Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)") 126 | */ 127 | public function show(Post $post) 128 | { 129 | // ... 130 | } 131 | 132 | .. code-block:: php-attributes 133 | 134 | #[Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)")] 135 | public function show(Post $post) 136 | { 137 | // ... 138 | } 139 | 140 | The expression can use all functions that you can use in the ``access_control`` 141 | section of the security bundle configuration, with the addition of the 142 | ``is_granted()`` function. 143 | 144 | The expression has access to the following variables: 145 | 146 | * ``token``: The current security token; 147 | * ``user``: The current user object; 148 | * ``request``: The request instance; 149 | * ``roles``: The user roles; 150 | * and all request attributes. 151 | 152 | You can throw an ``Symfony\Component\HttpKernel\Exception\HttpException`` 153 | exception instead of 154 | ``Symfony\Component\Security\Core\Exception\AccessDeniedException`` using the 155 | ``statusCode`` option: 156 | 157 | .. configuration-block:: 158 | 159 | .. code-block:: php-annotations 160 | 161 | /** 162 | * @Security("is_granted('POST_SHOW', post)", statusCode=404) 163 | */ 164 | public function show(Post $post) 165 | { 166 | } 167 | 168 | .. code-block:: php-attributes 169 | 170 | #[Security("is_granted('POST_SHOW', post)", statusCode: 404)] 171 | public function show(Post $post) 172 | { 173 | } 174 | 175 | The ``message`` option allows you to customize the exception message: 176 | 177 | .. configuration-block:: 178 | 179 | .. code-block:: php-annotations 180 | 181 | /** 182 | * @Security("is_granted('POST_SHOW', post)", statusCode=404, message="Resource not found.") 183 | */ 184 | public function show(Post $post) 185 | { 186 | } 187 | 188 | .. code-block:: php-attributes 189 | 190 | #[Security("is_granted('POST_SHOW', post)", statusCode: 404, message: 'Resource not found.')] 191 | public function show(Post $post) 192 | { 193 | } 194 | 195 | .. tip:: 196 | 197 | You can also add ``@IsGranted`` or ``@Security`` annotations on a 198 | controller class to prevent access to *all* actions in the class. 199 | 200 | .. _`the Security Voters page`: http://symfony.com/doc/current/cookbook/security/voters_data_permission.html 201 | -------------------------------------------------------------------------------- /src/Resources/doc/annotations/view.rst: -------------------------------------------------------------------------------- 1 | @Template 2 | ========= 3 | 4 | Usage 5 | ----- 6 | 7 | The ``@Template`` annotation associates a controller with a template name: 8 | 9 | .. configuration-block:: 10 | 11 | .. code-block:: php-annotations 12 | 13 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 14 | 15 | /** 16 | * @Template("@SensioBlog/post/show.html.twig") 17 | */ 18 | public function show($id) 19 | { 20 | // get the Post 21 | $post = ...; 22 | 23 | return array('post' => $post); 24 | } 25 | 26 | .. code-block:: php-attributes 27 | 28 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 29 | 30 | #[Template('@SensioBlog/post/show.html.twig')] 31 | public function show($id) 32 | { 33 | // get the Post 34 | $post = ...; 35 | 36 | return array('post' => $post); 37 | } 38 | 39 | When using the ``@Template`` annotation, the controller should return an 40 | array of parameters to pass to the view instead of a ``Response`` object. 41 | 42 | .. note:: 43 | 44 | If you want to stream your template, you can make it with the following configuration: 45 | 46 | .. configuration-block:: 47 | 48 | .. code-block:: php-annotations 49 | 50 | /** 51 | * @Template(isStreamable=true) 52 | */ 53 | public function show($id) 54 | { 55 | // ... 56 | } 57 | 58 | .. code-block:: php-attributes 59 | 60 | #[Template(isStreamable: true)] 61 | public function show($id) 62 | { 63 | // ... 64 | } 65 | 66 | .. tip:: 67 | 68 | If the action returns a ``Response`` object, the ``@Template`` annotation is 69 | simply ignored. 70 | 71 | If the template is named after the controller and action names, which is the 72 | case for the above example, you can even omit the annotation value: 73 | 74 | .. configuration-block:: 75 | 76 | .. code-block:: php-annotations 77 | 78 | /** 79 | * @Template 80 | */ 81 | public function show($id) 82 | { 83 | // get the Post 84 | $post = ...; 85 | 86 | return array('post' => $post); 87 | } 88 | 89 | .. code-block:: php-attributes 90 | 91 | #[Template] 92 | public function show($id) 93 | { 94 | // get the Post 95 | $post = ...; 96 | 97 | return array('post' => $post); 98 | } 99 | 100 | .. tip:: 101 | 102 | Sub-namespaces are converted into underscores. The 103 | ``Sensio\BlogBundle\Controller\UserProfileController::showDetails()`` action 104 | will resolve to ``@SensioBlog/user_profile/show_details.html.twig`` 105 | 106 | And if the only parameters to pass to the template are method arguments, you 107 | can use the ``vars`` attribute instead of returning an array. This is very 108 | useful in combination with the ``@ParamConverter`` :doc:`annotation 109 | `: 110 | 111 | .. configuration-block:: 112 | 113 | .. code-block:: php-annotations 114 | 115 | /** 116 | * @ParamConverter("post", class="SensioBlogBundle:Post") 117 | * @Template("@SensioBlog/post/show.html.twig", vars={"post"}) 118 | */ 119 | public function show(Post $post) 120 | { 121 | } 122 | 123 | .. code-block:: php-attributes 124 | 125 | #[ParamConverter('post', class: 'SensioBlogBundle:Post')] 126 | #[Template('@SensioBlog/post/show.html.twig"', vars: ['post'])] 127 | public function show(Post $post) 128 | { 129 | } 130 | 131 | which, thanks to conventions, is equivalent to the following configuration: 132 | 133 | .. configuration-block:: 134 | 135 | .. code-block:: php-annotations 136 | 137 | /** 138 | * @Template(vars={"post"}) 139 | */ 140 | public function show(Post $post) 141 | { 142 | } 143 | 144 | .. code-block:: php-attributes 145 | 146 | #[Template(vars: ['post'])] 147 | public function show(Post $post) 148 | { 149 | } 150 | 151 | You can make it even more concise as all method arguments are automatically 152 | passed to the template if the method returns ``null`` and no ``vars`` attribute 153 | is defined: 154 | 155 | .. configuration-block:: 156 | 157 | .. code-block:: php-annotations 158 | 159 | /** 160 | * @Template 161 | */ 162 | public function show(Post $post) 163 | { 164 | } 165 | 166 | .. code-block:: php-attributes 167 | 168 | #[Template] 169 | public function show(Post $post) 170 | { 171 | } 172 | -------------------------------------------------------------------------------- /src/Resources/doc/index.rst: -------------------------------------------------------------------------------- 1 | SensioFrameworkExtraBundle 2 | ========================== 3 | 4 | The default Symfony ``FrameworkBundle`` implements a basic but robust and 5 | flexible MVC framework. `SensioFrameworkExtraBundle`_ extends it to add sweet 6 | conventions and annotations. It allows for more concise controllers. 7 | 8 | Installation 9 | ------------ 10 | 11 | An official Symfony recipe is available for this bundle. To automatically 12 | install and configure it run: 13 | 14 | .. code-block:: bash 15 | 16 | $ composer require sensio/framework-extra-bundle 17 | 18 | You're done! 19 | 20 | Configuration 21 | ------------- 22 | 23 | All features provided by the bundle are enabled by default when the bundle is 24 | registered in your Kernel class. 25 | 26 | The default configuration is as follow: 27 | 28 | .. configuration-block:: 29 | 30 | .. code-block:: yaml 31 | 32 | sensio_framework_extra: 33 | router: { annotations: true } # Deprecated; use routing annotations of Symfony core instead 34 | request: { converters: true, auto_convert: true } 35 | view: { annotations: true } 36 | cache: { annotations: true } 37 | security: { annotations: true } 38 | 39 | 40 | .. code-block:: xml 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | .. code-block:: php 52 | 53 | // load the profiler 54 | $container->loadFromExtension('sensio_framework_extra', array( 55 | 'router' => array('annotations' => true), 56 | 'request' => array('converters' => true, 'auto_convert' => true), 57 | 'view' => array('annotations' => true), 58 | 'cache' => array('annotations' => true), 59 | 'security' => array('annotations' => true), 60 | )); 61 | 62 | You can disable some annotations and conventions by defining one or more 63 | settings to ``false``. 64 | 65 | Annotations for Controllers 66 | --------------------------- 67 | 68 | Annotations are a great way to easily configure your controllers, from the 69 | routes to the cache configuration. 70 | 71 | Even if annotations are not a native feature of PHP, it still has several 72 | advantages over the classic Symfony configuration methods: 73 | 74 | * Code and configuration are in the same place (the controller class); 75 | * Simple to learn and to use; 76 | * Concise to write; 77 | * Makes your Controller thin (as its sole responsibility is to get data from 78 | the Model). 79 | 80 | .. tip:: 81 | 82 | If you use view classes, annotations are a great way to avoid creating 83 | view classes for simple and common use cases. 84 | 85 | The following annotations are defined by the bundle: 86 | 87 | .. toctree:: 88 | :maxdepth: 1 89 | 90 | annotations/routing 91 | annotations/converters 92 | annotations/view 93 | annotations/cache 94 | annotations/security 95 | 96 | This example shows all the available annotations in action (here and in all 97 | the other examples both plain old annotations and PHP 8 attributes are shown): 98 | 99 | .. configuration-block:: 100 | 101 | .. code-block:: php-annotations 102 | 103 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 104 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 105 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 106 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 107 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 108 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 109 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 110 | 111 | /** 112 | * @Route("/blog") 113 | * @Cache(expires="tomorrow") 114 | */ 115 | class AnnotController 116 | { 117 | /** 118 | * @Route("/") 119 | * @Template 120 | */ 121 | public function index() 122 | { 123 | $posts = ...; 124 | 125 | return array('posts' => $posts); 126 | } 127 | 128 | /** 129 | * @Route("/{id}") 130 | * @Method("GET") 131 | * @ParamConverter("post", class="SensioBlogBundle:Post") 132 | * @Template("@SensioBlog/annot/show.html.twig", vars={"post"}) 133 | * @Cache(smaxage="15", lastmodified="post.getUpdatedAt()", etag="'Post' ~ post.getId() ~ post.getUpdatedAt()") 134 | * @IsGranted("ROLE_SPECIAL_USER") 135 | * @Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)") 136 | */ 137 | public function show(Post $post) 138 | { 139 | } 140 | } 141 | 142 | .. code-block:: php-attributes 143 | 144 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 145 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 146 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 147 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 148 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 149 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; 150 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; 151 | 152 | #[Route('/blog')] 153 | #[Cache(expired: 'tomorrow')] 154 | class AnnotController 155 | { 156 | #[Route('/')] 157 | #[Template] 158 | public function index() 159 | { 160 | $posts = ...; 161 | 162 | return array('posts' => $posts); 163 | } 164 | 165 | #[Route('/{id}')] 166 | #[Method('GET')] 167 | #[ParamConverter('post', class: 'SensioBlogBundle:Post')] 168 | #[Template('@SensioBlog/annot/show.html.twig", vars: ['post'])] 169 | #[Cache(smaxage: 15, lastmodified: 'post.getUpdatedAt()', etag: "'Post' ~ post.getId() ~ post.getUpdatedAt()")] 170 | #[IsGranted('ROLE_SPECIAL_USER')] 171 | #[Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)")] 172 | public function show(Post $post) 173 | { 174 | } 175 | } 176 | 177 | As the ``showAction`` method follows some conventions, you can omit some 178 | annotations: 179 | 180 | .. configuration-block:: 181 | 182 | .. code-block:: php-annotations 183 | 184 | /** 185 | * @Route("/{id}") 186 | * @Cache(smaxage="15", lastModified="post.getUpdatedAt()", Etag="'Post' ~ post.getId() ~ post.getUpdatedAt()") 187 | * @IsGranted("ROLE_SPECIAL_USER") 188 | * @Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)") 189 | */ 190 | public function show(Post $post) 191 | { 192 | } 193 | 194 | .. code-block:: php-attributes 195 | 196 | #[Route('/{id}')] 197 | #[Cache(smaxage: 15, lastmodified: 'post.getUpdatedAt()', etag: "'Post' ~ post.getId() ~ post.getUpdatedAt()")] 198 | #[IsGranted('ROLE_SPECIAL_USER')] 199 | #[Security("is_granted('ROLE_ADMIN') and is_granted('POST_SHOW', post)")] 200 | public function show(Post $post) 201 | { 202 | } 203 | 204 | The routes need to be imported to be active as any other routing resources, for 205 | example: 206 | 207 | .. code-block:: yaml 208 | 209 | # config/routes/annotations.yaml 210 | 211 | # import routes from a controller directory 212 | annot: 213 | resource: "@AnnotRoutingBundle/Controller" 214 | type: annotation 215 | 216 | .. _`SensioFrameworkExtraBundle`: https://github.com/sensiolabs/SensioFrameworkExtraBundle 217 | -------------------------------------------------------------------------------- /src/Routing/AnnotatedRouteControllerLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Routing; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; 15 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route as FrameworkExtraBundleRoute; 16 | use Symfony\Component\Routing\Loader\AnnotationClassLoader; 17 | use Symfony\Component\Routing\Route; 18 | 19 | @trigger_error(sprintf('The "%s" class is deprecated since version 5.2. Use "%s" instead.', AnnotatedRouteControllerLoader::class, \Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader::class), \E_USER_DEPRECATED); 20 | 21 | /** 22 | * AnnotatedRouteControllerLoader is an implementation of AnnotationClassLoader 23 | * that sets the '_controller' default based on the class and method names. 24 | * 25 | * It also parse the @Method annotation. 26 | * 27 | * @author Fabien Potencier 28 | * 29 | * @deprecated since version 5.2 30 | */ 31 | class AnnotatedRouteControllerLoader extends AnnotationClassLoader 32 | { 33 | /** 34 | * Configures the _controller default parameter and eventually the HTTP method 35 | * requirement of a given Route instance. 36 | * 37 | * @param mixed $annot The annotation class instance 38 | * 39 | * @throws \LogicException When the service option is specified on a method 40 | */ 41 | protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) 42 | { 43 | // controller 44 | $classAnnot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass); 45 | if ($classAnnot instanceof FrameworkExtraBundleRoute && $service = $classAnnot->getService()) { 46 | $route->setDefault('_controller', $service.':'.$method->getName()); 47 | } elseif ('__invoke' === $method->getName()) { 48 | $route->setDefault('_controller', $class->getName()); 49 | } else { 50 | $route->setDefault('_controller', $class->getName().'::'.$method->getName()); 51 | } 52 | 53 | // requirements (@Method) 54 | foreach ($this->reader->getMethodAnnotations($method) as $configuration) { 55 | if ($configuration instanceof Method) { 56 | $route->setMethods($configuration->getMethods()); 57 | } elseif ($configuration instanceof FrameworkExtraBundleRoute && $configuration->getService()) { 58 | throw new \LogicException('The service option can only be specified at class level.'); 59 | } 60 | } 61 | } 62 | 63 | protected function getGlobals(\ReflectionClass $class) 64 | { 65 | $globals = parent::getGlobals($class); 66 | 67 | foreach ($this->reader->getClassAnnotations($class) as $configuration) { 68 | if ($configuration instanceof Method) { 69 | $globals['methods'] = array_merge($globals['methods'], $configuration->getMethods()); 70 | } 71 | } 72 | 73 | return $globals; 74 | } 75 | 76 | /** 77 | * Makes the default route name more sane by removing common keywords. 78 | * 79 | * @return string The default route name 80 | */ 81 | protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) 82 | { 83 | $routeName = parent::getDefaultRouteName($class, $method); 84 | 85 | return preg_replace( 86 | ['/(bundle|controller)_/', '/action(_\d+)?$/', '/__/'], 87 | ['_', '\\1', '_'], 88 | $routeName 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Security/ExpressionLanguage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Security; 13 | 14 | use Symfony\Component\Security\Core\Authorization\ExpressionLanguage as BaseExpressionLanguage; 15 | 16 | /** 17 | * Adds some function to the default Symfony Security ExpressionLanguage. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class ExpressionLanguage extends BaseExpressionLanguage 22 | { 23 | protected function registerFunctions() 24 | { 25 | parent::registerFunctions(); 26 | 27 | $this->register('is_granted', function ($attributes, $object = 'null') { 28 | return sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object); 29 | }, function (array $variables, $attributes, $object = null) { 30 | return $variables['auth_checker']->isGranted($attributes, $object); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SensioFrameworkExtraBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle; 13 | 14 | use Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; 15 | use Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddParamConverterPass; 16 | use Sensio\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\OptimizerPass; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\HttpKernel\Bundle\Bundle; 19 | 20 | /** 21 | * @author Fabien Potencier 22 | */ 23 | class SensioFrameworkExtraBundle extends Bundle 24 | { 25 | public function build(ContainerBuilder $container) 26 | { 27 | parent::build($container); 28 | 29 | $container->addCompilerPass(new AddParamConverterPass()); 30 | $container->addCompilerPass(new OptimizerPass()); 31 | $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Templating/TemplateGuesser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Sensio\Bundle\FrameworkExtraBundle\Templating; 13 | 14 | use Doctrine\Persistence\Proxy; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpKernel\KernelInterface; 17 | 18 | /** 19 | * The TemplateGuesser class handles the guessing of template name based on controller. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | class TemplateGuesser 24 | { 25 | /** 26 | * @var KernelInterface 27 | */ 28 | private $kernel; 29 | 30 | /** 31 | * @var string[] 32 | */ 33 | private $controllerPatterns; 34 | 35 | /** 36 | * @param string[] $controllerPatterns Regexps extracting the controller name from its FQN 37 | */ 38 | public function __construct(KernelInterface $kernel, array $controllerPatterns = []) 39 | { 40 | $controllerPatterns[] = '/Controller\\\(.+)Controller$/'; 41 | 42 | $this->kernel = $kernel; 43 | $this->controllerPatterns = $controllerPatterns; 44 | } 45 | 46 | /** 47 | * Guesses and returns the template name to render based on the controller 48 | * and action names. 49 | * 50 | * @param callable $controller An array storing the controller object and action method 51 | * 52 | * @return string The template name 53 | * 54 | * @throws \InvalidArgumentException 55 | */ 56 | public function guessTemplateName($controller, Request $request) 57 | { 58 | if (\is_object($controller) && method_exists($controller, '__invoke')) { 59 | $controller = [$controller, '__invoke']; 60 | } elseif (!\is_array($controller)) { 61 | throw new \InvalidArgumentException(sprintf('First argument of "%s" must be an array callable or an object defining the magic method __invoke. "%s" given.', __METHOD__, \gettype($controller))); 62 | } 63 | 64 | $className = $this->getRealClass(\get_class($controller[0])); 65 | 66 | $matchController = null; 67 | foreach ($this->controllerPatterns as $pattern) { 68 | if (preg_match($pattern, $className, $tempMatch)) { 69 | $matchController = str_replace('\\', '/', strtolower(preg_replace('/([a-z\d])([A-Z])/', '\\1_\\2', $tempMatch[1]))); 70 | break; 71 | } 72 | } 73 | if (null === $matchController) { 74 | throw new \InvalidArgumentException(sprintf('The "%s" class does not look like a controller class (its FQN must match one of the following regexps: "%s").', \get_class($controller[0]), implode('", "', $this->controllerPatterns))); 75 | } 76 | 77 | if ('__invoke' === $controller[1]) { 78 | $matchAction = $matchController; 79 | $matchController = null; 80 | } else { 81 | $matchAction = preg_replace('/Action$/', '', $controller[1]); 82 | } 83 | 84 | $matchAction = strtolower(preg_replace('/([a-z\d])([A-Z])/', '\\1_\\2', $matchAction)); 85 | $bundleName = $this->getBundleForClass($className); 86 | 87 | return ($bundleName ? '@'.$bundleName.'/' : '').$matchController.($matchController ? '/' : '').$matchAction.'.'.$request->getRequestFormat().'.twig'; 88 | } 89 | 90 | /** 91 | * Returns the bundle name in which the given class name is located. 92 | * 93 | * @param string $class A fully qualified controller class name 94 | * 95 | * @return string|null $bundle A bundle name 96 | */ 97 | private function getBundleForClass($class) 98 | { 99 | $reflectionClass = new \ReflectionClass($class); 100 | $bundles = $this->kernel->getBundles(); 101 | 102 | do { 103 | $namespace = $reflectionClass->getNamespaceName(); 104 | foreach ($bundles as $bundle) { 105 | if ('Symfony\Bundle\FrameworkBundle' === $bundle->getNamespace()) { 106 | continue; 107 | } 108 | if (0 === strpos($namespace, $bundle->getNamespace())) { 109 | return preg_replace('/Bundle$/', '', $bundle->getName()); 110 | } 111 | } 112 | $reflectionClass = $reflectionClass->getParentClass(); 113 | } while ($reflectionClass); 114 | } 115 | 116 | private static function getRealClass(string $class): string 117 | { 118 | if (!class_exists(Proxy::class)) { 119 | return $class; 120 | } 121 | if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) { 122 | return $class; 123 | } 124 | 125 | return substr($class, $pos + Proxy::MARKER_LENGTH + 2); 126 | } 127 | } 128 | --------------------------------------------------------------------------------