Symfony框架最佳实践
本文描述了使用 Symfony 开发 Web 应用程序的最佳实践,这些实践符合原始 Symfony 创建者设想的理念。
如果您不同意其中一些建议,它们可能是一个很好的起点,您可以扩展并使其适应您的特定需求。您甚至可以完全忽略它们,并继续使用您自己的最佳实践和方法。Symfony 足够灵活,可以适应您的需求。
本文假设您已经有开发 Symfony 应用程序的经验。如果您没有,请先阅读文档的入门部分。
提示
Symfony 提供了一个名为 Symfony Demo 的示例应用程序,该应用程序遵循所有这些最佳实践,因此您可以在实践中体验它们。
创建项目
使用 Symfony Binary 创建 Symfony 应用程序
Symfony binary 是在您下载 Symfony 时在您的机器上创建的可执行命令。它提供了多种实用程序,包括创建新的 Symfony 应用程序的最简单方法
1
$ symfony new my_project_directory
在底层,此 Symfony binary 命令执行所需的 Composer 命令,以基于当前稳定版本创建一个新的 Symfony 应用程序。
使用默认目录结构
除非您的项目遵循强制使用特定目录结构的开发实践,否则请遵循默认的 Symfony 目录结构。它扁平化、不言自明,并且不与 Symfony 耦合
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
your_project/
├─ assets/
├─ bin/
│ └─ console
├─ config/
│ ├─ packages/
│ ├─ routes/
│ └─ services.yaml
├─ migrations/
├─ public/
│ ├─ build/
│ └─ index.php
├─ src/
│ ├─ Kernel.php
│ ├─ Command/
│ ├─ Controller/
│ ├─ DataFixtures/
│ ├─ Entity/
│ ├─ EventSubscriber/
│ ├─ Form/
│ ├─ Repository/
│ ├─ Security/
│ └─ Twig/
├─ templates/
├─ tests/
├─ translations/
├─ var/
│ ├─ cache/
│ └─ log/
└─ vendor/
配置
使用环境变量进行基础设施配置
这些选项的值在一台机器与另一台机器之间发生变化(例如,从您的开发机器到生产服务器),但它们不会修改应用程序的行为。
在您的项目中使用 env vars 定义这些选项,并创建多个 .env
文件来为每个环境配置 env vars。
使用密钥存储敏感信息
当您的应用程序具有敏感配置(如 API 密钥)时,您应该通过 Symfony 的密钥管理系统安全地存储这些配置。
使用参数进行应用配置
这些是用于修改应用程序行为的选项,例如电子邮件通知的发送者,或启用的 功能开关。它们的值不会因机器而异,因此不要将它们定义为环境变量。
在 config/services.yaml
文件中将这些选项定义为参数。您可以在 config/services_dev.yaml
和 config/services_prod.yaml
文件中为每个环境覆盖这些选项。
除非应用程序配置被多次重用并且需要严格的验证,否则不要使用 Config 组件来定义选项。
使用简短且带前缀的参数名称
考虑使用 app.
作为您的参数的前缀,以避免与 Symfony 和第三方 bundle/库参数发生冲突。然后,仅使用一两个词来描述参数的目的
1 2 3 4 5 6 7 8 9 10
# config/services.yaml
parameters:
# don't do this: 'dir' is too generic, and it doesn't convey any meaning
app.dir: '...'
# do this: short but easy to understand names
app.contents_dir: '...'
# it's OK to use dots, underscores, dashes or nothing, but always
# be consistent and use the same format for all the parameters
app.dir.contents: '...'
app.contents-dir: '...'
使用常量定义极少更改的选项
诸如在某些列表中要显示的条目数之类的配置选项很少更改。与其将它们定义为配置参数,不如在相关类中将它们定义为 PHP 常量。示例
1 2 3 4 5 6 7 8 9
// src/Entity/Post.php
namespace App\Entity;
class Post
{
public const NUMBER_OF_ITEMS = 10;
// ...
}
常量的主要优点是您可以在任何地方使用它们,包括 Twig 模板和 Doctrine 实体,而参数仅可从可以访问服务容器的位置访问。
对于此类配置值,使用常量的唯一显着缺点是,在测试中重新定义它们的值很复杂。
业务逻辑
不要创建任何 Bundle 来组织您的应用程序逻辑
当 Symfony 2.0 发布时,应用程序使用 bundles 将其代码划分为逻辑功能:UserBundle、ProductBundle、InvoiceBundle 等。但是,bundle 旨在成为可以作为独立软件重用的东西。
如果您需要在您的项目中重用某些功能,请为其创建一个 bundle(在私有仓库中,不要使其公开可用)。对于您的应用程序代码的其余部分,请使用 PHP 命名空间来组织代码,而不是 bundle。
使用自动连线来自动化配置应用程序服务
服务自动连线是一项功能,它可以读取您构造函数(或其他方法)上的类型提示,并自动将正确的服务传递给每个方法,从而无需显式配置服务并简化应用程序维护。
服务应尽可能设为私有
将服务设为私有,以防止您通过 $container->get()
访问这些服务。相反,您将需要使用适当的依赖注入。
使用 YAML 格式配置您自己的服务
如果您使用默认的 services.yaml 配置,则大多数服务将自动配置。但是,在某些边缘情况下,您需要手动配置服务(或其中的一部分)。
YAML 是推荐的配置服务格式,因为它对新手友好且简洁,但 Symfony 也支持 XML 和 PHP 配置。
使用属性定义 Doctrine 实体映射
Doctrine 实体是您存储在某个“数据库”中的普通 PHP 对象。Doctrine 仅通过为您模型类配置的映射元数据来了解您的实体。
Doctrine 支持多种元数据格式,但建议使用 PHP 属性,因为它们是设置和查找映射信息的最便捷和最灵活的方式。
控制器
使您的控制器继承 AbstractController
基类控制器
Symfony 提供了一个基类控制器,其中包括最常见需求的快捷方式,例如渲染模板或检查安全权限。
从这个基类控制器扩展您的控制器会将您的应用程序耦合到 Symfony。耦合通常是错误的,但在这种情况下可能是可以接受的,因为控制器不应包含任何业务逻辑。控制器应仅包含几行胶水代码,因此您不会耦合应用程序的重要部分。
使用属性配置路由、缓存和安全性
使用属性进行路由、缓存和安全配置简化了配置。您无需浏览使用不同格式(YAML、XML、PHP)创建的多个文件:所有配置都正好在您需要它的地方,并且仅使用一种格式。
使用依赖注入获取服务
如果您扩展了基类 AbstractController
,您只能通过 $this->container->get()
直接从容器访问最常用的服务(例如 twig
、router
、doctrine
等)。相反,您必须使用依赖注入,通过类型提示操作方法参数或构造函数参数来获取服务。
如果实体值解析器方便,则使用它们
如果您正在使用 Doctrine,那么您可以可选地使用 EntityValueResolver 来自动查询实体并将其作为参数传递给您的控制器。如果找不到实体,它还将显示 404 页面。
如果从路由变量获取实体的逻辑更复杂,则最好在控制器内部进行 Doctrine 查询(例如,通过调用 Doctrine 仓库方法),而不是配置 EntityValueResolver。
模板
模板名称和变量使用蛇形命名法
模板名称、目录和变量使用小写蛇形命名法(例如,user_profile
而不是 userProfile
,以及 product/edit_form.html.twig
而不是 Product/EditForm.html.twig
)。
模板片段以一个下划线作为前缀
模板片段,也称为“局部模板”,允许重用模板内容。以一个下划线作为前缀命名它们,以便更好地区分它们与完整模板(例如,_user_metadata.html.twig
或 _caution_message.html.twig
)。
表单
将您的表单定义为 PHP 类
在类中创建表单允许在应用程序的不同部分重用它们。此外,不在控制器中创建表单简化了控制器的代码和维护。
在模板中添加表单按钮
表单类应该与它们将在何处使用无关。例如,用于创建和编辑条目的表单的按钮应根据其使用位置,从“添加新的”更改为“保存更改”。
建议在模板中添加按钮,而不是在表单类或控制器中添加按钮。这还提高了关注点分离,因为按钮样式(CSS 类和其他属性)是在模板中定义的,而不是在 PHP 类中定义的。
但是,如果您创建了一个带有多个提交按钮的表单,则应在控制器中而不是在模板中定义它们。否则,您将无法在控制器中处理表单时检查单击了哪个按钮。
在底层对象上定义验证约束
将验证约束附加到表单字段而不是映射对象,会阻止验证在其他表单或对象使用的其他位置被重用。
使用单个操作来渲染和处理表单
渲染表单和处理表单是处理表单时的两个主要任务。两者非常相似(大多数时候几乎相同),因此让单个控制器操作处理两者会简单得多。
国际化
为您的翻译文件使用 XLIFF 格式
在 Symfony 支持的所有翻译格式(PHP、Qt、.po
、.mo
、JSON、CSV、INI 等)中,XLIFF
和 gettext
在专业翻译人员使用的工具中具有最佳支持。而且由于它基于 XML,因此您可以在编写 XLIFF
文件内容时对其进行验证。
Symfony 还支持 XLIFF 文件中的注释,使其对翻译人员更友好。最终,好的翻译都与上下文有关,而这些 XLIFF 注释允许您定义该上下文。
翻译使用键而不是内容字符串
使用键简化了翻译文件的管理,因为您可以更改模板、控制器和服务中的原始内容,而无需更新所有翻译文件。
键应始终描述其目的,而不是其位置。例如,如果表单有一个标签为“用户名”的字段,那么一个好的键将是 label.username
,而不是 edit_form.label.username
。
安全
定义单个防火墙
除非您有两个合法不同的身份验证系统和用户(例如,主站点的表单登录和仅用于您的 API 的令牌系统),否则建议仅使用一个防火墙以保持简单。
此外,您应该在防火墙下使用 anonymous
键。如果您要求用户在您站点的不同部分登录,请使用 access_control 选项。
使用 auto
密码哈希器
auto 密码哈希器根据您的 PHP 安装自动选择最佳可能的编码器/哈希器。目前,默认的 auto 哈希器是 bcrypt
。
使用 Voter 实现细粒度的安全限制
如果您的安全逻辑很复杂,您应该创建自定义安全 Voter,而不是在 #[Security]
属性中定义长表达式。
Web 资源
使用 AssetMapper 管理 Web 资源
Web 资源是使您网站的前端看起来和工作起来都很棒的 CSS、JavaScript 和图像文件。AssetMapper 让您可以编写现代 JavaScript 和 CSS,而无需使用诸如 Webpack(直接或通过 Webpack Encore)之类的打包器。
测试
冒烟测试您的 URL
在软件工程中,冒烟测试包括“初步测试,以揭示严重到足以拒绝预期软件版本的简单故障”。使用 PHPUnit 数据提供器,您可以定义一个功能测试,以检查所有应用程序 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
// tests/ApplicationAvailabilityFunctionalTest.php
namespace App\Tests;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ApplicationAvailabilityFunctionalTest extends WebTestCase
{
/**
* @dataProvider urlProvider
*/
public function testPageIsSuccessful($url): void
{
$client = self::createClient();
$client->request('GET', $url);
$this->assertResponseIsSuccessful();
}
public function urlProvider(): \Generator
{
yield ['/'];
yield ['/posts'];
yield ['/post/fixture-post-1'];
yield ['/blog/category/fixture-category'];
yield ['/archives'];
// ...
}
}
在创建应用程序时添加此测试,因为它几乎不需要任何工作,并且可以检查您的页面是否都没有返回错误。稍后,您将为每个页面添加更具体的测试。
在功能测试中硬编码 URL
在 Symfony 应用程序中,建议使用路由生成 URL,以便在 URL 更改时自动更新所有链接。但是,如果公共 URL 更改,则除非您设置重定向到新 URL,否则用户将无法浏览它。
这就是为什么建议在测试中使用原始 URL 而不是从路由生成它们的原因。每当路由更改时,测试都会失败,您就会知道您必须设置重定向。