MidТеория10 min

Инструменты качества кода

PHP CS Fixer, PHP_CodeSniffer, Psalm, PHPMD, Rector, CI/CD интеграция

Инструменты качества кода в PHP

Обзор экосистемы

Качество PHP-кода обеспечивается несколькими категориями инструментов:

Категория Инструменты Что проверяют
Форматирование PHP CS Fixer, PHP_CodeSniffer Стиль кода (PSR-12, Symfony)
Статический анализ PHPStan, Psalm Типы, логические ошибки
Метрики PHPMD, PHPMetrics Сложность, связанность
Дублирование PHPCPD Copy-paste
Рефакторинг Rector Автоматические изменения
Pre-commit GrumPHP, Captain Hook Автоматизация перед коммитом

PHP CS Fixer

PHP CS Fixer — инструмент автоматического форматирования кода. Он не только проверяет, но и исправляет стиль кода.

Установка

composer require --dev friendsofphp/php-cs-fixer

Конфигурация .php-cs-fixer.dist.php

<?php

declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
    ->in([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->exclude([
        'var',
        'vendor',
        'node_modules',
    ])
    ->name('*.php')
    ->notName('*.blade.php');

return (new PhpCsFixer\Config())
    ->setRiskyAllowed(true)
    ->setRules([
        // Presets
        '@PSR12' => true,
        '@Symfony' => true,
        '@Symfony:risky' => true,
        '@PHP84Migration' => true,

        // Strict types
        'declare_strict_types' => true,

        // Arrays
        'array_syntax' => ['syntax' => 'short'],
        'no_trailing_comma_in_singleline' => true,
        'trailing_comma_in_multiline' => [
            'elements' => ['arrays', 'arguments', 'parameters', 'match'],
        ],

        // Imports
        'global_namespace_import' => [
            'import_classes' => true,
            'import_constants' => false,
            'import_functions' => false,
        ],
        'ordered_imports' => [
            'sort_algorithm' => 'alpha',
            'imports_order' => ['class', 'function', 'const'],
        ],
        'no_unused_imports' => true,

        // Classes
        'class_attributes_separation' => [
            'elements' => [
                'const' => 'one',
                'method' => 'one',
                'property' => 'one',
            ],
        ],
        'final_class' => true,
        'self_accessor' => true,

        // PHPDoc
        'phpdoc_align' => ['align' => 'left'],
        'phpdoc_order' => true,
        'phpdoc_separation' => true,
        'phpdoc_trim' => true,
        'no_superfluous_phpdoc_tags' => [
            'allow_mixed' => true,
            'remove_inheritdoc' => true,
        ],

        // Spaces
        'concat_space' => ['spacing' => 'one'],
        'binary_operator_spaces' => [
            'default' => 'single_space',
        ],

        // Functions
        'native_function_invocation' => [
            'include' => ['@compiler_optimized'],
            'scope' => 'namespaced',
            'strict' => true,
        ],

        // Misc
        'yoda_style' => false,
        'not_operator_with_successor_space' => false,
        'single_line_throw' => false,
        'nullable_type_declaration_for_default_null_value' => true,
    ])
    ->setFinder($finder)
    ->setCacheFile(__DIR__ . '/var/.php-cs-fixer.cache');

Использование

# Fix all files
vendor/bin/php-cs-fixer fix

# Dry run (show what would be changed)
vendor/bin/php-cs-fixer fix --dry-run

# Dry run with diff (show exact changes)
vendor/bin/php-cs-fixer fix --dry-run --diff

# Fix specific file
vendor/bin/php-cs-fixer fix src/Service/UserService.php

# Fix with verbose output
vendor/bin/php-cs-fixer fix -v

# Show available rules
vendor/bin/php-cs-fixer describe @PSR12
vendor/bin/php-cs-fixer describe declare_strict_types

Пример: до и после

// BEFORE PHP CS Fixer
<?php
namespace App\Service;
use App\Entity\User;
use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;

class userService {
    private $repo;
    private $logger;
    public function __construct(UserRepository $repo , LoggerInterface $logger) {
        $this->repo=$repo;
        $this->logger = $logger;
    }
    public function findUser(int $id) {
        $user = $this->repo->find( $id );
        if($user == null){
            return NULL;
        }
        return $user;
    }
}
// AFTER PHP CS Fixer
<?php

declare(strict_types=1);

namespace App\Service;

use App\Repository\UserRepository;
use Psr\Log\LoggerInterface;

final class UserService
{
    public function __construct(
        private UserRepository $repo,
        private LoggerInterface $logger,
    ) {}

    public function findUser(int $id): ?User
    {
        $user = $this->repo->find($id);
        if ($user === null) {
            return null;
        }

        return $user;
    }
}

PHP_CodeSniffer

PHP_CodeSniffer (phpcs) — инструмент проверки стиля кода. В отличие от PHP CS Fixer, он ориентирован на стандарты (PSR-12, PSR-1) и имеет отдельный фиксер (phpcbf).

Установка

composer require --dev squizlabs/php_codesniffer

Конфигурация phpcs.xml

<?xml version="1.0"?>
<ruleset name="Project">
    <description>Project coding standard</description>

    <!-- Paths to check -->
    <file>src</file>
    <file>tests</file>

    <!-- Exclude patterns -->
    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>*/var/*</exclude-pattern>
    <exclude-pattern>*/node_modules/*</exclude-pattern>

    <!-- Arguments -->
    <arg name="colors"/>
    <arg value="sp"/>  <!-- s=show sniff codes, p=show progress -->

    <!-- Use PSR-12 standard -->
    <rule ref="PSR12"/>

    <!-- Custom rules -->
    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="120"/>
            <property name="absoluteLineLimit" value="150"/>
        </properties>
    </rule>

    <rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
        <properties>
            <property name="spacesCountAroundEqualsSign" value="0"/>
        </properties>
    </rule>
</ruleset>

Использование

# Check for violations
vendor/bin/phpcs

# Check specific file
vendor/bin/phpcs src/Service/UserService.php

# Auto-fix violations
vendor/bin/phpcbf

# Show installed standards
vendor/bin/phpcs -i

# Use specific standard
vendor/bin/phpcs --standard=PSR12 src/

PHP CS Fixer vs PHP_CodeSniffer

Функция PHP CS Fixer PHP_CodeSniffer
Конфигурация PHP-файл (.php-cs-fixer.dist.php) XML (phpcs.xml)
Автоисправление Встроено (fix) Отдельный инструмент (phpcbf)
Правила 200+ встроенных Стандарты + Slevomat
Preset'ы @PSR12, @Symfony, @PHP84Migration PSR1, PSR2, PSR12
Кастомизация Гибкая через PHP Через XML
Скорость Быстрый Быстрый
Популярность Symfony-экосистема Laravel-экосистема

Рекомендация: используйте PHP CS Fixer — он мощнее и гибче. Не используйте оба одновременно.

Psalm

Psalm — альтернативный статический анализатор от Vimeo. Его уникальная фича — taint analysis (анализ потоков данных для безопасности).

Установка

composer require --dev vimeo/psalm

# Initialize configuration
vendor/bin/psalm --init

Конфигурация psalm.xml

<?xml version="1.0"?>
<psalm
    errorLevel="1"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
    findUnusedBaselineEntry="true"
    findUnusedCode="true"
>
    <projectFiles>
        <directory name="src"/>
        <ignoreFiles>
            <directory name="vendor"/>
        </ignoreFiles>
    </projectFiles>
</psalm>

Уровни Psalm

Psalm имеет 8 уровней (1 — строжайший, 8 — мягчайший):

Level Описание
1 Максимально строгий (все ошибки)
2 Немного мягче
3 Подходит для большинства проектов
4 Средний уровень
5-8 Для legacy-проектов

Taint Analysis — уникальная фича Psalm

Taint analysis отслеживает потоки данных от пользовательского ввода до опасных операций (SQL, HTML, shell):

<?php

declare(strict_types=1);

// Psalm taint analysis detects:

// SQL Injection
function getUser(string $id): void
{
    // TAINT ERROR: $id flows from user input to SQL query
    $query = "SELECT * FROM users WHERE id = " . $id;
    // Fix: use prepared statements
}

// XSS
function renderName(string $name): string
{
    // TAINT ERROR: $name flows from user input to HTML output
    return "<h1>" . $name . "</h1>";
    // Fix: htmlspecialchars($name, ENT_QUOTES, 'UTF-8')
}

// Command Injection
function runCommand(string $input): void
{
    // TAINT ERROR: $input flows to shell execution
    exec("ls " . $input);
    // Fix: escapeshellarg($input)
}
# Run taint analysis
vendor/bin/psalm --taint-analysis

Psalm-specific annotations

<?php

declare(strict_types=1);

/**
 * @psalm-immutable     — class is immutable
 * @psalm-pure          — function has no side effects
 * @psalm-readonly      — property cannot be changed after construction
 * @psalm-suppress      — suppress specific errors
 * @psalm-assert        — assert types in function
 * @psalm-type          — define custom types
 */

/** @psalm-immutable */
final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    /** @psalm-pure */
    public function add(Money $other): self
    {
        assert($this->currency === $other->currency);

        return new self($this->amount + $other->amount, $this->currency);
    }
}

/**
 * @psalm-assert User $value
 * @throws \InvalidArgumentException
 */
function assertUser(mixed $value): void
{
    if (!$value instanceof User) {
        throw new \InvalidArgumentException('Expected User');
    }
    // After this function, Psalm knows $value is User
}

PHPStan vs Psalm

Функция PHPStan Psalm
Уровни 0-10 (10 строже) 1-8 (1 строже)
Taint analysis Нет Да
Расширения Богатая экосистема Меньше
Скорость Быстрее Медленнее
Generics Через PHPDoc Через PHPDoc
Community Очень активное Активное
IDE интеграция PHPStorm, VS Code PHPStorm, VS Code
Baseline Да Да

Рекомендация: Для большинства проектов — PHPStan. Для проектов с особыми требованиями к безопасности — дополнительно Psalm с taint analysis.

PHP Mess Detector (PHPMD)

PHPMD анализирует код на сложность, именование, неиспользуемый код.

Установка

composer require --dev phpmd/phpmd

Конфигурация phpmd.xml

<?xml version="1.0"?>
<ruleset name="Project"
         xmlns="http://pmd.sf.net/ruleset/1.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd">

    <description>Project rules</description>

    <!-- Clean Code Rules -->
    <rule ref="rulesets/cleancode.xml">
        <!-- Allow static access for factories -->
        <exclude name="StaticAccess"/>
    </rule>

    <!-- Code Size Rules -->
    <rule ref="rulesets/codesize.xml">
        <exclude name="TooManyPublicMethods"/>
    </rule>

    <!-- Cyclomatic Complexity -->
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity">
        <properties>
            <property name="reportLevel" value="10"/>
        </properties>
    </rule>

    <!-- Method Length -->
    <rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
        <properties>
            <property name="minimum" value="30"/>
        </properties>
    </rule>

    <!-- Class Length -->
    <rule ref="rulesets/codesize.xml/ExcessiveClassLength">
        <properties>
            <property name="minimum" value="300"/>
        </properties>
    </rule>

    <!-- Naming Rules -->
    <rule ref="rulesets/naming.xml">
        <exclude name="ShortVariable"/>
    </rule>
    <rule ref="rulesets/naming.xml/ShortVariable">
        <properties>
            <property name="minimum" value="2"/>
            <property name="exceptions" value="id,i,j,k,e,x,y"/>
        </properties>
    </rule>

    <!-- Unused Code -->
    <rule ref="rulesets/unusedcode.xml"/>
</ruleset>

Использование

# Run with configuration file
vendor/bin/phpmd src/ text phpmd.xml

# Quick check with built-in rulesets
vendor/bin/phpmd src/ text cleancode,codesize,naming,unusedcode

# Output in different formats
vendor/bin/phpmd src/ xml phpmd.xml     # XML format
vendor/bin/phpmd src/ json phpmd.xml    # JSON format
vendor/bin/phpmd src/ html phpmd.xml    # HTML report

Что находит PHPMD

<?php

declare(strict_types=1);

// PHPMD: CyclomaticComplexity — too many branches
function processOrder(Order $order): string
{
    if ($order->isPaid()) {
        if ($order->hasShipping()) {
            if ($order->isInternational()) {
                // ... nested conditions increase complexity
            }
        }
    }
    // Fix: extract methods, use early returns, strategy pattern
}

// PHPMD: ExcessiveParameterList — too many parameters
function createUser(
    string $name,
    string $email,
    string $phone,
    string $address,
    string $city,
    string $country,
    string $zip,
    int $age,
): User {
    // Fix: use a DTO or Value Object
}

// PHPMD: UnusedLocalVariable
function calculate(int $a, int $b): int
{
    $unused = $a * 2;  // This variable is never used
    return $a + $b;
}

PHP Copy/Paste Detector (PHPCPD)

PHPCPD находит дублированный код (copy-paste).

Установка и использование

composer require --dev sebastian/phpcpd

# Run
vendor/bin/phpcpd src/

# With minimum lines and tokens
vendor/bin/phpcpd --min-lines=5 --min-tokens=70 src/

# Exclude directories
vendor/bin/phpcpd --exclude=vendor --exclude=var src/

Пример вывода

Found 3 clones with 45 duplicated lines in 2 files:

  - src/Service/UserService.php:45-67 (22 lines)
    src/Service/AdminService.php:30-52

  - src/Controller/ApiController.php:100-115 (15 lines)
    src/Controller/WebController.php:80-95

Rector — автоматический рефакторинг

Rector автоматически модернизирует PHP-код: обновляет синтаксис, применяет паттерны, мигрирует между версиями PHP.

Установка

composer require --dev rector/rector

Конфигурация rector.php

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
use Rector\Set\ValueObject\LevelSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->withSkip([
        __DIR__ . '/src/Migrations',
        __DIR__ . '/src/DataFixtures',
    ])
    ->withSets([
        // PHP version upgrades
        LevelSetList::UP_TO_PHP_84,

        // Code quality
        SetList::CODE_QUALITY,
        SetList::DEAD_CODE,
        SetList::EARLY_RETURN,
        SetList::TYPE_DECLARATION,

        // Framework-specific
        SymfonySetList::SYMFONY_72,
        PHPUnitSetList::PHPUNIT_110,
    ])
    ->withRules([
        ExplicitBoolCompareRector::class,
        RemoveUnusedPrivateMethodRector::class,
    ]);

Использование

# Preview changes (dry run)
vendor/bin/rector process --dry-run

# Apply changes
vendor/bin/rector process

# Process specific file
vendor/bin/rector process src/Service/UserService.php

# Process with specific rule
vendor/bin/rector process --only=Rector\\DeadCode\\Rector\\ClassMethod\\RemoveUnusedPrivateMethodRector

Примеры трансформаций Rector

// BEFORE: PHP 7.4 code
class UserService
{
    /** @var UserRepository */
    private $repository;

    /** @var LoggerInterface */
    private $logger;

    public function __construct(UserRepository $repository, LoggerInterface $logger)
    {
        $this->repository = $repository;
        $this->logger = $logger;
    }

    /**
     * @return User|null
     */
    public function find(int $id)
    {
        $user = $this->repository->find($id);
        if ($user !== null) {
            return $user;
        }
        return null;
    }
}
// AFTER: Rector applied PHP 8.4 rules
final class UserService
{
    public function __construct(
        private readonly UserRepository $repository,
        private readonly LoggerInterface $logger,
    ) {}

    public function find(int $id): ?User
    {
        return $this->repository->find($id);
    }
}

Rector автоматически:

  • Добавил final
  • Применил constructor property promotion
  • Добавил readonly
  • Добавил return type ?User
  • Упростил метод (убрал лишний if)

CI/CD интеграция

GitHub Actions

name: Code Quality

on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.4']

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, pdo_pgsql
          coverage: xdebug

      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      # Step 1: Code Style
      - name: Check code style
        run: vendor/bin/php-cs-fixer fix --dry-run --diff

      # Step 2: Static Analysis
      - name: PHPStan
        run: vendor/bin/phpstan analyse --error-format=github

      # Step 3: Tests
      - name: PHPUnit
        run: vendor/bin/phpunit --coverage-clover coverage.xml

      # Step 4: Security
      - name: Security audit
        run: composer audit

      # Step 5: Check for code duplication
      - name: PHPCPD
        run: vendor/bin/phpcpd src/ || true  # Non-blocking

GitLab CI

stages:
  - quality
  - test

code-style:
  stage: quality
  script:
    - composer install --no-interaction
    - vendor/bin/php-cs-fixer fix --dry-run --diff

static-analysis:
  stage: quality
  script:
    - composer install --no-interaction
    - vendor/bin/phpstan analyse

tests:
  stage: test
  script:
    - composer install --no-interaction
    - vendor/bin/phpunit --coverage-text --min=80
  coverage: '/Lines:\s*(\d+\.\d+)%/'

Pre-commit hooks: GrumPHP

GrumPHP автоматически запускает проверки перед каждым коммитом.

Установка

composer require --dev phpro/grumphp

Конфигурация grumphp.yml

grumphp:
    tasks:
        phpcsfixer:
            config: .php-cs-fixer.dist.php
            triggered_by: ['php']

        phpstan:
            configuration: phpstan.neon
            level: 9
            triggered_by: ['php']

        phpunit:
            config_file: phpunit.xml
            always_execute: true

        composer:
            no_check_lock: false
            no_check_publish: true

        composer_audit:
            locked: true

    # Don't block commit on warnings (only errors)
    ignore_unstaged_changes: true
    process_timeout: 300

При каждом git commit GrumPHP автоматически запустит CS Fixer, PHPStan и PHPUnit. Если какая-либо проверка не пройдёт, коммит будет заблокирован.

Сравнительная таблица всех инструментов

Инструмент Тип Что делает Автофикс CI/CD
PHP CS Fixer Форматирование Стиль кода Да Обязательно
PHP_CodeSniffer Форматирование Стиль кода Частичный (phpcbf) Опционально
PHPStan Статический анализ Типы, ошибки Нет Обязательно
Psalm Статический анализ Типы, taint analysis Нет (Psalter частично) Опционально
PHPMD Метрики Сложность, naming Нет Рекомендуется
PHPCPD Дублирование Copy-paste Нет Рекомендуется
Rector Рефакторинг Модернизация кода Да При обновлениях
GrumPHP Pre-commit Автоматизация — Локально

Рекомендуемый workflow

Минимальный набор (для каждого проекта)

{
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.68",
        "phpstan/phpstan": "^2.1",
        "phpunit/phpunit": "^11.5"
    },
    "scripts": {
        "cs-fix": "php-cs-fixer fix",
        "cs-check": "php-cs-fixer fix --dry-run --diff",
        "analyse": "phpstan analyse",
        "test": "phpunit",
        "check": [
            "@cs-check",
            "@analyse",
            "@test"
        ]
    }
}

