PHP-CS-Fixer braces_position 规则详解:7 个配置项精准控制花括号位置
【免费下载链接】PHP-CS-FixerA tool to automatically fix PHP Coding Standards issues项目地址: https://gitcode.com/gh_mirrors/ph/PHP-CS-Fixer
braces_position是 PHP-CS-Fixer 中负责统一花括号({)位置的规则,允许你按类、匿名类、函数、匿名函数、控制结构五类场景分别指定开括号"同行"或"换行"的风格,同时控制匿名函数/匿名类是否允许单行书写。读完本文,你将掌握该规则的 7 个配置项含义、默认值与"future-mode"差异,看懂全部 8 个官方转换示例,并了解其底层 token 处理逻辑与在@PSR12、@Symfony等规则集中的作用。
规则概览
规则braces_position的官方定义只有一句话:Braces must be placed as configured(花括号必须按配置放置)。它属于可配置规则(CONFIGURABLE),对应 Fixer 类为src/Fixer/Basic/BracesPositionFixer.php,测试类为tests/Fixer/Basic/BracesPositionFixerTest.php。
从源码看,该 Fixer 实现了ConfigurableFixerInterface与WhitespacesAwareFixerInterface,并混入ConfigurableFixerTrait与IndentationTrait。其isCandidate()判断逻辑非常简单:只要 token 流中出现{字符($tokens->isTokenKindFound('{'))就参与修复,因此几乎对所有 PHP 文件生效。
配置项详解:7 个选项、两类取值
规则共开放 7 个配置项。其中 5 个控制"开括号位置"的选项,取值只有两个枚举字符串:
| 取值 | 含义 |
|---|---|
same_line | 开括号与前一行代码放在同一行(K&R / Allman 混合风格中的同行风格) |
next_line_unless_newline_at_signature_end | 开括号放在下一行;但如果签名末尾(右括号)或返回类型之后)已经存在换行,则保持同行 |
第二个取值是"有条件的换行":它默认把花括号放到下一行,却允许多行签名(如每个参数单独一行的写法)保留"签名末行同行"的形态,避免出现)单独一行后又跟一个{的怪异排版。该常量在源码中定义为BracesPositionFixer::NEXT_LINE_UNLESS_NEWLINE_AT_SIGNATURE_END与SAME_LINE(见 BracesPositionFixer.php)。
下表汇总 7 个选项的类型、允许值与默认值(均与 官方文档 及源码createConfigurationDefinition()一致):
| 配置项 | 类型 | 允许值 | 默认值 |
|---|---|---|---|
allow_single_line_anonymous_functions | bool | true/false | true(future-mode 下为false) |
allow_single_line_empty_anonymous_classes | bool | true/false | true |
anonymous_classes_opening_brace | 枚举 | next_line_unless_newline_at_signature_end/same_line | same_line |
anonymous_functions_opening_brace | 枚举 | 同上 | same_line |
classes_opening_brace | 枚举 | 同上 | next_line_unless_newline_at_signature_end |
control_structures_opening_brace | 枚举 | 同上 | same_line |
functions_opening_brace | 枚举 | 同上 | next_line_unless_newline_at_signature_end |
默认值背后的"风格分工"
从默认值可以读出 PHP-CS-Fixer 默认遵循的混合风格:
- 类与具名函数:开括号换行(
next_line_unless_newline_at_signature_end)——对应 PSR-12 中类声明、函数声明花括号独占一行的要求; - 控制结构、匿名函数、匿名类:开括号同行(
same_line)——对应if (...) {、function () {、new class {的常见写法。
两个布尔选项则控制"单行紧凑写法"的放行范围:默认允许匿名函数写成function () { return true; }、允许空匿名类写成new class {},但非空的匿名类即使开了allow_single_line_empty_anonymous_classes也会被展开为多行(见示例 #7)。
future-mode 的默认值差异
allow_single_line_anonymous_functions的文档标注了"Default value (future-mode):false"。其实现位于源码的配置定义处:->setDefault(Future::getV4OrV3(false, true))。通过src/Future.php中的Future::getV4OrV3()可知:在启用"未来模式"(设置环境变量PHP_CS_FIXER_FUTURE_MODE或经runWithEnforcedFutureMode()强制执行)时取新值false,即未来大版本将默认禁止匿名函数单行书写;普通模式下取旧值true。这是 PHP-CS-Fixer 为 v4 平滑迁移提供的"预览默认值"机制,理解它有助于提前评估升级影响。
8 个官方示例:默认与定制行为对照
官方文档提供了 8 个 diff 示例,完整覆盖默认配置与 6 种定制组合,逐一说明如下。
示例 #1:默认配置下的整体效果
<?php -class Foo { +class Foo +{ } -function foo() { +function foo() +{ } -$foo = function() -{ +$foo = function() { }; -if (foo()) -{ +if (foo()) { bar(); } -$foo = new class -{ +$foo = new class { };可以看到:类、具名函数的开括号被移到下一行;匿名函数、控制结构、匿名类的开括号被移到上一行(同行)。这正是默认值组合的直接体现。
示例 #2:控制结构开括号换行
配置:['control_structures_opening_brace' => 'next_line_unless_newline_at_signature_end']
<?php -if (foo()) { +if (foo()) +{ bar(); }适用于希望if/for/while/switch/try等控制结构也采用 Allman 风格(花括号独占一行)的团队。注意此配置对else、elseif、catch、finally、do、declare、match同样生效——源码中CONTROL_STRUCTURE_TOKENS常量枚举了全部目标 token:T_DECLARE, T_DO, T_ELSE, T_ELSEIF, T_FINALLY, T_FOR, T_FOREACH, T_IF, T_WHILE, T_TRY, T_CATCH, T_SWITCH以及FCT::T_MATCH(见 BracesPositionFixer.php)。
示例 #3:具名函数开括号同行
配置:['functions_opening_brace' => 'same_line']
<?php -function foo() -{ +function foo() { }适合采用 K&R 风格书写函数体的团队。需要留意:该选项同时作用于命名函数与方法。
示例 #4:匿名函数开括号换行
配置:['anonymous_functions_opening_brace' => 'next_line_unless_newline_at_signature_end']
<?php -$foo = function () { +$foo = function () +{ };示例 #5:类开括号同行
配置:['classes_opening_brace' => 'same_line']
<?php -class Foo -{ +class Foo { }示例 #6:匿名类开括号换行
配置:['anonymous_classes_opening_brace' => 'next_line_unless_newline_at_signature_end']
<?php -$foo = new class { +$foo = new class +{ };示例 #7:允许单行空匿名类
配置:['allow_single_line_empty_anonymous_classes' => true]
<?php $foo = new class { }; -$bar = new class { private $baz; }; +$bar = new class { +private $baz; +};关键语义:只有"空"的匿名类(花括号间仅含空白或注释)才能保持单行;一旦体内有实际代码(如属性private $baz;),即使开启该选项也会被展开为多行。源码中通过$allowSingleLineIfEmpty分支配合"遍历括号内 token,发现非空白、非注释内容即强制多行"的逻辑实现(见 BracesPositionFixer.php)。
示例 #8:允许单行匿名函数
配置:['allow_single_line_anonymous_functions' => true]
<?php $foo = function () { return true; }; -$bar = function () { $result = true; - return $result; }; +$bar = function () { +$result = true; + return $result; +};同理,单行放行只适用于真正单行的匿名函数;如果原代码在花括号内出现换行(如示例中$bar的写法),规则会把整个函数体展开为标准多行结构。
深入源码:开括号定位与边界处理
applyFix()是整个规则的核心(BracesPositionFixer.php),它按 token 类型分流处理:
- 类/匿名类:命中类 token 后向后找
{;再经TokensAnalyzer::isAnonymousClass()区分匿名类与具名类,分别读取anonymous_classes_opening_brace或classes_opening_brace配置。 - 函数/匿名函数:命中
T_FUNCTION后查找{、;或属性钩子花括号;若遇到;(如抽象方法或接口方法)则跳过;用isLambda()区分匿名函数与具名函数。 - 控制结构:先定位
(...)参数块的结束位置(findParenthesisEnd()用BLOCK_TYPE_PARENTHESIS查找配对),再取其后第一个有意义 token 判断是否为{。 - 属性钩子(PHP 8.4):命中
T_VARIABLE且后续出现CT::T_PROPERTY_HOOK_BRACE_OPEN时,按控制结构规则整理属性钩子的花括号位置,并跳过数组默认值等干扰场景。
两个值得注意的实现细节:
- 签名末端换行检测:当配置为
next_line_unless_newline_at_signature_end时,源码会从开括号向前回溯,跳过返回类型相关的 token(CT::T_TYPE_COLON、T_NULLABLE_TYPE、T_STRING、T_NS_SEPARATOR、T_STATIC、T_CALLABLE、联合/交叉类型等,见 BracesPositionFixer.php),检查右括号前是否存在换行。测试用例next line with multiline signature与next line with multiline signature and return type系列(BracesPositionFixerTest.php)验证了多行签名、?int、array、类名、callable等返回类型下的行为。 - 注释安全:开括号前后存在注释时,规则不会粗暴挪动括号,而是借助
hasCommentOnSameLine()、isFollowedByNewLine()等辅助方法把括号移动到注释后的合理位置。测试集中open brace preceded by comment and whitespace、open brace surrounded by comment and whitespace等用例(BracesPositionFixerTest.php)保证了注释场景下不产生破坏性变更。
与相邻规则的执行顺序
getPriority()返回-2,并声明了明确的运行顺序约束:
- 必须在该规则之后运行(Must run after):
ControlStructureBracesFixer、MultilinePromotedPropertiesFixer、NoMultipleStatementsPerLineFixer; - 必须在该规则之前运行(Must run before):
SingleLineEmptyBodyFixer、StatementIndentationFixer。
这保证了:控制结构先由control_structure_braces补齐花括号,多行提升属性与单行多语句先整理完毕,braces_position再统一花括号位置,最后由缩进与空体规则收尾。
测试覆盖:官方兼容性承诺
官方文档明确说明:"The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise."(测试类定义了官方支持的行为,每个用例都是向后兼容承诺的一部分)。BracesPositionFixerTest.php共 1117 行,测试矩阵包括:
- 全部控制结构(
if/else/elseif/else if/for/foreach/while/do-while/switch/try-catch-finally)的默认与"next line"两种形态; - 类、函数、匿名函数、匿名类的默认与定制形态;
- 注释(行注释
//与块注释/* */)与开括号的多种组合; - 多行签名、返回类型(
int、?int、array、\Foo\Bar、callable、static); - PHP 8.0 联合类型、8.1 交叉类型、8.2 DNF 类型、8.4 属性钩子(
property hook,包括提升属性中的钩子与带默认值的钩子)等版本特性用例(以#[RequiresPhp]注解按版本门控)。
这些用例同时是你在@PSR12、@Symfony等规则集下启用本规则时的行为基准。
所属规则集与内置配置
braces_position被以下官方规则集收录(完整清单见 官方文档 与src/RuleSet/Sets目录):
| 规则集 | 内置配置 |
|---|---|
@PER、@PER-CS、@PER-CS1.0、@PER-CS1x0、@PER-CS2.0、@PER-CS2x0、@PER-CS3.0、@PER-CS3x0 | ['allow_single_line_anonymous_functions' => false, 'allow_single_line_empty_anonymous_classes' => true] |
@PSR12 | ['allow_single_line_anonymous_functions' => false, 'allow_single_line_empty_anonymous_classes' => true] |
@PSR2 | ['allow_single_line_anonymous_functions' => false] |
@PhpCsFixer、@Symfony | ['allow_single_line_anonymous_functions' => true, 'allow_single_line_empty_anonymous_classes' => true] |
对照源码可以印证(PSR12Set.php、PSR2Set.php、SymfonySet.php):PSR 系列明确禁止匿名函数单行(false),而@PhpCsFixer/@Symfony则保留默认的true。这也解释了为何同一份代码在不同规则集下格式化结果可能不同——选择规则集前应确认其对本规则的覆盖配置。
使用建议
- 命令行快速启用:
php php-cs-fixer fix path/to/file.php --rules='braces_position'(在仓库根目录执行),或通过.php-cs-fixer.php配置文件在rules数组中按需定制。 - 典型定制场景:若团队采用全量 Allman 风格,可将
control_structures_opening_brace与functions_opening_brace同时设为next_line_unless_newline_at_signature_end;若坚持 K&R 风格,则把classes_opening_brace设为same_line即可。 - 升级兼容:关注
PHP_CS_FIXER_FUTURE_MODE环境变量下allow_single_line_anonymous_functions将变为false的预告,提前在 CI 中验证未来默认值对代码库的影响。
从文档到测试再到源码实现,braces_position展示了 PHP-CS-Fixer 在"花括号位置"这一基础排版问题上的完整工程化方案:精细的配置粒度、明确的枚举取值、注释与多行签名的边界保护,以及严格的测试兼容承诺。掌握它,即可在团队中落地统一、可预期的大括号风格。
【免费下载链接】PHP-CS-FixerA tool to automatically fix PHP Coding Standards issues项目地址: https://gitcode.com/gh_mirrors/ph/PHP-CS-Fixer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考