Что такое PSR и зачем он нужен

PSR — это PHP Standard Recommendation, то есть рекомендация PHP-FIG для совместимости PHP-проектов. Это не часть синтаксиса языка и не «закон PHP». Код без PSR может выполняться нормально. Смысл другой: когда проект, библиотека, фреймворк и команда используют общий стиль, читать diff, ревьюить merge request и подключать пакеты из Composer становится проще.

PHP-FIG возник именно вокруг interop — совместимости между фреймворками и библиотеками. Поэтому рядом с PSR-1 и PSR-12 в списке стандартов есть Autoloading и PSR-4, PSR-7 для HTTP messages и другие соглашения. В этой статье важны два стандарта: PSR-1 как базовый минимум и PSR-12 как современный coding style для обычного PHP-кода.

flowchart TD A[PHP-FIG] --> B[PSR] B --> C[PSR-1: базовый coding standard] B --> D[PSR-12: extended coding style] C --> E[Имена, теги, UTF-8, side effects] D --> F[Формат файла, отступы, скобки, visibility] F --> G[PHP-CS-Fixer / PHP_CodeSniffer] G --> H[CI и ревью без споров о пробелах]
flowchart TD
    A[PHP-FIG] --> B[PSR]
    B --> C[PSR-1: базовый coding standard]
    B --> D[PSR-12: extended coding style]
    C --> E[Имена, теги, UTF-8, side effects]
    D --> F[Формат файла, отступы, скобки, visibility]
    F --> G[PHP-CS-Fixer / PHP_CodeSniffer]
    G --> H[CI и ревью без споров о пробелах]
PSR-1 задаёт минимальные правила совместимости, PSR-12 расширяет их до практического стиля, а инструменты переносят эти правила в CI.

PSR-1: базовый минимум

PSR-1 отвечает на вопрос: «Каким должен быть PHP-файл, чтобы его можно было без сюрпризов подключать в чужом проекте?» Он задаёт небольшое ядро правил.

Файлы с PHP-кодом используют только <?php и <?=, а не старые варианты коротких тегов. Код должен быть в UTF-8 без BOM. Файл должен либо объявлять символы — классы, функции, константы, — либо выполнять side effects, но не смешивать оба поведения без необходимости.

Плохой пример: файл одновременно меняет настройки, подключает другой файл, печатает HTML и объявляет функцию.

<?php

ini_set('display_errors', '1');
require __DIR__ . '/bootstrap.php';
echo '<h1>Report</h1>';

function formatMoney(int $cents): string
{
    return number_format($cents / 100, 2);
}

Для автозагрузки и поддержки Composer такой файл неудобен: простое подключение функции уже запускает вывод и меняет окружение. В стиле PSR-1 декларации живут отдельно от исполняемого сценария.

<?php

namespace App\Support;

function formatMoney(int $cents): string
{
    return number_format($cents / 100, 2);
}

А side effects остаются в entrypoint-файле, bootstrap-файле или CLI-скрипте. Это хорошо стыкуется с Синтаксис, теги и подключение файлов и Неймспейсы и use: подключение файла становится предсказуемым.

PSR-1 также фиксирует имена: классы пишутся в StudlyCaps, что PSR-12 уточняет как PascalCase; константы класса — в UPPER_CASE_WITH_UNDERSCORES; методы — в camelCase. Для свойств PSR-1 намеренно не выбирает единственный стиль, но требует последовательности в разумной области: vendor, package, class или method.

<?php

namespace App\Billing;

final class InvoiceCalculator
{
    public const DEFAULT_TAX_RATE = 0.2;

    public function calculateTotal(int $subtotal): int
    {
        return (int) round($subtotal * (1 + self::DEFAULT_TAX_RATE));
    }
}

PSR-12: современный формат файла

PSR-12 расширяет PSR-1 и заменяет устаревший PSR-2. Его задача — убрать мелкие стилистические споры: где ставить скобку, сколько пробелов в отступе, как упорядочить declare, namespace и use.