Полный набор (для enterprise)

{
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.68",
        "phpstan/phpstan": "^2.1",
        "phpstan/phpstan-strict-rules": "^2.0",
        "phpstan/phpstan-symfony": "^2.0",
        "phpstan/phpstan-doctrine": "^2.0",
        "phpstan/phpstan-phpunit": "^2.0",
        "phpunit/phpunit": "^11.5",
        "phpmd/phpmd": "^2.15",
        "rector/rector": "^2.0",
        "phpro/grumphp": "^2.9"
    }
}

Порядок запуска в CI/CD

1. composer install
2. composer audit         — security check
3. php-cs-fixer --dry-run — code style
4. phpstan analyse        — static analysis
5. phpunit                — tests with coverage
6. phpcpd (optional)      — copy-paste detection
7. phpmd (optional)       — code metrics

Каждый следующий шаг запускается только при успехе предыдущего. Порядок оптимален: быстрые проверки первыми (стиль, статический анализ), медленные — последними (тесты с покрытием).


Проверь себя

5 из 11

Какой минимальный набор инструментов рекомендуется для каждого PHP-проекта?

Что проверяет PHPMD?

Что находит PHPCPD?

Чем PHP CS Fixer лучше PHP_CodeSniffer для большинства проектов?

Какой инструмент автоматически исправляет стиль кода по PSR-12?