Skip to content

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 关键字顺序

类的 extendsimplements 关键字必须与类名在同一行。

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 自动加载规范,掌握命名空间与目录映射的规则。

参考链接