路由
当您的应用程序收到请求时,它会调用控制器操作来生成响应。路由配置定义了对于每个传入的 URL 运行哪个操作。它还提供了其他有用的功能,例如生成 SEO 友好的 URL(例如,/read/intro-to-symfony
而不是 index.php?article_id=57
)。
创建路由
路由可以在 YAML、XML、PHP 或使用属性进行配置。所有格式都提供相同的功能和性能,因此请选择您喜欢的格式。Symfony 推荐使用属性,因为它将路由和控制器放在同一位置很方便。
使用属性创建路由
PHP 属性允许在与这些路由关联的控制器代码旁边定义路由。属性在 PHP 8 及更高版本中是原生的,因此您可以立即使用它们。
在使用它们之前,您需要在项目中添加一些配置。如果您的项目使用Symfony Flex,则已为您创建此文件。否则,请手动创建以下文件
1 2 3 4 5 6 7 8 9 10
# config/routes/attributes.yaml
controllers:
resource:
path: ../../src/Controller/
namespace: App\Controller
type: attribute
kernel:
resource: App\Kernel
type: attribute
此配置告诉 Symfony 查找在 App\Controller
命名空间中声明并在遵循 PSR-4 标准的 src/Controller/
目录中存储的类上定义为属性的路由。内核也可以充当控制器,这对于将 Symfony 用作微框架的小型应用程序尤其有用。
假设您想在应用程序中为 /blog
URL 定义路由。为此,请创建一个控制器类,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// ...
}
}
此配置定义了一个名为 blog_list
的路由,当用户请求 /blog
URL 时匹配。当发生匹配时,应用程序运行 BlogController
类的 list()
方法。
注意
匹配路由时,不考虑 URL 的查询字符串。在此示例中,诸如 /blog?foo=bar
和 /blog?foo=bar&bar=foo
之类的 URL 也将匹配 blog_list
路由。
警告
如果您在同一文件中定义多个 PHP 类,Symfony 仅加载第一个类的路由,而忽略所有其他路由。
路由名称(blog_list
)现在并不重要,但稍后在生成 URL时,它将至关重要。您只需要记住,每个路由名称在应用程序中都必须是唯一的。
在 YAML、XML 或 PHP 文件中创建路由
除了在控制器类中定义路由之外,您还可以在单独的 YAML、XML 或 PHP 文件中定义它们。主要优点是它们不需要任何额外的依赖项。主要缺点是,在检查某些控制器操作的路由时,您必须使用多个文件。
以下示例展示了如何在 YAML/XML/PHP 中定义一个名为 blog_list
的路由,该路由将 /blog
URL 与 BlogController
的 list()
操作关联起来
1 2 3 4 5 6 7 8 9
# config/routes.yaml
blog_list:
path: /blog
# the controller value has the format 'controller_class::method_name'
controller: App\Controller\BlogController::list
# if the action is implemented as the __invoke() method of the
# controller class, you can skip the '::method_name' part:
# controller: App\Controller\BlogController
注意
默认情况下,Symfony 加载以 YAML 和 PHP 格式定义的路由。如果您以 XML 格式定义路由,则需要更新 src/Kernel.php 文件。
匹配 HTTP 方法
默认情况下,路由匹配任何 HTTP 谓词(GET
、POST
、PUT
等)。使用 methods
选项来限制每个路由应响应的谓词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/BlogApiController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogApiController extends AbstractController
{
#[Route('/api/posts/{id}', methods: ['GET', 'HEAD'])]
public function show(int $id): Response
{
// ... return a JSON response with the post
}
#[Route('/api/posts/{id}', methods: ['PUT'])]
public function edit(int $id): Response
{
// ... edit a post
}
}
提示
HTML 表单仅支持 GET
和 POST
方法。如果您从 HTML 表单中使用不同的方法调用路由,请添加一个名为 _method
的隐藏字段,其中包含要使用的方法(例如,<input type="hidden" name="_method" value="PUT">
)。如果您使用Symfony 表单创建表单,并且framework.http_method_override 选项为 true
,则会自动为您完成此操作。
匹配表达式
如果您需要某些路由根据某些任意匹配逻辑进行匹配,请使用 condition
选项
1 2 3 4 5 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/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\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(): Response
{
// ...
}
#[Route(
'/posts/{id}',
name: 'post_show',
// expressions can retrieve route parameter values using the "params" variable
condition: "params['id'] < 1000"
)]
public function showPost(int $id): Response
{
// ... return a JSON response with the post
}
}
condition
选项的值是使用任何有效的表达式语言语法的表达式,并且可以使用 Symfony 创建的以下任何变量
context
- RequestContext 的实例,它保存有关正在匹配的路由的最基本信息。
request
- 表示当前请求的Symfony Request 对象。
params
- 当前路由的匹配路由参数数组。
您还可以使用以下函数
env(string $name)
- 使用环境变量处理器返回变量的值
service(string $alias)
-
返回路由条件服务。
首先,将
#[AsRoutingConditionService]
属性或routing.condition_service
标签添加到您想要在路由条件中使用的服务1 2 3 4 5 6 7 8 9 10 11
use Symfony\Bundle\FrameworkBundle\Routing\Attribute\AsRoutingConditionService; use Symfony\Component\HttpFoundation\Request; #[AsRoutingConditionService(alias: 'route_checker')] class RouteChecker { public function check(Request $request): bool { // ... } }
然后,使用
service()
函数在条件内引用该服务1 2 3 4
// Controller (using an alias): #[Route(condition: "service('route_checker').check(request)")] // Or without alias: #[Route(condition: "service('App\\\Service\\\RouteChecker').check(request)")]
在幕后,表达式被编译为原始 PHP。因此,使用 condition
键不会造成额外的开销,超出底层 PHP 执行所需的时间。
警告
生成 URL 时,不考虑条件(这将在本文后面解释)。
调试路由
随着应用程序的增长,您最终将拥有很多路由。Symfony 包含一些命令来帮助您调试路由问题。首先,debug:router
命令以 Symfony 评估它们的相同顺序列出您的所有应用程序路由
1 2 3 4 5 6 7 8 9 10 11 12
$ php bin/console debug:router
---------------- ------- ------- ----- --------------------------------------------
Name Method Scheme Host Path
---------------- ------- ------- ----- --------------------------------------------
homepage ANY ANY ANY /
contact GET ANY ANY /contact
contact_process POST ANY ANY /contact
article_show ANY ANY ANY /articles/{_locale}/{year}/{title}.{_format}
blog ANY ANY ANY /blog/{page}
blog_show ANY ANY ANY /blog/{slug}
---------------- ------- ------- ----- --------------------------------------------
将某些路由的名称(或部分名称)传递给此参数以打印路由详细信息
1 2 3 4 5 6 7 8 9 10 11
$ php bin/console debug:router app_lucky_number
+-------------+---------------------------------------------------------+
| Property | Value |
+-------------+---------------------------------------------------------+
| Route Name | app_lucky_number |
| Path | /lucky/number/{max} |
| ... | ... |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
| | utf8: true |
+-------------+---------------------------------------------------------+
提示
使用 --show-aliases
选项显示给定路由的所有可用别名。
另一个命令称为 router:match
,它显示哪个路由将匹配给定的 URL。它有助于找出为什么某些 URL 没有执行您期望的控制器操作
1 2 3
$ php bin/console router:match /lucky/number/8
[OK] Route "app_lucky_number" matches
路由参数
前面的示例定义了 URL 永远不会更改的路由(例如 /blog
)。但是,通常定义路由,其中某些部分是可变的。例如,用于显示某些博客文章的 URL 可能会包含标题或 slug(例如 /blog/my-first-post
或 /blog/all-about-symfony
)。
在 Symfony 路由中,可变部分用 { }
包裹。例如,用于显示博客文章内容的路由定义为 /blog/{slug}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
// ...
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(string $slug): Response
{
// $slug will equal the dynamic part of the URL
// e.g. at /blog/yay-routing, then $slug='yay-routing'
// ...
}
}
变量部分的名称(本例中为 {slug}
)用于创建一个 PHP 变量,其中存储该路由内容并传递给控制器。如果用户访问 /blog/my-first-post
URL,Symfony 将执行 BlogController
类中的 show()
方法,并将 $slug = 'my-first-post'
参数传递给 show()
方法。
路由可以定义任意数量的参数,但每个参数在每个路由上只能使用一次(例如 /blog/posts-about-{category}/page/{pageNumber}
)。
参数验证
假设您的应用程序具有 blog_show
路由(URL:/blog/{slug}
)和 blog_list
路由(URL:/blog/{page}
)。鉴于路由参数接受任何值,因此无法区分这两个路由。
如果用户请求 /blog/my-first-post
,则两个路由都将匹配,并且 Symfony 将使用首先定义的路由。要解决此问题,请使用 requirements
选项向 {page}
参数添加一些验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])]
public function list(int $page): Response
{
// ...
}
#[Route('/blog/{slug}', name: 'blog_show')]
public function show($slug): Response
{
// ...
}
}
requirements
选项定义了PHP 正则表达式,路由参数必须匹配这些表达式,整个路由才能匹配。在此示例中,\d+
是一个正则表达式,它匹配任意长度的数字。现在
URL | 路由 | 参数 |
---|---|---|
/blog/2 |
blog_list |
$page = 2 |
/blog/my-first-post |
blog_show |
$slug = my-first-post |
提示
Requirement 枚举包含常用正则表达式常量的集合,例如数字、日期和 UUID,这些常量可以用作路由参数要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
class BlogController extends AbstractController
{
#[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => Requirement::DIGITS])]
public function list(int $page): Response
{
// ...
}
}
提示
路由要求(以及路由路径)可以包含配置参数,这对于定义一次复杂的正则表达式并在多个路由中重用它们很有用。
提示
参数还支持PCRE Unicode 属性,这些属性是匹配通用字符类型的转义序列。例如,\p{Lu}
匹配任何语言中的任何大写字符,\p{Greek}
匹配任何希腊字符,等等。
注意
在路由参数中使用正则表达式时,您可以将 utf8
路由选项设置为 true
,以使任何 .
字符匹配任何 UTF-8 字符,而不仅仅是单个字节。
如果您愿意,可以使用语法 {parameter_name<requirements>}
在每个参数中内联要求。此功能使配置更加简洁,但当要求复杂时,可能会降低路由的可读性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{page<\d+>}', name: 'blog_list')]
public function list(int $page): Response
{
// ...
}
}
可选参数
在前面的示例中,blog_list
的 URL 是 /blog/{page}
。如果用户访问 /blog/1
,它将匹配。但是,如果他们访问 /blog
,它将不匹配。一旦您向路由添加参数,它就必须有一个值。
您可以通过为 {page}
参数添加默认值,使 blog_list
再次在用户访问 /blog
时匹配。当使用属性时,默认值在控制器操作的参数中定义。在其他配置格式中,它们使用 defaults
选项定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{page}', name: 'blog_list', requirements: ['page' => '\d+'])]
public function list(int $page = 1): Response
{
// ...
}
}
现在,当用户访问 /blog
时,blog_list
路由将匹配,并且 $page
将默认为值 1
。
警告
您可以有多个可选参数(例如 /blog/{slug}/{page}
),但在可选参数之后的所有内容都必须是可选的。例如,/{page}/blog
是一个有效的路径,但 page
将始终是必需的(即 /blog
将不匹配此路由)。
如果您想始终在生成的 URL 中包含一些默认值(例如,为了强制生成 /blog/1
而不是上例中的 /blog
),请在参数名称前添加 !
字符:/blog/{!page}
与要求一样,默认值也可以使用语法 {parameter_name?default_value}
在每个参数中内联。此功能与内联要求兼容,因此您可以在单个参数中内联两者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{page<\d+>?1}', name: 'blog_list')]
public function list(int $page): Response
{
// ...
}
}
提示
要为任何参数指定 null
默认值,请在 ?
字符后不添加任何内容(例如 /blog/{page?}
)。如果这样做,请不要忘记更新相关控制器参数的类型,以允许传递 null
值(例如,将 int $page
替换为 ?int $page
)。
优先级参数
Symfony 按照路由定义的顺序评估路由。如果路由的路径匹配许多不同的模式,则可能会阻止其他路由被匹配。在 YAML 和 XML 中,您可以上下移动配置文件中的路由定义来控制其优先级。在定义为 PHP 属性的路由中,这很难做到,因此您可以在这些路由中设置可选的 priority
参数来控制其优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\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): Response
{
// ...
}
/**
* This route could not be matched without defining a higher priority than 0.
*/
#[Route('/blog/list', name: 'blog_list', priority: 2)]
public function list(): Response
{
// ...
}
}
priority 参数需要一个整数值。优先级较高的路由在优先级较低的路由之前排序。未定义时的默认值为 0
。
参数转换
常见的路由需求是将存储在某些参数中的值(例如,充当用户 ID 的整数)转换为另一个值(例如,表示用户的对象)。此功能称为“参数转换器”。
现在,保留之前的路由配置,但更改控制器操作的参数。不要使用 string $slug
,而是添加 BlogPost $post
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/BlogController.php
namespace App\Controller;
use App\Entity\BlogPost;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
// ...
#[Route('/blog/{slug}', name: 'blog_show')]
public function show(BlogPost $post): Response
{
// $post is the object whose slug matches the routing parameter
// ...
}
}
如果您的控制器参数包含对象的类型提示(本例中为 BlogPost
),则“参数转换器”会发出数据库请求,以使用请求参数(本例中为 slug
)查找对象。如果未找到对象,Symfony 会自动生成 404 响应。
查看Doctrine 参数转换文档,了解可用于自定义用于从路由参数中获取对象的数据库查询的 #[MapEntity]
属性。
Backed Enum 参数
您可以使用 PHP backed enumerations(backed 枚举) 作为路由参数,因为 Symfony 会自动将它们转换为标量值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/Controller/OrderController.php
namespace App\Controller;
use App\Enum\OrderStatusEnum;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class OrderController extends AbstractController
{
#[Route('/orders/list/{status}', name: 'list_orders_by_status')]
public function list(OrderStatusEnum $status = OrderStatusEnum::Paid): Response
{
// ...
}
}
特殊参数
除了您自己的参数之外,路由还可以包含由 Symfony 创建的以下任何特殊参数
_controller
- 此参数用于确定在路由匹配时执行哪个控制器和动作。
_format
- 匹配的值用于设置
Request
对象的“请求格式”。这用于设置响应的Content-Type
,例如,json
格式转换为application/json
的Content-Type
。 _fragment
- 用于设置片段标识符,它是 URL 的可选最后部分,以
#
字符开头,用于标识文档的一部分。 _locale
- 用于在请求上设置 locale(区域设置)。
您可以将这些属性(_fragment
除外)包含在单个路由和路由导入中。Symfony 定义了一些具有相同名称的特殊属性(下划线除外),以便您可以更轻松地定义它们
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 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(): Response
{
}
}
额外参数
在路由的 defaults
选项中,您可以选择定义未包含在路由配置中的参数。这对于将额外的参数传递给路由的控制器非常有用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog/{page}', name: 'blog_index', defaults: ['page' => 1, 'title' => 'Hello world!'])]
public function index(int $page, string $title): Response
{
// ...
}
}
路由参数中的斜线字符
路由参数可以包含除 /
斜杠字符之外的任何值,因为该字符用于分隔 URL 的不同部分。例如,如果 /share/{token}
路由中的 token
值包含 /
字符,则此路由将不匹配。
一个可能的解决方案是更改参数要求以使其更宽松
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class DefaultController extends AbstractController
{
#[Route('/share/{token}', name: 'share', requirements: ['token' => '.+'])]
public function share($token): Response
{
// ...
}
}
注意
如果路由定义了多个参数,并且您将此宽松的正则表达式应用于所有参数,则可能会得到意外的结果。例如,如果路由定义为 /share/{path}/{token}
,并且 path
和 token
都接受 /
,则 token
将仅获得最后一部分,其余部分由 path
匹配。
注意
如果路由包含特殊的 {_format}
参数,则不应为允许斜杠的参数使用 .+
要求。例如,如果模式为 /share/{token}.{_format}
并且 {token}
允许任何字符,则 /share/foo/bar.json
URL 将把 foo/bar.json
视为 token,并且格式将为空。这可以通过将 .+
要求替换为 [^.]+
来解决,以允许除点以外的任何字符。
路由别名
路由别名允许您为同一路由设置多个名称
1 2 3
# config/routes.yaml
new_route_name:
alias: original_route_name
在此示例中,original_route_name
和 new_route_name
路由都可以在应用程序中使用,并将产生相同的结果。
弃用路由别名
如果某些路由别名不再使用(因为它已过时或您决定不再维护它),您可以弃用其定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
new_route_name:
alias: original_route_name
# this outputs the following generic deprecation message:
# Since acme/package 1.2: The "new_route_name" route alias is deprecated. You should stop using it, as it will be removed in the future.
deprecated:
package: 'acme/package'
version: '1.2'
# you can also define a custom deprecation message (%alias_id% placeholder is available)
deprecated:
package: 'acme/package'
version: '1.2'
message: 'The "%alias_id%" route alias is deprecated. Do not use it anymore.'
在此示例中,每次使用 new_route_name
别名时,都会触发弃用警告,建议您停止使用该别名。
该消息实际上是一个消息模板,它将 %alias_id%
占位符的出现替换为路由别名名称。您必须在模板中至少出现一次 %alias_id%
占位符。
路由组和前缀
一组路由通常会共享一些选项(例如,所有与博客相关的路由都以 /blog
开头)。这就是 Symfony 包含共享路由配置功能的原因。
当将路由定义为属性时,请将公共配置放在控制器类的 #[Route]
属性中。在其他路由格式中,导入路由时使用选项定义公共配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/blog', requirements: ['_locale' => 'en|es|fr'], name: 'blog_')]
class BlogController extends AbstractController
{
#[Route('/{_locale}', name: 'index')]
public function index(): Response
{
// ...
}
#[Route('/{_locale}/posts/{slug}', name: 'show')]
public function show(string $slug): Response
{
// ...
}
}
警告
exclude
选项仅在 resource
值是 glob 字符串时才有效。如果您使用常规字符串(例如 '../src/Controller'
),则 exclude
值将被忽略。
在此示例中,index()
动作的路由将被称为 blog_index
,其 URL 将为 /blog/{_locale}
。show()
动作的路由将被称为 blog_show
,其 URL 将为 /blog/{_locale}/posts/{slug}
。这两个路由还将验证 _locale
参数是否与类属性中定义的正则表达式匹配。
注意
如果任何前缀路由定义了空路径,Symfony 会向其添加尾部斜杠。在前面的示例中,以 /blog
为前缀的空路径将导致 /blog/
URL。如果您想避免此行为,请将 trailing_slash_on_root
选项设置为 false
(使用 PHP 属性时,此选项不可用)
1 2 3 4 5 6 7
# config/routes/attributes.yaml
controllers:
resource: '../../src/Controller/'
type: attribute
prefix: '/blog'
trailing_slash_on_root: false
# ...
另请参阅
Symfony 可以从不同的来源导入路由,您甚至可以创建自己的路由加载器。
获取路由名称和参数
Symfony 创建的 Request
对象将所有路由配置(例如名称和参数)存储在“请求属性”中。您可以通过 Request
对象在控制器中获取此信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(Request $request): Response
{
$routeName = $request->attributes->get('_route');
$routeParameters = $request->attributes->get('_route_params');
// use this to get all the available attributes (not only routing ones):
$allAttributes = $request->attributes->all();
// ...
}
}
在服务中,您可以通过注入 RequestStack 服务来获取此信息。在模板中,使用 Twig 全局 app 变量来获取当前路由名称 (app.current_route
) 及其参数 (app.current_route_parameters
)。
特殊路由
Symfony 定义了一些特殊的控制器,用于从路由配置中渲染模板并重定向到其他路由,因此您无需创建控制器动作。
直接从路由重定向到 URL 和路由
使用 RedirectController
重定向到其他路由和 URL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# config/routes.yaml
doc_shortcut:
path: /doc
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
defaults:
route: 'doc_page'
# optionally you can define some arguments passed to the route
page: 'index'
version: 'current'
# redirections are temporary by default (code 302) but you can make them permanent (code 301)
permanent: true
# add this to keep the original query string parameters when redirecting
keepQueryParams: true
# add this to keep the HTTP method when redirecting. The redirect status changes
# * for temporary redirects, it uses the 307 status code instead of 302
# * for permanent redirects, it uses the 308 status code instead of 301
keepRequestMethod: true
# add this to remove all original route attributes when redirecting
ignoreAttributes: true
# or specify which attributes to ignore:
# ignoreAttributes: ['offset', 'limit']
legacy_doc:
path: /legacy/doc
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
defaults:
# this value can be an absolute path or an absolute URL
path: 'https://legacy.example.com/doc'
permanent: true
提示
Symfony 还提供了一些实用程序,用于在控制器内部重定向
重定向带有尾部斜杠的 URL
从历史上看,URL 遵循 UNIX 约定,为目录添加尾部斜杠(例如 https://example.com/foo/
),并删除它们以引用文件(https://example.com/foo
)。尽管为两个 URL 提供不同的内容是可以的,但现在通常将两个 URL 视为相同的 URL 并在它们之间重定向。
Symfony 遵循此逻辑在带有和不带有尾部斜杠的 URL 之间重定向(但仅适用于 GET
和 HEAD
请求)
路由 URL | 如果请求的 URL 是 /foo |
如果请求的 URL 是 /foo/ |
---|---|---|
/foo |
它匹配(200 状态响应) |
它进行 301 重定向到 /foo |
/foo/ |
它进行 301 重定向到 /foo/ |
它匹配(200 状态响应) |
子域名路由
路由可以配置 host
选项,以要求传入请求的 HTTP host 与某些特定值匹配。在以下示例中,两个路由都匹配相同的路径 (/
),但其中一个仅响应特定的主机名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/MainController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class MainController extends AbstractController
{
#[Route('/', name: 'mobile_homepage', host: 'm.example.com')]
public function mobileHomepage(): Response
{
// ...
}
#[Route('/', name: 'homepage')]
public function homepage(): Response
{
// ...
}
}
host
选项的值可以包含参数(这在多租户应用程序中很有用),并且这些参数也可以使用 requirements
进行验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// src/Controller/MainController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class MainController extends AbstractController
{
#[Route(
'/',
name: 'mobile_homepage',
host: '{subdomain}.example.com',
defaults: ['subdomain' => 'm'],
requirements: ['subdomain' => 'm|mobile'],
)]
public function mobileHomepage(): Response
{
// ...
}
#[Route('/', name: 'homepage')]
public function homepage(): Response
{
// ...
}
}
在上面的示例中,subdomain
参数定义了一个默认值,因为否则每次使用这些路由生成 URL 时都需要包含一个子域值。
提示
当导入路由时,您还可以设置 host
选项,以使所有路由都要求该主机名。
注意
使用子域路由时,您必须在功能测试中设置 Host
HTTP 标头,否则路由将不匹配
1 2 3 4 5 6 7 8 9
$crawler = $client->request(
'GET',
'/',
[],
[],
['HTTP_HOST' => 'm.example.com']
// or get the value from some configuration parameter:
// ['HTTP_HOST' => 'm.'.$client->getContainer()->getParameter('domain')]
);
提示
您还可以在 host
选项中使用内联默认值和要求格式:{subdomain<m|mobile>?m}.example.com
本地化路由 (i18n)
如果您的应用程序被翻译成多种语言,则每个路由可以为每个翻译区域设置定义不同的 URL。这避免了重复路由的需求,这也减少了潜在的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Controller/CompanyController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class CompanyController extends AbstractController
{
#[Route(path: [
'en' => '/about-us',
'nl' => '/over-ons'
], name: 'about_us')]
public function about(): Response
{
// ...
}
}
注意
当为本地化路由使用 PHP 属性时,您必须使用 path
命名参数来指定路径数组。
当匹配本地化路由时,Symfony 会在整个请求期间自动使用相同的区域设置。
提示
当应用程序使用完整的“语言 + 地区”区域设置(例如 fr_FR
、fr_BE
)时,如果所有相关区域设置中的 URL 相同,则路由可以仅使用语言部分(例如 fr
)以避免重复相同的 URL。
国际化应用程序的常见要求是以区域设置作为所有路由的前缀。这可以通过为每个区域设置定义不同的前缀来完成(如果您愿意,可以为默认区域设置设置空前缀)
1 2 3 4 5 6 7
# config/routes/attributes.yaml
controllers:
resource: '../../src/Controller/'
type: attribute
prefix:
en: '' # don't prefix URLs for English, the default locale
nl: '/nl'
注意
如果正在导入的路由在其自身定义中包含特殊的_locale 参数,则 Symfony 将仅为该区域设置导入它,而不会为其他配置的区域设置前缀导入它。
例如,如果路由在其定义中包含 locale: 'en'
并且正在使用 en
(prefix: empty) 和 nl
(prefix: /nl
) 区域设置导入,则该路由将仅在 en
区域设置中可用,而不在 nl
中可用。
另一个常见的需求是根据区域设置在不同的域上托管网站。这可以通过为每个区域设置定义不同的主机来完成。
1 2 3 4 5 6 7
# config/routes/attributes.yaml
controllers:
resource: '../../src/Controller/'
type: attribute
host:
en: 'www.example.com'
nl: 'www.example.nl'
无状态路由
有时,当 HTTP 响应应该被缓存时,确保可以发生这种情况很重要。但是,每当在请求期间启动会话时,Symfony 都会将响应转换为私有的不可缓存的响应。
有关详细信息,请参阅HTTP 缓存。
路由可以配置 stateless
布尔选项,以声明在匹配请求时不应使用会话
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Controller/MainController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
class MainController extends AbstractController
{
#[Route('/', name: 'homepage', stateless: true)]
public function homepage(): Response
{
// ...
}
}
现在,如果使用了会话,应用程序将根据您的 kernel.debug
参数报告它
enabled
:将抛出 UnexpectedSessionUsageException 异常disabled
:将记录警告
它将帮助您理解并希望修复应用程序中意外的行为。
生成 URL
路由系统是双向的
- 它们将 URL 与控制器关联(如前几节所述);
- 它们为给定的路由生成 URL。
从路由生成 URL 允许您不必在 HTML 模板中手动编写 <a href="...">
值。此外,如果某些路由的 URL 发生更改,您只需更新路由配置,所有链接都将更新。
要生成 URL,您需要指定路由的名称(例如 blog_show
)和路由定义的参数值(例如 slug = my-blog-post
)。
因此,每个路由都有一个内部名称,该名称在应用程序中必须是唯一的。如果您没有使用 name
选项显式设置路由名称,则 Symfony 会根据控制器和动作生成一个自动名称。
如果目标类具有添加路由的 __invoke()
方法并且如果目标类恰好添加了一个路由,则 Symfony 会根据 FQCN 声明路由别名。Symfony 还会为每个仅定义一个路由的方法自动添加别名。考虑以下类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Controller/MainController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
final class MainController extends AbstractController
{
#[Route('/', name: 'homepage')]
public function homepage(): Response
{
// ...
}
}
Symfony 将添加一个名为 App\Controller\MainController::homepage
的路由别名。
在控制器中生成 URL
如果您的控制器从AbstractController 扩展,请使用 generateUrl()
助手
1 2 3 4 5 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/Controller/BlogController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class BlogController extends AbstractController
{
#[Route('/blog', name: 'blog_list')]
public function list(): Response
{
// generate a URL with no route arguments
$signUpPage = $this->generateUrl('sign_up');
// generate a URL with route arguments
$userProfilePage = $this->generateUrl('user_profile', [
'username' => $user->getUserIdentifier(),
]);
// generated URLs are "absolute paths" by default. Pass a third optional
// argument to generate different URLs (e.g. an "absolute URL")
$signUpPage = $this->generateUrl('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);
// when a route is localized, Symfony uses by default the current request locale
// pass a different '_locale' value if you want to set the locale explicitly
$signUpPageInDutch = $this->generateUrl('sign_up', ['_locale' => 'nl']);
// ...
}
}
注意
如果您将一些未包含在路由定义中的参数传递给 generateUrl()
方法,它们将作为查询字符串包含在生成的 URL 中
1 2 3
$this->generateUrl('blog', ['page' => 2, 'category' => 'Symfony']);
// the 'blog' route only defines the 'page' parameter; the generated URL is:
// /blog/2?category=Symfony
警告
当对象用作占位符时,它们会被转换为字符串,但当用作额外参数时,它们不会被转换。因此,如果您将对象(例如 Uuid)作为额外参数的值传递,则需要显式将其转换为字符串
1
$this->generateUrl('blog', ['uuid' => (string) $entity->getUuid()]);
如果您的控制器未从 AbstractController
扩展,您需要在控制器中获取服务,并按照下一节的说明进行操作。
在服务中生成 URL
将 router
Symfony 服务注入到您自己的服务中,并使用其 generate()
方法。当使用服务自动装配时,您只需要在服务构造函数中添加一个参数,并使用 UrlGeneratorInterface 类进行类型提示
1 2 3 4 5 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 33
// src/Service/SomeService.php
namespace App\Service;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SomeService
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
) {
}
public function someMethod(): void
{
// ...
// generate a URL with no route arguments
$signUpPage = $this->urlGenerator->generate('sign_up');
// generate a URL with route arguments
$userProfilePage = $this->urlGenerator->generate('user_profile', [
'username' => $user->getUserIdentifier(),
]);
// generated URLs are "absolute paths" by default. Pass a third optional
// argument to generate different URLs (e.g. an "absolute URL")
$signUpPage = $this->urlGenerator->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);
// when a route is localized, Symfony uses by default the current request locale
// pass a different '_locale' value if you want to set the locale explicitly
$signUpPageInDutch = $this->urlGenerator->generate('sign_up', ['_locale' => 'nl']);
}
}
在模板中生成 URL
阅读关于 Symfony 模板主文章中关于创建页面之间链接的部分。
在 JavaScript 中生成 URL
如果您的 JavaScript 代码包含在 Twig 模板中,则可以使用 path()
和 url()
Twig 函数生成 URL 并将其存储在 JavaScript 变量中。需要 escape()
过滤器来转义任何非 JavaScript 安全的值
1 2 3
<script>
const route = "{{ path('blog_show', {slug: 'my-blog-post'})|escape('js') }}";
</script>
如果您需要动态生成 URL 或者您正在使用纯 JavaScript 代码,则此解决方案不起作用。在这些情况下,请考虑使用 FOSJsRoutingBundle。
在命令中生成 URL
在命令中生成 URL 的工作方式与在服务中生成 URL相同。唯一的区别是命令不在 HTTP 上下文中执行。因此,如果您生成绝对 URL,您将获得 http://127.0.0.1/
作为主机名,而不是您的真实主机名。
解决方案是配置 default_uri
选项,以定义命令生成 URL 时使用的“请求上下文”
1 2 3 4 5
# config/packages/routing.yaml
framework:
router:
# ...
default_uri: 'https://example.org/my/path/'
现在,在命令中生成 URL 时,您将获得预期的结果
1 2 3 4 5 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 33 34 35 36 37
// src/Command/SomeCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
// ...
class SomeCommand extends Command
{
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// generate a URL with no route arguments
$signUpPage = $this->urlGenerator->generate('sign_up');
// generate a URL with route arguments
$userProfilePage = $this->urlGenerator->generate('user_profile', [
'username' => $user->getUserIdentifier(),
]);
// by default, generated URLs are "absolute paths". Pass a third optional
// argument to generate different URIs (e.g. an "absolute URL")
$signUpPage = $this->urlGenerator->generate('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);
// when a route is localized, Symfony uses by default the current request locale
// pass a different '_locale' value if you want to set the locale explicitly
$signUpPageInDutch = $this->urlGenerator->generate('sign_up', ['_locale' => 'nl']);
// ...
}
}
注意
默认情况下,为 Web 资源生成的 URL 使用相同的 default_uri
值,但您可以使用 asset.request_context.base_path
和 asset.request_context.secure
容器参数来更改它。
检查路由是否存在
在高度动态的应用程序中,可能需要在使用路由生成 URL 之前检查路由是否存在。在这些情况下,请勿使用 getRouteCollection() 方法,因为这会重新生成路由缓存并减慢应用程序的速度。
相反,尝试生成 URL 并捕获当路由不存在时抛出的 RouteNotFoundException。
1 2 3 4 5 6 7 8 9
use Symfony\Component\Routing\Exception\RouteNotFoundException;
// ...
try {
$url = $this->router->generate($routeName, $routeParameters);
} catch (RouteNotFoundException $e) {
// the route is not defined...
}
强制在生成的 URL 上使用 HTTPS
注意
如果您的服务器在终止 SSL 的代理后面运行,请确保配置 Symfony 以在代理后面工作
scheme 的配置仅用于非 HTTP 请求。schemes
选项与不正确的代理配置一起使用将导致重定向循环。
默认情况下,生成的 URL 使用与当前请求相同的 HTTP scheme。在没有 HTTP 请求的控制台命令中,URL 默认使用 http
。您可以按命令(通过路由器的 getContext()
方法)或使用以下配置参数全局更改此设置
1 2 3 4
# config/services.yaml
parameters:
router.request_context.scheme: 'https'
asset.request_context.secure: true
在控制台命令之外,使用 schemes
选项显式定义每个路由的 scheme
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/SecurityController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'login', schemes: ['https'])]
public function login(): Response
{
// ...
}
}
为 login
路由生成的 URL 将始终使用 HTTPS。这意味着当使用 path()
Twig 函数生成 URL 时,如果原始请求的 HTTP scheme 与路由使用的 scheme 不同,您可能会获得绝对 URL 而不是相对 URL
1 2 3 4 5 6
{# if the current scheme is HTTPS, generates a relative URL: /login #}
{{ path('login') }}
{# if the current scheme is HTTP, generates an absolute URL to change
the scheme: https://example.com/login #}
{{ path('login') }}
scheme 要求也适用于传入请求。如果您尝试使用 HTTP 访问 /login
URL,您将自动重定向到相同的 URL,但使用 HTTPS scheme。
如果您想强制一组路由使用 HTTPS,您可以在导入它们时定义默认 scheme。以下示例强制将 HTTPS 应用于所有定义为属性的路由
1 2 3 4 5
# config/routes/attributes.yaml
controllers:
resource: '../../src/Controller/'
type: attribute
schemes: [https]
注意
Security 组件提供了另一种通过 requires_channel
设置强制 HTTP 或 HTTPS 的方法。
签名 URI
签名 URI 是包含哈希值的 URI,该哈希值取决于 URI 的内容。这样,您稍后可以通过重新计算其哈希值并将其与 URI 中包含的哈希值进行比较来检查签名 URI 的完整性。
Symfony 通过 UriSigner 服务提供了一个实用程序来签名 URI,您可以将其注入到您的服务或控制器中
1 2 3 4 5 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/Service/SomeService.php
namespace App\Service;
use Symfony\Component\HttpFoundation\UriSigner;
class SomeService
{
public function __construct(
private UriSigner $uriSigner,
) {
}
public function someMethod(): void
{
// ...
// generate a URL yourself or get it somehow...
$url = 'https://example.com/foo/bar?sort=desc';
// sign the URL (it adds a query parameter called '_hash')
$signedUrl = $this->uriSigner->sign($url);
// $url = 'https://example.com/foo/bar?sort=desc&_hash=e4a21b9'
// check the URL signature
$uriSignatureIsValid = $this->uriSigner->check($signedUrl);
// $uriSignatureIsValid = true
// if you have access to the current Request object, you can use this
// other method to pass the entire Request object instead of the URI:
$uriSignatureIsValid = $this->uriSigner->checkRequest($request);
}
}
出于安全原因,通常使签名 URI 在一段时间后过期(例如,在将它们用于重置用户凭据时)。默认情况下,签名 URI 不会过期,但您可以使用 sign() 的 $expiration
参数定义过期日期/时间
1 2 3 4 5 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/Service/SomeService.php
namespace App\Service;
use Symfony\Component\HttpFoundation\UriSigner;
class SomeService
{
public function __construct(
private UriSigner $uriSigner,
) {
}
public function someMethod(): void
{
// ...
// generate a URL yourself or get it somehow...
$url = 'https://example.com/foo/bar?sort=desc';
// sign the URL with an explicit expiration date
$signedUrl = $this->uriSigner->sign($url, new \DateTimeImmutable('2050-01-01'));
// $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=2524608000&_hash=e4a21b9'
// if you pass a \DateInterval, it will be added from now to get the expiration date
$signedUrl = $this->uriSigner->sign($url, new \DateInterval('PT10S')); // valid for 10 seconds from now
// $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=1712414278&_hash=e4a21b9'
// you can also use a timestamp in seconds
$signedUrl = $this->uriSigner->sign($url, 4070908800); // timestamp for the date 2099-01-01
// $signedUrl = 'https://example.com/foo/bar?sort=desc&_expiration=4070908800&_hash=e4a21b9'
}
}
注意
过期日期/时间作为时间戳通过 _expiration
查询参数包含在签名 URI 中。
7.1
为签名 URI 添加过期日期的功能在 Symfony 7.1 中引入。
注意
生成的 URI 哈希可能包含 /
和 +
字符,这可能会导致某些客户端出现问题。如果您遇到此问题,请使用以下内容替换它们:strtr($hash, ['/' => '_', '+' => '-'])
。
故障排除
以下是在使用路由时可能会看到的一些常见错误
1 2
Controller "App\\Controller\\BlogController::show()" requires that you
provide a value for the "$slug" argument.
当您的控制器方法具有参数(例如 $slug
)时,会发生这种情况
1 2 3 4
public function show(string $slug): Response
{
// ...
}
但您的路由路径没有 {slug}
参数(例如,它是 /blog/show
)。将 {slug}
添加到您的路由路径:/blog/show/{slug}
,或为参数指定默认值(即 $slug = null
)。
1 2
Some mandatory parameters are missing ("slug") to generate a URL for route
"blog_show".
这意味着您正在尝试为 blog_show
路由生成 URL,但您没有传递 slug
值(这是必需的,因为它在路由路径中具有 {slug}
参数)。要解决此问题,请在生成路由时传递 slug
值
1
$this->generateUrl('blog_show', ['slug' => 'slug-value']);
或者,在 Twig 中
1
{{ path('blog_show', {slug: 'slug-value'}) }}