Базовая структура PHP-файла по PSR-12 выглядит так:

<?php

declare(strict_types=1);

namespace App\Billing;

use App\User\UserId;
use DateTimeImmutable;
use Psr\Log\LoggerInterface;

final class InvoiceService
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function issueInvoice(UserId $userId, DateTimeImmutable $issuedAt): void
    {
        $this->logger->info('Invoice issued', [
            'userId' => (string) $userId,
            'issuedAt' => $issuedAt->format(DATE_ATOM),
        ]);
    }
}

Обрати внимание на порядок: <?php, затем declare(strict_types=1), затем namespace, затем use, затем сам код. Это напрямую связано с Типы и strict_types: declare(strict_types=1) должен быть в начале файла, иначе строгий режим будет оформлен неверно.

Отступ — 4 пробела, не табы. Закрывающий ?> в файлах, содержащих только PHP, опускается: так меньше риска случайно отправить пробел или перевод строки в output до вызова header(), что важно для HTTP-заголовки, ответы и редиректы.

PSR-12 не устанавливает жёсткий максимум длины строки, но задаёт soft limit 120 символов и рекомендует держаться ближе к 80, когда это не ухудшает читаемость. Это не повод ломать каждое выражение механически. Длинная сигнатура метода обычно читается лучше в многострочном виде:

public function createSubscription(
    UserId $userId,
    PlanId $planId,
    DateTimeImmutable $startsAt,
    ?CouponCode $couponCode = null,
): Subscription {
    // ...
}

Скобки, visibility и управляющие конструкции

У классов открывающая фигурная скобка стоит на отдельной строке. У методов — тоже. У if, foreach, while, switch скобка остаётся на той же строке, что и условие.

final class AccessPolicy
{
    public function canEdit(User $user, Document $document): bool
    {
        if ($user->isAdmin()) {
            return true;
        }

        return $document->ownerId()->equals($user->id());
    }
}

Тела управляющих конструкций всегда оборачиваются в фигурные скобки. Даже если внутри одна строка. Это снижает риск ошибки при последующем добавлении строки.

// Плохо: легко ошибиться при расширении условия.
if ($user->isBlocked())
    return false;

// Нормально.
if ($user->isBlocked()) {
    return false;
}

Visibility обязателен для методов и свойств. Для констант класса — тоже, если минимальная версия PHP проекта поддерживает видимость констант. Старый var для свойств не используется.

final class TokenBucket
{
    private int $tokens = 0;

    public function refill(int $amount): void
    {
        $this->tokens += $amount;
    }
}

Префиксы _privateMethod() и _protectedProperty не считаются visibility. В современном PHP это легаси-сигнал, а не механизм языка. Механизм — private, protected, public. Это пересекается с Классы, объекты и видимость.

Где стиль заканчивается

PSR-12 хорошо автоматизируется: PHP-CS-Fixer, PHP_CodeSniffer и IDE formatter могут расставить пробелы, скобки, blank lines, порядок импортов. Такие проверки обычно живут в CI, линтеры и автоматические проверки, рядом с composer validate, composer audit, статическим анализом и PHPUnit.

Но форматтер не решает архитектурные вопросы. Он не скажет, что класс слишком много знает о базе, HTTP и шаблонах одновременно. Не выберет хорошие имена доменных объектов. Не поймёт, что mixed в PHPDoc скрывает реальную модель данных. Для этого нужны ревью, PHPDoc, generics и статический анализ, тесты и нормальная проектная дисциплина.

Практичная граница такая: форматирование должно быть скучным и автоматическим. После composer cs-fix или vendor/bin/phpcs команда не спорит о пробелах. А вот имена, ответственность классов, публичные API, исключения и совместимость при Миграции между версиями PHP остаются инженерными решениями.

См. также

Источники

  1. PHP-FIG: PHP Standard Recommendations
  2. PHP-FIG: PSR-1 Basic Coding Standard
  3. PHP-FIG: PSR-12 Extended Coding Style Guide
  4. PHP Manual: Пространства имён