EasyТеория5 min

Стиль кода

PSR-1 базовые правила, PSR-12 расширенный стиль, PER Coding Style 2.0, PHP CS Fixer

Стиль кода: PSR-1, PSR-12, PER CS

PSR-1: Basic Coding Standard

PSR-1 определяет минимальные правила, которым должен соответствовать любой PHP-код:

Основные требования

  1. Теги: только <?php и <?= (короткие теги <? запрещены)
  2. Кодировка: файлы в UTF-8 без BOM
  3. Один файл: либо объявляет символы (классы, функции, константы), либо выполняет побочные эффекты (вывод, изменение настроек), но не оба сразу
  4. Namespace: обязательно для классов
  5. Имена классов: StudlyCaps (PascalCase)
  6. Константы: UPPER_CASE с подчёркиваниями
  7. Методы: camelCase
<?php
// ❌ BAD: side effects AND declarations in same file
declare(strict_types=1);

ini_set('display_errors', '0');        // Side effect
echo 'Initializing...';                // Side effect

class Config                            // Declaration
{
    public const MAX_RETRIES = 3;
}
<?php
// ✅ GOOD: only declarations
declare(strict_types=1);

namespace App\Config;

final class AppConfig
{
    public const MAX_RETRIES = 3;
    public const DEFAULT_TIMEOUT = 30;

    public function getRetryDelay(): int
    {
        return 1000;
    }
}

Именование по PSR-1

<?php
declare(strict_types=1);

namespace App\Service;

// Class: StudlyCaps (PascalCase)
final class OrderProcessor
{
    // Constants: UPPER_CASE
    public const MAX_ITEMS = 100;
    public const DEFAULT_CURRENCY = 'USD';

    // Methods: camelCase
    public function processOrder(int $orderId): void {}

    public function calculateTotal(): int
    {
        return 0;
    }

    // Properties: no strict rule in PSR-1
    // PSR-12 recommends camelCase
    private int $itemCount = 0;
    private string $lastError = '';
}

PSR-12: Extended Coding Style

PSR-12 расширяет PSR-1 детальными правилами форматирования. Заменил устаревший PSR-2.

Файлы

<?php
// 1. Opening <?php tag on first line
// 2. declare(strict_types=1) on next line (blank line after)
// 3. Namespace declaration (blank line after)
// 4. Use imports grouped and sorted
// 5. Blank line before class body

declare(strict_types=1);

namespace App\Controller;

use App\DTO\CreateUserDTO;
use App\Service\UserService;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class UserController
{
    // Class body
}
// Blank line at end of file, single newline

Фигурные скобки

<?php
declare(strict_types=1);

namespace App\Example;

// Classes/interfaces/traits: opening brace on NEXT line
final class PaymentService
{                                          // Next line
    // Methods: opening brace on NEXT line
    public function charge(int $amount): bool
    {                                      // Next line
        // Control structures: opening brace on SAME line
        if ($amount <= 0) {                // Same line
            return false;
        }

        for ($i = 0; $i < 3; $i++) {       // Same line
            // retry logic
        }

        while ($this->isProcessing()) {    // Same line
            usleep(100_000);
        }

        switch ($amount) {                 // Same line
            case 0:
                return false;
            default:
                return true;
        }

        return true;
    }
}

Видимость и модификаторы

<?php
declare(strict_types=1);

namespace App\Example;

// Visibility MUST be declared on all properties and methods
abstract class BaseService
{
    // Order: abstract/final, then visibility, then static/readonly
    abstract protected function validate(): bool;

    final public static function create(): static
    {
        return new static();
    }

    // Property order: visibility, then static/readonly, then type
    protected readonly string $name;
    private static int $instanceCount = 0;
}

Пробелы и отступы

<?php
declare(strict_types=1);

namespace App\Example;

final class FormattingRules
{
    // 4 spaces indentation (NO tabs)
    // No trailing whitespace
    // Max line length: soft limit 120 chars

    public function methodArguments(
        string $firstName,        // One argument per line if multiline
        string $lastName,
        int $age,
    ): string {                   // Closing paren + opening brace on same line
        return "{$firstName} {$lastName}, age {$age}";
    }

    public function controlStructures(): void
    {
        // Space after keywords, no space after function name
        if ($a === $b) {         // Space after 'if', space before '{'
            // ...
        } elseif ($a > $b) {     // 'elseif' not 'else if'
            // ...
        } else {
            // ...
        }

        // Ternary and null coalesce
        $value = $condition ? 'yes' : 'no';
        $name = $input ?? 'default';

        // Closures
        $closure = function (int $x, int $y) use ($multiplier): int {
            return ($x + $y) * $multiplier;
        };

        // Arrow functions
        $double = fn(int $n): int => $n * 2;
    }

    // Return type after colon with space
    public function getItems(): array
    {
        return [];
    }

    // Nullable types
    public function findUser(int $id): ?User
    {
        return null;
    }

    // Union types (PHP 8.0+)
    public function parse(string|int $value): string
    {
        return (string) $value;
    }
}

Группировка use-импортов

<?php
declare(strict_types=1);

namespace App\Service;

