Что такое 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 и ревью без споров о пробелах]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 остаются инженерными решениями.
См. также
- Неймспейсы и use — порядок
namespace, imports и полные имена классов. - Autoloading и PSR-4 — как стиль файлов связан с Composer autoload.
- Типы и strict_types — почему
declare(strict_types=1)стоит в начале файла. - Классы, объекты и видимость —
public,protected,private, свойства и методы. - CI, линтеры и автоматические проверки — где запускать форматтеры и style checks.
- PHPDoc, generics и статический анализ — что остаётся за пределами автоформатирования.