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