// Group 1: Classes/interfaces
use App\Entity\Order;
use App\Repository\OrderRepository;
use Psr\Log\LoggerInterface;

// Group 2: Functions (blank line between groups)
use function array_map;
use function sprintf;

// Group 3: Constants
use const PHP_INT_MAX;
use const PHP_EOL;

PER Coding Style 2.0

PER CS (PHP Evolving Recommendation) -- наследник PSR-12, разработанный для поддержки новых возможностей PHP 8.x. Это "живой" стандарт, который обновляется вместе с языком.

Новые правила для PHP 8.x

<?php
declare(strict_types=1);

namespace App\Example;

// Enums: same brace rules as classes
enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';

    // Methods in enums follow same rules
    public function label(): string
    {
        return match ($this) {
            self::Active => 'Active',
            self::Inactive => 'Inactive',
        };
    }
}

// Readonly classes (PHP 8.2+)
final readonly class Money
{
    // Order: final, readonly, class
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
}

// Intersection types (PHP 8.1+)
function process(Countable&Iterator $collection): void
{
    // ...
}

// DNF types (PHP 8.2+)
function handle((Countable&Iterator)|null $items): void
{
    // ...
}

// Named arguments in calls
$user = new User(
    name: 'John',
    email: '[email protected]',
    role: Role::Admin,
);

// Match expression formatting
$result = match (true) {
    $value < 0 => 'negative',
    $value === 0 => 'zero',
    $value > 0 => 'positive',
};

// First-class callables (PHP 8.1+)
$callback = strlen(...);
$mapper = $this->transform(...);

Trailing comma

PER CS рекомендует trailing comma в многострочных конструкциях:

<?php
declare(strict_types=1);

// Function declarations
function createUser(
    string $name,
    string $email,
    int $age,      // Trailing comma
): User {
    // ...
}

// Function calls
$result = createUser(
    name: 'John',
    email: '[email protected]',
    age: 30,       // Trailing comma
);

// Arrays (always)
$config = [
    'debug' => true,
    'cache' => false,
    'timeout' => 30,   // Trailing comma
];

// Closure use
$fn = function () use (
    $serviceA,
    $serviceB,
    $logger,       // Trailing comma
): void {
    // ...
};

PHP CS Fixer

PHP CS Fixer -- основной инструмент для автоматического форматирования кода. Настройка для PSR-12 и PER CS:

<?php
// .php-cs-fixer.dist.php

declare(strict_types=1);

$finder = (new PhpCsFixer\Finder())
    ->in([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->exclude([
        'var',
        'vendor',
    ]);

return (new PhpCsFixer\Config())
    ->setRules([
        // PER CS 2.0 ruleset (includes PSR-12)
        '@PER-CS2.0' => true,

        // Strict types
        'declare_strict_types' => true,

        // Modern PHP
        'final_class' => true,
        'self_static_accessor' => true,
        'global_namespace_import' => [
            'import_classes' => true,
            'import_functions' => false,
            'import_constants' => false,
        ],

        // Clean code
        'no_unused_imports' => true,
        'ordered_imports' => ['sort_algorithm' => 'alpha'],
        'trailing_comma_in_multiline' => [
            'elements' => ['arguments', 'arrays', 'parameters'],
        ],
        'single_quote' => true,
        'no_empty_statement' => true,
        'no_extra_blank_lines' => true,
    ])
    ->setFinder($finder)
    ->setRiskyAllowed(true);

Запуск PHP CS Fixer

# Check without fixing
php-cs-fixer fix --dry-run --diff

# Fix all files
php-cs-fixer fix

# Fix specific file
php-cs-fixer fix src/Service/OrderService.php

# Check with verbose output
php-cs-fixer fix --dry-run --diff --verbose

phpcs vs php-cs-fixer

Аспект PHP_CodeSniffer (phpcs) PHP CS Fixer
Подход Находит нарушения Находит и исправляет
Исправление phpcbf (отдельная утилита) Встроено (fix)
Конфигурация XML (phpcs.xml) PHP (.php-cs-fixer.dist.php)
Правила Sniffs Fixers
Популярность Старше, больше sniffs Чаще в Symfony-проектах
Скорость Быстрее проверка Быстрее исправление

phpcs конфигурация

<?xml version="1.0"?>
<!-- phpcs.xml.dist -->
<ruleset name="Project">
    <description>Project coding standard</description>

    <file>src/</file>
    <file>tests/</file>

    <exclude-pattern>vendor/</exclude-pattern>

    <rule ref="PSR12"/>

    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="120"/>
            <property name="absoluteLineLimit" value="150"/>
        </properties>
    </rule>
</ruleset>
# Check code style
phpcs

# Fix automatically (what it can)
phpcbf

Рекомендация: В современных PHP-проектах (особенно с Symfony) используйте PHP CS Fixer с набором правил @PER-CS2.0. Это актуальный стандарт, поддерживающий все возможности PHP 8.4. Добавьте проверку в CI/CD pipeline.


Проверь себя

Какой размер отступа требует PSR-12?

Что запрещает PSR-1 в одном файле?

Чем php-cs-fixer отличается от phpcs?

Какой стандарт пришёл на смену PSR-12?

Где по PSR-12 ставится открывающая фигурная скобка для класса?