PSR-12 扩展编码样式
PSR-12(PHP Standard Recommendation 12)是 PHP-FIG 制定的扩展编码样式规范,在 PSR-1 的基础上定义了更详细的格式化规则。它涵盖了缩进、空白行、大括号位置、命名空间声明、类结构、控制结构等各方面的格式要求。本节将逐条讲解 PSR-12 的规则,提供大量正确与错误的代码示例对比。
基础概念
PSR-12 的历史
| 规范 | 状态 | 说明 |
|---|---|---|
| PSR-2 | 已废弃 | 旧版编码样式规范 |
| PSR-12 | 当前 | 取代 PSR-2,更新并扩展 |
| PER-2.0(草案) | 未来 | 基于 PSR-12 的进一步扩展 |
PSR-2 vs PSR-12
PSR-12 完全取代了 PSR-2。如果你在旧文档中看到 PSR-2,请参考 PSR-12。
详细说明
1. 概览规则
1.1 缩进
- 必须使用 4 个空格缩进,不得使用 Tab
php
<?php
// ✅ 正确:4 个空格
class User
{
public function getName(): string
{
return $this->name;
}
}
// ❌ 错误:使用 Tab
class User
{
public function getName(): string
{
return $this->name;
}
}
// ❌ 错误:2 个空格
class User
{
public function getName(): string
{
return $this->name;
}
}1.2 文件行长度
- 必须不超过 120 个字符
- 应该不超过 80 个字符
- 软限制:80 字符;硬限制:120 字符
php
<?php
// ✅ 推荐:80 字符以内
$result = $this->userRepository->findById($userId);
// ✅ 可接受:120 字符以内
$result = $this->getContainer()->get(UserService::class)->findUserWithRelations($userId, ['orders', 'profile']);
// ❌ 错误:超过 120 字符
$result = $this->getContainer()->get(very_long_service_name::class)->someVeryLongMethodName($veryLongParameterName);1.3 行尾
- 必须使用 Unix 风格换行符(LF,
\n) - 文件末尾必须有一个空行
- 文件末尾不得有尾随空白
2. 关键字与类型
2.1 PHP 关键字必须小写
php
<?php
// ✅ 正确
declare(strict_types=1);
namespace App\Models;
class User extends BaseModel implements
\JsonSerializable,
\ArrayAccess
{
public function __construct(private readonly int $id)
{
}
public function toArray(): array
{
return [];
}
}
// ❌ 错误
Namespace App\Models;
Class User Extends BaseModel Implements \JsonSerializable
{
Public Function toArray(): Array
{
}
}2.2 PHP 内置类型必须小写
php
<?php
declare(strict_types=1);
// ✅ 正确
function process(
int $id,
string $name,
float $price,
bool $active,
array $items,
?\stdClass $object,
callable $callback,
iterable $data,
mixed $value
): void {}
// ❌ 错误
function process(
Int $id,
String $name,
Float $price,
Bool $active,
Array $items
): void {}3. 命名空间与 use 声明
3.1 命名空间声明后必须有一个空行
3.2 use 声明块
- 每个
use声明必须独占一行 use声明块后必须有一个空行
php
<?php
declare(strict_types=1);
namespace Vendor\Package;
// ✅ 正确:每个 use 独占一行,分组排列
use Vendor\Package\SomeClass;
use Vendor\Package\AnotherClass;
use Vendor\Package\YetAnotherClass;
// PHP 内置类不使用 use
use ArrayObject;
use RuntimeException;
class ClassName
{
// ...
}3.3 use 声明的排序
php
<?php
declare(strict_types=1);
namespace App\Services;
// 1. PHP 内置类
use ArrayObject;
use RuntimeException;
use SplFixedArray;
// 2. 第三方库
use GuzzleHttp\Client;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
// 3. 当前项目的其他命名空间
use App\Models\User;
use App\Repositories\UserRepository;
class UserService
{
// ...
}4. 类、属性、方法
4.1 关键字顺序
类的 extends 和 implements 关键字必须与类名在同一行。
php
<?php
// ✅ 正确
class ClassName extends ParentClass implements
InterfaceA,
InterfaceB,
InterfaceC
{
}
// 接口也可以多行
class ClassName extends ParentClass implements InterfaceA, InterfaceB, InterfaceC
{
}
// ❌ 错误
class ClassName
extends ParentClass
implements InterfaceA,
InterfaceB
{
}4.2 大括号位置
- 类/方法/函数的左大括号必须独占新行
- 右大括号必须独占新行
php
<?php
// ✅ 正确:左大括号独占新行
class User
{
public function getName(): string
{
return $this->name;
}
}
// ❌ 错误:K&R 风格
class User {
public function getName(): string {
return $this->name;
}
}4.3 属性声明
php
<?php
declare(strict_types=1);
namespace App\Models;
class User
{
// ✅ 正确
public readonly string $name;
private int $age = 0;
protected ?string $email = null;
public static int $count = 0;
// ❌ 错误
public $name; // 缺少类型声明(推荐)
var $age = 0; // 不使用 var
private int $age=0; // 缺少空格
}PSR-12 与 PHP 8.1+ 特性
PSR-12 发布于 PHP 7.x 时代,但对于 PHP 8.1+ 的 readonly 属性、枚举、命名参数等新特性,社区约定:
readonly修饰符放在可见性修饰符之前或之后均可- 推荐顺序:
public readonly string $name
4.4 方法声明
php
<?php
declare(strict_types=1);
namespace App\Services;
class UserService
{
// ✅ 正确:参数之间有空格
public function createUser(
string $name,
string $email,
int $age = 0
): User {
return new User($name, $email, $age);
}
// 返回类型声明
private function validateEmail(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// 可变参数
public function processItems(string ...$items): array
{
return $items;
}
// ❌ 错误
public function createUser(string $name,string $email,int $age=0):User {
}
}5. 控制结构
5.1 通用规则
- 关键字后必须有一个空格
- 左大括号必须在同一行
- 右大括号必须独占新行
php
<?php
declare(strict_types=1);
// ✅ 正确
if ($condition) {
// ...
} elseif ($anotherCondition) {
// ...
} else {
// ...
}
// ✅ 正确:switch
switch ($status) {
case 'active':
$result = 1;
break;
case 'inactive':
$result = 0;
break;
default:
$result = -1;
break;
}
// ✅ 正确:for
for ($i = 0; $i < 10; $i++) {
// ...
}
// ✅ 正确:foreach
foreach ($items as $key => $value) {
// ...
}
// ✅ 正确:while
while ($condition) {
// ...
}
// ✅ 正确:do-while
do {
// ...
} while ($condition);
// ✅ 正确:try-catch-finally
try {
$result = riskyOperation();
} catch (SpecificException $e) {
// 处理特定异常
} catch (Throwable $e) {
// 处理所有异常
} finally {
// 清理
}5.2 match 表达式(PHP 8.0+)
php
<?php
declare(strict_types=1);
// ✅ match 表达式
$result = match ($status) {
'active', 'verified' => 1,
'inactive' => 0,
default => -1,
};5.3 匿名函数和箭头函数
php
<?php
declare(strict_types=1);
// ✅ 匿名函数
$callback = function (int $a, int $b): int {
return $a + $b;
};
// ✅ 箭头函数(PHP 7.4+)
$add = fn(int $a, int $b): int => $a + $b;
// ✅ 带类型声明的闭包
$users = array_filter($users, function (User $user): bool {
return $user->isActive();
});
// ✅ 使用箭头函数的简洁写法
$activeUsers = array_filter(
$users,
fn(User $user): bool => $user->isActive()
);实战示例
场景一:PSR-12 完全合规的完整文件
php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Request;
use App\Http\Response;
use App\Models\User;
use App\Services\UserService;
use Psr\Log\LoggerInterface;
class UserController
{
public const PAGE_SIZE = 20;
public function __construct(
private readonly UserService $userService,
private readonly LoggerInterface $logger
) {
}
public function index(Request $request): Response
{
$page = $request->query('page', 1);
$perPage = $request->query('per_page', self::PAGE_SIZE);
$users = $this->userService->paginate($page, $perPage);
return new Response([
'data' => $users->items(),
'total' => $users->total(),
'page' => $page,
'per_page' => $perPage,
]);
}
public function show(int $id): Response
{
$user = $this->userService->findById($id);
if ($user === null) {
return new Response(['error' => 'User not found'], 404);
}
return new Response(['data' => $user->toArray()]);
}
public function store(Request $request): Response
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
try {
$user = $this->userService->create($data);
return new Response(['data' => $user->toArray()], 201);
} catch (\RuntimeException $e) {
$this->logger->error('User creation failed', [
'error' => $e->getMessage(),
]);
return new Response(['error' => 'Creation failed'], 500);
}
}
public function destroy(int $id): Response
{
try {
$this->userService->delete($id);
return new Response(null, 204);
} catch (\RuntimeException $e) {
return new Response(['error' => 'Deletion failed'], 500);
}
}
}场景二:使用 PHP-CS-Fixer 自动格式化
bash
# 安装 PHP-CS-Fixer
composer require --dev friendsofphp/php-cs-fixer
# 创建配置文件
cat > .php-cs-fixer.dist.php << 'EOF'
<?php
$finder = PhpCsFixer\Finder::create()
->in(['src/', 'app/'])
->exclude(['vendor/', 'storage/', 'cache/']);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
])
->setFinder($finder)
->setUsingCache(false);
EOF
# 检查
vendor/bin/php-cs-fixer fix --dry-run --diff
# 修复
vendor/bin/php-cs-fixer fix注意事项
1. 团队统一
bash
# 在 composer.json 中添加格式化检查脚本
{
"scripts": {
"cs:check": "phpcs --standard=PSR12 --colors src/",
"cs:fix": "phpcbf --standard=PSR12 src/"
}
}2. 编辑器配置
ini
; .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false下一节
继续学习:PSR-4 自动加载 — 深入了解 PSR-4 自动加载规范,掌握命名空间与目录映射的规则。