diff --git a/_build/conf.py b/_build/conf.py index 49cc12581ad..071991c5411 100644 --- a/_build/conf.py +++ b/_build/conf.py @@ -111,6 +111,7 @@ lexers['markdown'] = TextLexer() lexers['php'] = PhpLexer(startinline=True) lexers['php-annotations'] = PhpLexer(startinline=True) +lexers['php-attributes'] = PhpLexer(startinline=True) lexers['php-standalone'] = PhpLexer(startinline=True) lexers['php-symfony'] = PhpLexer(startinline=True) lexers['rst'] = RstLexer() diff --git a/best_practices.rst b/best_practices.rst index 02434a7c812..f43d4798452 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -223,12 +223,13 @@ important parts of your application. .. _best-practice-controller-annotations: -Use Annotations to Configure Routing, Caching and Security -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Use Attributes or Annotations to Configure Routing, Caching and Security +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Using annotations for routing, caching and security simplifies configuration. -You don't need to browse several files created with different formats (YAML, XML, -PHP): all the configuration is just where you need it and it only uses one format. +Using attributes or annotations for routing, caching and security simplifies +configuration. You don't need to browse several files created with different +formats (YAML, XML, PHP): all the configuration is just where you need it and +it only uses one format. Don't Use Annotations to Configure the Controller Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/contributing/documentation/format.rst b/contributing/documentation/format.rst index 733e9e6f21f..2c465096f0b 100644 --- a/contributing/documentation/format.rst +++ b/contributing/documentation/format.rst @@ -104,6 +104,7 @@ Markup Format Use It to Display ``html+php`` PHP code blended with HTML ``ini`` INI ``php-annotations`` PHP Annotations +``php-attributes`` PHP Attributes =================== ====================================== Adding Links diff --git a/routing.rst b/routing.rst index 214b57574ae..832b5df2c53 100644 --- a/routing.rst +++ b/routing.rst @@ -15,22 +15,33 @@ provides other useful features, like generating SEO-friendly URLs (e.g. Creating Routes --------------- -Routes can be configured in YAML, XML, PHP or using annotations. All formats -provide the same features and performance, so choose your favorite. -:ref:`Symfony recommends annotations <best-practice-controller-annotations>` +Routes can be configured in YAML, XML, PHP or using either attributes or +annotations. All formats provide the same features and performance, so choose +your favorite. +:ref:`Symfony recommends attributes <best-practice-controller-annotations>` because it's convenient to put the route and controller in the same place. -Creating Routes as Annotations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Creating Routes as Attributes or Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On PHP 8, you can use native attributes to configure routes right away. On +PHP 7, where attributes are not available, you can use annotations instead, +provided by the Doctrine Annotations library. -Run this command once in your application to add support for annotations: +In case you want to use annotations instead of attributes, run this command +once in your application to enable them: .. code-block:: terminal $ composer require annotations -In addition to installing the needed dependencies, this command creates the -following configuration file: +.. versionadded:: 5.2 + + The ability to use PHP attributes to configure routes was introduced in + Symfony 5.2. Prior to this, Doctrine Annotations were the only way to + annotate controller actions with routing configuration. + +This command also creates the following configuration file: .. code-block:: yaml @@ -49,22 +60,43 @@ any PHP class stored in the ``src/Controller/`` directory. Suppose you want to define a route for the ``/blog`` URL in your application. To do so, create a :doc:`controller class </controller>` like the following:: - // src/Controller/BlogController.php - namespace App\Controller; +.. configuration-block:: - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + .. code-block:: php-annotations - class BlogController extends AbstractController - { - /** - * @Route("/blog", name="blog_list") - */ - public function list() + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController { - // ... + /** + * @Route("/blog", name="blog_list") + */ + public function list() + { + // ... + } + } + + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + #[Route('/blog', name: 'blog_list')] + public function list() + { + // ... + } } - } This configuration defines a route called ``blog_list`` that matches when the user requests the ``/blog`` URL. When the match occurs, the application runs @@ -182,6 +214,28 @@ Use the ``methods`` option to restrict the verbs each route should respond to: } } + .. code-block:: php-attributes + + // src/Controller/BlogApiController.php + namespace App\Controller; + + // ... + + class BlogApiController extends AbstractController + { + #[Route('/api/posts/{id}', methods: ['GET', 'HEAD'])] + public function show(int $id) + { + // ... return a JSON response with the post + } + + #[Route('/api/posts/{id}', methods: ['PUT'])] + public function edit(int $id) + { + // ... edit a post + } + } + .. code-block:: yaml # config/routes.yaml @@ -274,6 +328,29 @@ arbitrary matching logic: } } + .. code-block:: php-attributes + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController extends AbstractController + { + #[Route( + '/contact', + name: 'contact', + condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'", + )] + // expressions can also include config parameters: + // condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'" + public function contact() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -424,6 +501,28 @@ defined as ``/blog/{slug}``: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + // ... + + #[Route('/blog/{slug}', name: 'blog_show')] + public function show(string $slug) + { + // $slug will equal the dynamic part of the URL + // e.g. at /blog/yay-routing, then $slug='yay-routing' + + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -507,6 +606,29 @@ the ``{page}`` parameter using the ``requirements`` option: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + #[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])] + public function list(int $page) + { + // ... + } + + #[Route('/blog/{slug}', name: 'blog_show')] + public function show($slug) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -610,6 +732,23 @@ concise, but it can decrease route readability when requirements are complex: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + #[Route('/blog/{page<\d+>}', name: 'blog_list')] + public function list(int $page) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -678,6 +817,23 @@ other configuration formats they are defined with the ``defaults`` option: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + #[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])] + public function list(int $page = 1) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -764,6 +920,23 @@ parameter: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController + { + #[Route('/blog/{page<\d+>?1}', name: 'blog_list')] + public function list(int $page) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -816,36 +989,67 @@ matched. A ``priority`` optional parameter is available in order to let you choose the order of your routes, and it is only available when using annotations. -.. code-block:: php-annotations +.. configuration-block:: - // src/Controller/BlogController.php - namespace App\Controller; + .. code-block:: php-annotations - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\Routing\Annotation\Route; + // src/Controller/BlogController.php + namespace App\Controller; - class BlogController extends AbstractController - { - /** - * This route has a greedy pattern and is defined first. - * - * @Route("/blog/{slug}", name="blog_show") - */ - public function show(string $slug) + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController { - // ... + /** + * This route has a greedy pattern and is defined first. + * + * @Route("/blog/{slug}", name="blog_show") + */ + public function show(string $slug) + { + // ... + } + + /** + * This route could not be matched without defining a higher priority than 0. + * + * @Route("/blog/list", name="blog_list", priority=2) + */ + public function list() + { + // ... + } } - /** - * This route could not be matched without defining a higher priority than 0. - * - * @Route("/blog/list", name="blog_list", priority=2) - */ - public function list() + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class BlogController extends AbstractController { - // ... + /** + * This route has a greedy pattern and is defined first. + */ + #[Route('/blog/{slug}', name: 'blog_show')] + public function show(string $slug) + { + // ... + } + + /** + * This route could not be matched without defining a higher priority than 0. + */ + #[Route('/blog/list', name: 'blog_list', priority: 2)] + public function list() + { + // ... + } } - } The priority parameter expects an integer value. Routes with higher priority are sorted before routes with lower priority. The default value when it is not @@ -955,6 +1159,28 @@ and in route imports. Symfony defines some special attributes with the same name } } + .. code-block:: php-attributes + + // src/Controller/ArticleController.php + namespace App\Controller; + + // ... + class ArticleController extends AbstractController + { + #[Route( + path: '/articles/{_locale}/search.{_format}', + locale: 'en', + format: 'html', + requirements: [ + '_locale' => 'en|fr', + '_format' => 'html|xml', + ], + )] + public function search() + { + } + } + .. code-block:: yaml # config/routes.yaml @@ -1034,6 +1260,22 @@ the controllers of the routes: } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Component\Routing\Annotation\Route; + + class BlogController + { + #[Route('/blog/{page}', name: 'blog_index', defaults: ['page' => 1, 'title' => 'Hello world!'])] + public function index(int $page, string $title) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -1107,6 +1349,22 @@ A possible solution is to change the parameter requirements to be more permissiv } } + .. code-block:: php-attributes + + // src/Controller/DefaultController.php + namespace App\Controller; + + use Symfony\Component\Routing\Annotation\Route; + + class DefaultController + { + #[Route('/share/{token}', name: 'share', requirements: ['token' => '.+'])] + public function share($token) + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -1171,9 +1429,10 @@ It's common for a group of routes to share some options (e.g. all routes related to the blog start with ``/blog``) That's why Symfony includes a feature to share route configuration. -When defining routes as annotations, put the common configuration in the -``@Route`` annotation of the controller class. In other routing formats, define -the common configuration using options when importing the routes. +When defining routes as attributes or annotations, put the common configuration +in the ``#[Route]`` attribute (or ``@Route`` annotation) of the controller +class. In other routing formats, define the common configuration using options +when importing the routes. .. configuration-block:: @@ -1206,6 +1465,29 @@ the common configuration using options when importing the routes. } } + .. code-block:: php-attributes + + // src/Controller/BlogController.php + namespace App\Controller; + + use Symfony\Component\Routing\Annotation\Route; + + #[Route('/blog', requirements: ['_locale' => 'en|es|fr'], name: 'blog_')] + class BlogController + { + #[Route('/{_locale}', name: 'index')] + public function index() + { + // ... + } + + #[Route('/{_locale}/posts/{slug}', name: 'show')] + public function show(Post $post) + { + // ... + } + } + .. code-block:: yaml # config/routes/annotations.yaml @@ -1515,6 +1797,29 @@ host name: } } + .. code-block:: php-attributes + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class MainController extends AbstractController + { + #[Route('/', name: 'mobile_homepage', host: 'm.example.com')] + public function mobileHomepage() + { + // ... + } + + #[Route('/', name: 'homepage')] + public function homepage() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -1600,6 +1905,35 @@ multi-tenant applications) and these parameters can be validated too with } } + .. code-block:: php-attributes + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class MainController extends AbstractController + { + #[Route( + '/', + name: 'mobile_homepage', + host: '{subdomain}.example.com', + defaults: ['subdomain' => 'm'], + requirements: ['subdomain' => 'm|mobile'], + )] + public function mobileHomepage() + { + // ... + } + + #[Route('/', name: 'homepage')] + public function homepage() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -1715,6 +2049,26 @@ avoids the need for duplicating routes, which also reduces the potential bugs: } } + .. code-block:: php-attributes + + // src/Controller/CompanyController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class CompanyController extends AbstractController + { + #[Route(path: [ + 'en' => '/about-us', + 'nl' => '/over-ons' + ], name: 'about_us')] + public function about() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -1754,6 +2108,11 @@ avoids the need for duplicating routes, which also reduces the potential bugs: ; }; +.. note:: + + When using PHP attributes for localized routes, you have to use the `path` + named parameter to specify the array of paths. + When a localized route is matched, Symfony uses the same locale automatically during the entire request. @@ -1849,6 +2208,23 @@ session shouldn't be used when matching a request: } } + .. code-block:: php-attributes + + // src/Controller/MainController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class MainController extends AbstractController + { + #[Route('/', name: 'homepage', stateless: true)] + public function homepage() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml @@ -2224,6 +2600,23 @@ each route explicitly: } } + .. code-block:: php-attributes + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + #[Route('/login', name: 'login', schemes: ['https'])] + public function login() + { + // ... + } + } + .. code-block:: yaml # config/routes.yaml