
Chapter 14: Code Quality Tools
Intermediate 90-120 minOverview
Code quality tools help maintain consistent code standards, catch bugs early, and ensure your codebase remains maintainable. If you're coming from Java, you're likely familiar with tools like Checkstyle, PMD, SpotBugs, and SonarQube. PHP has equivalent tools that provide static analysis, code formatting, and quality metrics.
This chapter covers the essential code quality tools for PHP development, from static analysis with PHPStan and Psalm to automated code formatting with PHP CS Fixer. You'll learn how to set up these tools, configure them for your project, integrate them into your development workflow, and automate quality checks through Git hooks and CI/CD pipelines.
By the end of this chapter, you'll have a complete code quality toolchain that catches bugs before they reach production, enforces consistent coding standards across your team, and helps maintain a high-quality codebase as your project grows.
What You'll Learn:
- Static analysis with PHPStan and Psalm
- Code style checking with PHP_CodeSniffer
- Automatic code formatting with PHP CS Fixer
- Custom coding standards (PSR-12, PSR-1)
- Security-focused tools (Rector, Security Checker)
- Advanced analysis tools (Deptrac, PHP Insights, Infection, PHPBench)
- Git hooks for automated checks (pre-commit, pre-push, commit-msg)
- Integrating quality tools into CI/CD
- Mess detection with PHPMD
- Copy-paste detection with PHPCPD
- Documentation and API quality tools
- Dependency management quality checks
- Advanced configuration and optimization
- Metrics, reporting, and trend analysis
- IDE integration
What You'll Build
In this chapter, you'll set up:
- A complete PHPStan configuration with level 8 static analysis
- PHP_CodeSniffer rules enforcing PSR-12 coding standards
- PHP CS Fixer configuration for automatic code formatting
- PHPMD ruleset for detecting code smells and complexity issues
- Security scanning with Composer Security Checker and Rector
- Architecture testing with Deptrac
- Mutation testing setup with Infection PHP
- Performance benchmarking with PHPBench
- Git hooks (pre-commit, pre-push, commit-msg) for automated checks
- A comprehensive quality check script that runs all tools in sequence
- CI/CD pipeline integration with GitHub Actions including matrix builds
- Documentation quality tools and PHPDoc standards
- Dependency management quality checks (unused, outdated, licenses)
- Advanced configuration with baselines and custom rules
- Quality metrics dashboard and trend analysis
- IDE configuration for real-time quality feedback
- A production-ready code quality toolchain covering all aspects
Prerequisites
Time Estimate
⏱️ 90-120 minutes to complete this chapter
Before starting this chapter, you should be comfortable with:
- PHP development basics — /series/php-for-java-developers/chapters/01-types-variables-and-operators
- Composer for dependency management — /series/php-for-java-developers/chapters/08-composer-and-dependencies
- Command-line usage and Git version control
- PHP namespaces and autoloading — /series/php-for-java-developers/chapters/06-namespaces-and-autoloading
- Unit testing with PHPUnit — /series/php-for-java-developers/chapters/12-unit-testing-with-phpunit (helpful but not required)
Learning Objectives
By the end of this chapter, you will be able to:
- Configure PHPStan for static analysis
- Use PHP_CodeSniffer to enforce coding standards
- Format code automatically with PHP CS Fixer
- Set up security scanning with Rector and Security Checker
- Test architecture with Deptrac
- Assess test quality with Infection PHP mutation testing
- Benchmark performance with PHPBench
- Set up Git hooks for pre-commit, pre-push, and commit-msg checks
- Detect code smells with PHPMD
- Find duplicate code with PHPCPD
- Integrate tools into CI/CD pipelines with matrix builds
- Manage documentation quality with PHPDoc and API tools
- Check dependency quality (unused, outdated, licenses)
- Customize coding standards with baselines and custom rules
- Configure IDE integration for real-time feedback
- Measure and track code quality metrics with dashboards
Section 1: PHPStan - Static Analysis
PHPStan finds bugs without running your code.
Installation and Basic Usage
# Install PHPStan
composer require --dev phpstan/phpstan
# Run analysis
vendor/bin/phpstan analyse src tests
# Run with specific level (0-9)
vendor/bin/phpstan analyse src --level=8PHPStan Levels
<?php
declare(strict_types=1);
// Level 0 catches basic errors
class Example
{
public function process(): void
{
// PHPStan finds: Undefined variable $user
echo $user->name;
}
}<?php
declare(strict_types=1);
// Level 5 enforces type hints
class UserService
{
// PHPStan error: Missing return type
public function getUser(int $id)
{
return $this->repository->find($id);
}
// Fixed version
public function getUserFixed(int $id): ?User
{
return $this->repository->find($id);
}
}<?php
declare(strict_types=1);
// Level 8 catches subtle type issues
class Calculator
{
public function divide(int $a, int $b): float
{
// PHPStan error: Division by zero possible
return $a / $b;
}
// Fixed version
public function divideFixed(int $a, int $b): float
{
if ($b === 0) {
throw new \InvalidArgumentException('Division by zero');
}
return $a / $b;
}
}PHPStan Configuration
# phpstan.neon
parameters:
level: 8
paths:
- src
- tests
excludePaths:
- tests/bootstrap.php
- vendor
# Ignore specific errors
ignoreErrors:
- '#Call to an undefined method#'
# Check for missing typehints
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
# Strict rules
checkAlwaysTrueCheckTypeFunctionCall: true
checkAlwaysTrueInstanceof: true
checkAlwaysTrueStrictComparison: true
checkExplicitMixedMissingReturn: true
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
# Enable bleeding edge
reportUnmatchedIgnoredErrors: truePHPStan Extensions
# Install useful extensions
composer require --dev \
phpstan/phpstan-phpunit \
phpstan/phpstan-strict-rules \
phpstan/phpstan-deprecation-rules# phpstan.neon with extensions
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
parameters:
level: 8
paths:
- src
- testsReal-World Example
<?php
declare(strict_types=1);
namespace App\Services;
class OrderService
{
public function __construct(
private OrderRepository $orders,
private PaymentGateway $payment
) {}
/**
* PHPStan helps catch type errors
*
* @param array<string, mixed> $data
* @return Order
*/
public function createOrder(array $data): Order
{
// PHPStan ensures all required fields exist
if (!isset($data['user_id'], $data['items'], $data['total'])) {
throw new \InvalidArgumentException('Missing required fields');
}
// PHPStan verifies types match
$order = new Order(
userId: (int) $data['user_id'],
items: $data['items'], // Array<OrderItem>
total: (float) $data['total']
);
// PHPStan catches if save() doesn't return Order
return $this->orders->save($order);
}
/**
* PHPStan enforces proper null handling
*/
public function findOrder(int $id): ?Order
{
$order = $this->orders->findById($id);
// PHPStan error if we don't handle null case
if ($order === null) {
return null;
}
return $order;
}
}Section 2: Psalm - Alternative Static Analyzer
Psalm is another powerful static analyzer with different strengths.
Installation and Configuration
# Install Psalm
composer require --dev vimeo/psalm
# Initialize configuration
vendor/bin/psalm --init
# Run analysis
vendor/bin/psalmPsalm Configuration
<?xml version="1.0"?>
<psalm
errorLevel="3"
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"
>
<projectFiles>
<directory name="src" />
<directory name="tests" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MissingReturnType errorLevel="error" />
<MissingPropertyType errorLevel="error" />
<MissingParamType errorLevel="error" />
</issueHandlers>
</psalm>Psalm Annotations
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\User;
class UserService
{
/**
* Psalm template annotations for generics
*
* @template T
* @param class-string<T> $class
* @return T
*/
public function create(string $class): object
{
return new $class();
}
/**
* Psalm array shape annotations
*
* @param array{name: string, email: string, age: int} $data
* @return User
*/
public function createUser(array $data): User
{
return new User(
$data['name'],
$data['email'],
$data['age']
);
}
/**
* Psalm list vs array distinction
*
* @param list<int> $ids List (sequential array)
* @return array<int, User> Associative array
*/
public function getUsersByIds(array $ids): array
{
$users = [];
foreach ($ids as $id) {
$users[$id] = $this->repository->find($id);
}
return $users;
}
/**
* @psalm-assert !null $user
*/
private function ensureUserExists(?User $user): void
{
if ($user === null) {
throw new \RuntimeException('User not found');
}
}
}Section 3: Security-Focused Tools
Security is a critical aspect of code quality. These tools help identify vulnerabilities and security issues.
Rector - Automated Refactoring
Rector automates PHP upgrades and code modernization.
# Install Rector
composer require --dev rector/rector
# Initialize configuration
vendor/bin/rector init
# Run refactoring
vendor/bin/rector process src --dry-run
vendor/bin/rector process srcRector Configuration
<?php
// rector.php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets(
php84: true
)
->withSets([
LevelSetList::UP_TO_PHP_84,
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::TYPE_DECLARATION,
SetList::PRIVATIZATION,
])
->withSkip([
__DIR__ . '/vendor',
]);Composer Security Checker
Check for known vulnerabilities in dependencies.
# Install Composer Security Checker
composer require --dev sensiolabs/security-checker
# Check dependencies
vendor/bin/security-checker security:check
# Or use online service
composer require --dev symfony/security-checker
vendor/bin/security-checker security:checkPHP Security Checker Integration
#!/bin/bash
# Check for security vulnerabilities
echo "🔒 Checking for security vulnerabilities..."
# Check Composer dependencies
vendor/bin/security-checker security:check
if [ $? -ne 0 ]; then
echo "❌ Security vulnerabilities found!"
exit 1
fi
echo "✅ No known security vulnerabilities"Psalm Security Plugin
Psalm has security-focused analysis.
# Install Psalm security plugin
composer require --dev psalm/plugin-security-checker
# Run security analysis
vendor/bin/psalm --plugin=vendor/psalm/plugin-security-checker/psalm-plugin.phpSecurity-Focused PHPStan Rules
# Install PHPStan security rules
composer require --dev phpstan/phpstan-security-rules# phpstan.neon
includes:
- vendor/phpstan/phpstan-security-rules/rules.neon
parameters:
level: 8
paths:
- srcSection 4: PHP_CodeSniffer - Coding Standards
PHP_CodeSniffer enforces coding standards.
Installation
# Install PHP_CodeSniffer
composer require --dev squizlabs/php_codesniffer
# Check code style
vendor/bin/phpcs src tests
# Fix automatically (when possible)
vendor/bin/phpcbf src testsConfiguration
<?xml version="1.0"?>
<ruleset name="Project Coding Standard">
<description>Custom coding standard for the project</description>
<!-- Include PSR-12 standard -->
<rule ref="PSR12"/>
<!-- Paths to check -->
<file>src</file>
<file>tests</file>
<!-- Exclude patterns -->
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/cache/*</exclude-pattern>
<!-- Custom rules -->
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Generic.PHP.RequireStrictTypes"/>
<!-- Complexity rules -->
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="10"/>
<property name="absoluteComplexity" value="20"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="5"/>
<property name="absoluteNestingLevel" value="10"/>
</properties>
</rule>
<!-- Naming conventions -->
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
<!-- Documentation -->
<rule ref="Generic.Commenting.DocComment"/>
<rule ref="Squiz.Commenting.FunctionComment">
<exclude name="Squiz.Commenting.FunctionComment.MissingParamTag"/>
</rule>
<!-- Code organization -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="150"/>
</properties>
</rule>
<!-- Security -->
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="eval" value="null"/>
<element key="exec" value="null"/>
<element key="system" value="null"/>
<element key="var_dump" value="null"/>
<element key="print_r" value="null"/>
</property>
</properties>
</rule>
</ruleset>PSR Standards Comparison
| Standard | Description | Use Case |
|---|---|---|
| PSR-1 | Basic Coding Standard | Minimum requirements |
| PSR-12 | Extended Coding Style | Recommended for new projects |
| PSR-2 | Deprecated | Replaced by PSR-12 |
Custom Sniffs
<?php
declare(strict_types=1);
namespace MyProject\Sniffs\NamingConventions;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
/**
* Custom sniff to enforce service class naming
*/
class ServiceClassNameSniff implements Sniff
{
public function register(): array
{
return [T_CLASS];
}
public function process(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->getTokens();
$className = $phpcsFile->getDeclarationName($stackPtr);
// Check if class is in Services namespace
$namespace = $phpcsFile->getNamespace($stackPtr);
if (str_contains($namespace, 'Services')) {
// Enforce 'Service' suffix
if (!str_ends_with($className, 'Service')) {
$phpcsFile->addError(
'Service classes must end with "Service" suffix',
$stackPtr,
'InvalidServiceName'
);
}
}
}
}Section 5: PHP CS Fixer - Automatic Formatting
PHP CS Fixer automatically fixes code style issues.
Installation
# Install PHP CS Fixer
composer require --dev friendsofphp/php-cs-fixer
# Run fixer
vendor/bin/php-cs-fixer fix src
# Dry run (see what would change)
vendor/bin/php-cs-fixer fix src --dry-run --diffConfiguration
<?php
// .php-cs-fixer.php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = Finder::create()
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => [
'default' => 'single_space',
],
'blank_line_after_opening_tag' => true,
'blank_line_before_statement' => [
'statements' => ['return', 'try', 'throw'],
],
'cast_spaces' => true,
'class_attributes_separation' => [
'elements' => ['method' => 'one', 'property' => 'one'],
],
'concat_space' => ['spacing' => 'one'],
'declare_strict_types' => true,
'function_typehint_space' => true,
'lowercase_cast' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => [
'tokens' => [
'extra',
'throw',
'use',
],
],
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_mixed_echo_print' => ['use' => 'echo'],
'no_multiline_whitespace_around_double_arrow' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_around_offset' => true,
'no_trailing_comma_in_singleline' => true,
'no_unneeded_control_parentheses' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'normalize_index_brace' => true,
'object_operator_without_whitespace' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'phpdoc_indent' => true,
'phpdoc_inline_tag_normalizer' => true,
'phpdoc_no_access' => true,
'phpdoc_no_package' => true,
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_scalar' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_summary' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_var_without_name' => true,
'return_type_declaration' => true,
'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true,
'single_class_element_per_statement' => true,
'single_quote' => true,
'space_after_semicolon' => true,
'standardize_not_equals' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
])
->setFinder($finder)
->setRiskyAllowed(true);Before and After
<?php
namespace App\Services;
use App\Models\User;
use App\Repositories\UserRepository;
class UserService{
private $repository;
public function __construct(UserRepository $repository){
$this->repository=$repository;
}
public function getUser($id){
$user=$this->repository->find($id);
if(!$user){
throw new \Exception("User not found");
}
return $user;
}
}<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\User;
use App\Repositories\UserRepository;
class UserService
{
public function __construct(
private UserRepository $repository
) {}
public function getUser(int $id): User
{
$user = $this->repository->find($id);
if ($user === null) {
throw new \RuntimeException('User not found');
}
return $user;
}
}Section 6: PHPMD - Mess Detection
PHPMD finds potential problems in your code.
Installation
# Install PHPMD
composer require --dev phpmd/phpmd
# Run mess detector
vendor/bin/phpmd src text cleancode,codesize,controversial,design,naming,unusedcodeConfiguration
<?xml version="1.0"?>
<ruleset name="Project Mess Detection Rules">
<description>Custom PHPMD rules</description>
<!-- Code Size Rules -->
<rule ref="rulesets/codesize.xml/CyclomaticComplexity">
<properties>
<property name="reportLevel" value="10"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/NPathComplexity">
<properties>
<property name="minimum" value="200"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
<properties>
<property name="minimum" value="50"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveClassLength">
<properties>
<property name="minimum" value="500"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveParameterList">
<properties>
<property name="minimum" value="5"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyFields">
<properties>
<property name="maxfields" value="15"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyMethods">
<properties>
<property name="maxmethods" value="20"/>
</properties>
</rule>
<!-- Clean Code Rules -->
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess"/>
<exclude name="ElseExpression"/>
</rule>
<!-- Design Rules -->
<rule ref="rulesets/design.xml"/>
<!-- Naming Rules -->
<rule ref="rulesets/naming.xml">
<exclude name="ShortVariable"/>
<exclude name="LongVariable"/>
</rule>
<rule ref="rulesets/naming.xml/ShortVariable">
<properties>
<property name="minimum" value="2"/>
<property name="exceptions" value="id,db,tz"/>
</properties>
</rule>
<!-- Unused Code Rules -->
<rule ref="rulesets/unusedcode.xml"/>
</ruleset>Common Issues Detected
<?php
declare(strict_types=1);
// ❌ PHPMD: Too many parameters
class OrderService
{
public function createOrder(
int $userId,
array $items,
string $shippingAddress,
string $billingAddress,
string $paymentMethod,
?string $couponCode,
bool $giftWrap
): Order {
// ...
}
}
// ✅ Fixed: Use parameter object
class OrderService
{
public function createOrder(OrderData $orderData): Order
{
// ...
}
}
// ❌ PHPMD: Cyclomatic complexity too high
class PriceCalculator
{
public function calculate(Order $order): float
{
$total = 0;
if ($order->hasDiscount()) {
if ($order->discountType === 'percentage') {
$total -= $total * ($order->discountValue / 100);
} elseif ($order->discountType === 'fixed') {
$total -= $order->discountValue;
}
}
if ($order->requiresShipping()) {
if ($order->shippingMethod === 'express') {
$total += 20;
} elseif ($order->shippingMethod === 'standard') {
$total += 5;
}
}
// ... more nested conditions
}
}
// ✅ Fixed: Extract methods
class PriceCalculator
{
public function calculate(Order $order): float
{
$total = $order->getSubtotal();
$total = $this->applyDiscount($total, $order);
$total = $this->addShipping($total, $order);
return $total;
}
private function applyDiscount(float $total, Order $order): float
{
if (!$order->hasDiscount()) {
return $total;
}
return match ($order->discountType) {
'percentage' => $total * (1 - $order->discountValue / 100),
'fixed' => $total - $order->discountValue,
default => $total,
};
}
private function addShipping(float $total, Order $order): float
{
if (!$order->requiresShipping()) {
return $total;
}
return $total + match ($order->shippingMethod) {
'express' => 20,
'standard' => 5,
default => 0,
};
}
}Section 7: PHPCPD - Copy-Paste Detection
Find duplicate code blocks.
Installation and Usage
# Install PHPCPD
composer require --dev sebastian/phpcpd
# Detect duplicates
vendor/bin/phpcpd src
# With minimum lines and tokens
vendor/bin/phpcpd --min-lines 5 --min-tokens 50 srcExample Output
phpcpd 6.0.3 by Sebastian Bergmann.
Found 2 clones with 45 duplicated lines in 4 files:
- src/Services/UserService.php:15-30
src/Services/AdminService.php:20-35
- src/Repositories/UserRepository.php:40-55
src/Repositories/PostRepository.php:45-60
2.15% duplicated lines out of 2094 total lines of code.
Average: 22.5 lines per cloneRefactoring Duplicates
<?php
declare(strict_types=1);
// ❌ Before: Duplicated code
class UserRepository
{
public function findActive(): array
{
$stmt = $this->pdo->prepare(
'SELECT * FROM users WHERE status = ? ORDER BY created_at DESC'
);
$stmt->execute(['active']);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
class PostRepository
{
public function findPublished(): array
{
$stmt = $this->pdo->prepare(
'SELECT * FROM posts WHERE status = ? ORDER BY created_at DESC'
);
$stmt->execute(['published']);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// ✅ After: Extracted to base class
abstract class Repository
{
protected function findByStatus(string $status): array
{
$table = $this->getTable();
$stmt = $this->pdo->prepare(
"SELECT * FROM {$table} WHERE status = ? ORDER BY created_at DESC"
);
$stmt->execute([$status]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
abstract protected function getTable(): string;
}
class UserRepository extends Repository
{
protected function getTable(): string
{
return 'users';
}
public function findActive(): array
{
return $this->findByStatus('active');
}
}
class PostRepository extends Repository
{
protected function getTable(): string
{
return 'posts';
}
public function findPublished(): array
{
return $this->findByStatus('published');
}
}Section 8: Advanced Static Analysis Tools
Beyond PHPStan and Psalm, there are specialized tools for architecture, insights, and testing quality.
Deptrac - Architecture Testing
Deptrac enforces architectural boundaries and dependency rules.
# Install Deptrac
composer require --dev qossmic/deptrac-shim
# Initialize configuration
vendor/bin/deptrac init
# Analyze dependencies
vendor/bin/deptrac analyseDeptrac Configuration
# deptrac.yaml
deptrac:
paths:
- ./src
exclude_files:
- '#.*test.*#'
layers:
- name: Controller
collectors:
- type: className
regex: .*Controller.*
- name: Service
collectors:
- type: className
regex: .*Service.*
- name: Repository
collectors:
- type: className
regex: .*Repository.*
ruleset:
Controller:
- Service
- Repository
Service:
- Repository
Repository: []PHP Insights - All-in-One Quality Tool
PHP Insights combines multiple analyzers in one tool.
# Install PHP Insights
composer require --dev nunomaduro/phpinsights
# Analyze code
vendor/bin/phpinsights analyse src
# Fix issues automatically
vendor/bin/phpinsights fix srcPHP Insights Configuration
<?php
// phpinsights.php
declare(strict_types=1);
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenDefineGlobalConstants;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenNormalClasses;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenTraits;
use NunoMaduro\PhpInsights\Domain\Metrics\Architecture\Classes;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseConstantSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\ReturnTypeHintSniff;
return [
'preset' => 'laravel',
'ide' => 'vscode',
'exclude' => [
'vendor',
'storage',
'bootstrap/cache',
],
'add' => [
Classes::class => [
ForbiddenNormalClasses::class,
],
],
'remove' => [
ForbiddenTraits::class,
ForbiddenDefineGlobalConstants::class,
],
'config' => [
ReturnTypeHintSniff::class => [
'enableObjectTypeHint' => false,
],
LowerCaseConstantSniff::class => [
'exclude' => ['T_STRING'],
],
],
'requirements' => [
'min-quality' => 80,
'min-complexity' => 80,
'min-architecture' => 80,
'min-style' => 80,
],
];Infection PHP - Mutation Testing
Infection tests the quality of your test suite by introducing mutations.
# Install Infection
composer require --dev infection/infection
# Run mutation testing
vendor/bin/infection
# With coverage
vendor/bin/infection --coverage=coverageInfection Configuration
{
"timeout": 10,
"source": {
"directories": [
"src"
],
"excludes": [
"tests",
"vendor"
]
},
"logs": {
"text": "infection.log",
"html": "infection.html",
"summary": "infection-summary.log"
},
"mutators": {
"@default": true,
"TrueValue": {
"ignore": [
"App\\Service\\*"
]
}
},
"testFramework": "phpunit",
"phpUnit": {
"configDir": "."
},
"minMsi": 60,
"minCoveredMsi": 80
}PHPBench - Performance Benchmarking
PHPBench measures and compares performance of your code.
# Install PHPBench
composer require --dev phpbench/phpbench
# Run benchmarks
vendor/bin/phpbench run
# Compare results
vendor/bin/phpbench report --report=defaultPHPBench Example
<?php
declare(strict_types=1);
namespace Benchmarks;
use PhpBench\Attributes\BeforeMethods;
use PhpBench\Attributes\Iterations;
use PhpBench\Attributes\Revs;
use PhpBench\Attributes\Warmup;
/**
* Benchmark array operations
*/
class ArrayBench
{
private array $data;
#[BeforeMethods('setUp')]
#[Warmup(2)]
#[Revs(1000)]
#[Iterations(5)]
public function benchArrayMap(): void
{
array_map(fn($x) => $x * 2, $this->data);
}
#[BeforeMethods('setUp')]
#[Warmup(2)]
#[Revs(1000)]
#[Iterations(5)]
public function benchForeach(): void
{
$result = [];
foreach ($this->data as $value) {
$result[] = $value * 2;
}
}
public function setUp(): void
{
$this->data = range(1, 1000);
}
}Section 9: Git Hooks
Automate quality checks before commits.
Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit
echo "Running code quality checks..."
# Get list of staged PHP files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".php$")
if [ -z "$STAGED_FILES" ]; then
echo "No PHP files to check"
exit 0
fi
# Run PHPStan
echo "Running PHPStan..."
vendor/bin/phpstan analyse $STAGED_FILES --level=8 --no-progress
if [ $? -ne 0 ]; then
echo "❌ PHPStan failed. Please fix errors before committing."
exit 1
fi
# Run PHP_CodeSniffer
echo "Running PHP_CodeSniffer..."
vendor/bin/phpcs $STAGED_FILES --standard=PSR12
if [ $? -ne 0 ]; then
echo "❌ Code style issues found. Run 'vendor/bin/phpcbf' to fix."
exit 1
fi
# Run tests
echo "Running tests..."
vendor/bin/phpunit
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Please fix before committing."
exit 1
fi
echo "✅ All checks passed!"
exit 0Husky-like Setup with Composer
{
"scripts": {
"pre-commit": [
"@php-cs-fixer",
"@phpstan",
"@phpunit"
],
"php-cs-fixer": "php-cs-fixer fix --dry-run --diff",
"phpstan": "phpstan analyse src tests --level=8",
"phpunit": "phpunit",
"test": [
"@phpstan",
"@phpunit"
],
"fix": "php-cs-fixer fix"
}
}Captain Hook Integration
# Install Captain Hook
composer require --dev captainhook/captainhook
# Initialize hooks
vendor/bin/captainhook install{
"config": {
"captainhook": {
"pre-commit": {
"enabled": true,
"actions": [
{
"action": "vendor/bin/phpstan analyse src --level=8",
"options": [],
"conditions": []
},
{
"action": "vendor/bin/phpcs src --standard=PSR12",
"options": [],
"conditions": []
}
]
},
"pre-push": {
"enabled": true,
"actions": [
{
"action": "vendor/bin/phpunit",
"options": [],
"conditions": []
}
]
}
}
}
}Section 10: Advanced Git Hooks
Beyond pre-commit, there are other useful Git hooks for quality checks.
Pre-Push Hook
Run comprehensive checks before pushing to remote.
#!/bin/bash
# .git/hooks/pre-push
echo "Running pre-push quality checks..."
# Run full test suite
echo "Running tests..."
vendor/bin/phpunit
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Push aborted."
exit 1
fi
# Run all quality checks
echo "Running quality checks..."
./scripts/check-quality.sh
if [ $? -ne 0 ]; then
echo "❌ Quality checks failed. Push aborted."
exit 1
fi
echo "✅ All checks passed!"
exit 0Commit Message Hook
Enforce commit message standards.
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check format: type(scope): subject
if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+"; then
echo "❌ Invalid commit message format."
echo "Format: type(scope): subject"
echo "Types: feat, fix, docs, style, refactor, test, chore"
exit 1
fi
exit 0Post-Merge Hook
Update dependencies after merge.
#!/bin/bash
# .git/hooks/post-merge
echo "Running post-merge tasks..."
# Update Composer dependencies
if [ -f "composer.json" ]; then
echo "Updating Composer dependencies..."
composer install --no-interaction
fi
# Clear caches
if [ -d "var/cache" ]; then
echo "Clearing cache..."
rm -rf var/cache/*
fi
echo "✅ Post-merge tasks completed"Section 11: CI/CD Integration
Integrate quality tools into your pipeline.
GitHub Actions
# .github/workflows/code-quality.yml
name: Code Quality
on: [push, pull_request]
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer, cs2pr
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse src tests --level=8 --error-format=github
- name: Run PHP_CodeSniffer
run: vendor/bin/phpcs src tests --standard=PSR12 --report=checkstyle | cs2pr
- name: Run PHPMD
run: vendor/bin/phpmd src github cleancode,codesize,controversial,design,naming,unusedcode
- name: Run PHPCPD
run: vendor/bin/phpcpd src
- name: Run Security Check
run: vendor/bin/security-checker security:check
- name: Run Deptrac
run: vendor/bin/deptrac analyse
continue-on-error: true
- name: Run Infection
run: vendor/bin/infection --coverage=coverage --min-msi=60
continue-on-error: true
- name: Run Tests
run: vendor/bin/phpunit --coverage-clover=coverage.xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xmlCombining Tools
#!/bin/bash
# scripts/check-quality.sh
set -e
echo "🔍 Running code quality checks..."
echo ""
ERRORS=0
# Static analysis
echo "📊 PHPStan..."
if vendor/bin/phpstan analyse src tests --level=8 --no-progress; then
echo "✅ PHPStan passed"
else
echo "❌ PHPStan failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Code style
echo "🎨 PHP_CodeSniffer..."
if vendor/bin/phpcs src tests --standard=PSR12; then
echo "✅ PHP_CodeSniffer passed"
else
echo "❌ PHP_CodeSniffer failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Mess detection
echo "🔨 PHPMD..."
if vendor/bin/phpmd src text cleancode,codesize,design,naming,unusedcode; then
echo "✅ PHPMD passed"
else
echo "❌ PHPMD failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Copy-paste detection
echo "📋 PHPCPD..."
if vendor/bin/phpcpd src --min-lines=5; then
echo "✅ PHPCPD passed"
else
echo "❌ PHPCPD failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Security check
echo "🔒 Security Check..."
if vendor/bin/security-checker security:check; then
echo "✅ Security check passed"
else
echo "❌ Security vulnerabilities found"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Architecture check
echo "🏗️ Deptrac..."
if vendor/bin/deptrac analyse --no-progress; then
echo "✅ Deptrac passed"
else
echo "⚠️ Architecture violations found (non-blocking)"
fi
echo ""
# Summary
if [ $ERRORS -eq 0 ]; then
echo "✅ All quality checks passed!"
exit 0
else
echo "❌ $ERRORS check(s) failed"
exit 1
fiMatrix Builds for Multiple PHP Versions
# .github/workflows/quality-matrix.yml
name: Quality Matrix
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v3
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse src --level=8
- name: Run Tests
run: vendor/bin/phpunitSection 12: Documentation & API Quality
Maintaining high-quality documentation is part of code quality.
PHPDoc Standards
Enforce proper PHPDoc comments.
# Install PHPDoc Checker
composer require --dev phpstan/phpstan-strict-rules
# PHPStan checks PHPDoc completeness
vendor/bin/phpstan analyse src --level=8PHPDoc Example
<?php
declare(strict_types=1);
namespace App\Services;
/**
* User service for managing user operations
*
* @package App\Services
*/
class UserService
{
/**
* Create a new user
*
* @param array{name: string, email: string, age: int} $data User data
* @return User Created user instance
* @throws \InvalidArgumentException If data is invalid
* @throws \RuntimeException If user creation fails
*/
public function createUser(array $data): User
{
// Implementation
}
/**
* Find user by ID
*
* @param int $id User ID
* @return User|null User instance or null if not found
*/
public function findById(int $id): ?User
{
// Implementation
}
}API Documentation Tools
Generate API documentation from code.
# Install phpDocumentor
composer require --dev phpdocumentor/phpdocumentor
# Generate documentation
vendor/bin/phpdoc -d src -t docs/apiOpenAPI/Swagger Generation
# Install Swagger PHP
composer require --dev zircote/swagger-php
# Generate OpenAPI spec
vendor/bin/openapi src -o openapi.yamlREADME Quality Checks
#!/bin/bash
# Check README completeness
README="README.md"
if [ ! -f "$README" ]; then
echo "❌ README.md not found"
exit 1
fi
# Check for required sections
REQUIRED_SECTIONS=("Installation" "Usage" "Requirements" "License")
for section in "${REQUIRED_SECTIONS[@]}"; do
if ! grep -q "$section" "$README"; then
echo "⚠️ README missing section: $section"
fi
done
echo "✅ README check completed"Section 13: Dependency Management Quality
Ensure your dependencies are well-managed and secure.
Composer Unused Dependencies
Find unused Composer packages.
# Install Composer Unused
composer require --dev icanhazstring/composer-unused
# Check for unused dependencies
vendor/bin/composer-unusedComposer Normalize
Standardize composer.json format.
# Install Composer Normalize
composer require --dev ergebnis/composer-normalize
# Normalize composer.json
vendor/bin/composer-normalize
# Check without modifying
vendor/bin/composer-normalize --dry-runLicense Compatibility Checker
# Install License Checker
composer require --dev composer/composer
# Check licenses
composer licensesDependency Update Checker
# Install Composer Outdated
composer require --dev composer/composer
# Check for outdated packages
composer outdated
# Show security advisories
composer auditComposer Scripts for Quality
{
"scripts": {
"quality": [
"@phpstan",
"@phpcs",
"@security-check",
"@unused-check"
],
"phpstan": "phpstan analyse src --level=8",
"phpcs": "phpcs src --standard=PSR12",
"security-check": "security-checker security:check",
"unused-check": "composer-unused",
"normalize": "composer-normalize",
"audit": "composer audit"
}
}Section 14: Advanced Configuration
Optimize and customize your quality toolchain.
Baseline Files
Manage existing technical debt with baselines.
# Generate PHPStan baseline
vendor/bin/phpstan analyse src --level=8 --generate-baseline phpstan-baseline.neon# phpstan-baseline.neon
parameters:
ignoreErrors:
- '#Call to an undefined method App\\Service\\.*#'
- '#Access to an undefined property App\\Model\\.*#'Custom Rule Development
Create project-specific rules.
<?php
// src/Rules/CustomSniff.php
declare(strict_types=1);
namespace App\Rules;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
class ServiceNamingSniff implements Sniff
{
public function register(): array
{
return [T_CLASS];
}
public function process(File $phpcsFile, $stackPtr): void
{
$className = $phpcsFile->getDeclarationName($stackPtr);
$namespace = $phpcsFile->getNamespace($stackPtr);
if (str_contains($namespace, 'Service') && !str_ends_with($className, 'Service')) {
$phpcsFile->addError(
'Service classes must end with "Service"',
$stackPtr,
'InvalidServiceName'
);
}
}
}Team Configuration Sharing
Share quality configurations across teams.
# Create quality-config package
mkdir quality-config
cd quality-config
# Include configurations
# - phpstan.neon
# - phpcs.xml
# - .php-cs-fixer.php
# - deptrac.yaml
# - infection.jsonPerformance Optimization
Speed up analysis for large codebases.
# phpstan.neon - Optimized for performance
parameters:
level: 8
paths:
- src
parallel:
jobSize: 20
maximumNumberOfProcesses: 4
tmpDir: build/phpstan
resultCachePath: build/phpstan-result-cache.php# Use result cache
vendor/bin/phpstan analyse --memory-limit=2GQuality Gates
Set up pass/fail thresholds.
<?php
// scripts/quality-gate.php
declare(strict_types=1);
$gates = [
'phpstan' => ['command' => 'vendor/bin/phpstan analyse src --level=8', 'required' => true],
'phpcs' => ['command' => 'vendor/bin/phpcs src --standard=PSR12', 'required' => true],
'coverage' => ['command' => 'vendor/bin/phpunit --coverage-text', 'threshold' => 80, 'required' => false],
'infection' => ['command' => 'vendor/bin/infection --min-msi=60', 'required' => false],
];
$failed = [];
foreach ($gates as $name => $config) {
echo "Checking {$name}...\n";
exec($config['command'], $output, $code);
if ($code !== 0) {
if ($config['required'] ?? true) {
$failed[] = $name;
echo "❌ {$name} failed (required)\n";
} else {
echo "⚠️ {$name} failed (optional)\n";
}
} else {
echo "✅ {$name} passed\n";
}
}
if (!empty($failed)) {
echo "\n❌ Quality gate failed. Required checks: " . implode(', ', $failed) . "\n";
exit(1);
}
echo "\n✅ Quality gate passed!\n";
exit(0);Section 15: IDE Integration
Configure your IDE for real-time feedback.
PHPStorm Configuration
Enable PHPStan:
- Settings → PHP → Quality Tools → PHPStan
- Configuration: Point to
vendor/bin/phpstan - Configuration file:
phpstan.neon - Enable inspection: Settings → Editor → Inspections → PHPStan validation
Enable PHP_CodeSniffer:
- Settings → PHP → Quality Tools → PHP_CodeSniffer
- Configuration: Point to
vendor/bin/phpcs - Coding standard: PSR12
- Enable inspection: Settings → Editor → Inspections → PHP Code Sniffer validation
Enable PHP CS Fixer:
- Settings → PHP → Quality Tools → PHP CS Fixer
- PHP CS Fixer path:
vendor/bin/php-cs-fixer - Ruleset:
.php-cs-fixer.php - Enable on save: Settings → Tools → Actions on Save → Reformat code
VS Code Configuration
{
"php.validate.enable": true,
"php.validate.run": "onType",
// PHPStan
"phpstan.enabled": true,
"phpstan.level": "8",
"phpstan.configFile": "phpstan.neon",
// PHP_CodeSniffer
"phpcs.enable": true,
"phpcs.standard": "PSR12",
// PHP CS Fixer
"php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer",
"php-cs-fixer.onsave": true,
"php-cs-fixer.rules": "@PSR12",
// Format on save
"[php]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "junstyle.php-cs-fixer"
}
}Section 16: Metrics and Reporting
Measure and track code quality over time.
PHPMetrics
# Install PHPMetrics
composer require --dev phpmetrics/phpmetrics
# Generate report
vendor/bin/phpmetrics --report-html=build/metrics srcSonarQube for PHP
# sonar-project.properties
sonar.projectKey=my-php-project
sonar.projectName=My PHP Project
sonar.sources=src
sonar.tests=tests
sonar.php.coverage.reportPaths=coverage/clover.xml
sonar.php.tests.reportPath=build/phpunit.xml
sonar.exclusions=vendor/**,tests/**HTML Reports
Generate visual quality dashboards.
# PHPStan HTML report
vendor/bin/phpstan analyse src --level=8 --error-format=table > phpstan-report.html
# PHPMetrics HTML report
vendor/bin/phpmetrics --report-html=build/metrics src
# PHP Insights HTML report
vendor/bin/phpinsights analyse src --format=html > build/insights.htmlTrend Analysis
Track quality metrics over time.
<?php
// scripts/quality-trend.php
declare(strict_types=1);
/**
* Track quality metrics over time
*/
$metricsFile = 'build/quality-metrics.json';
$date = date('Y-m-d');
// Run tools and collect metrics
$metrics = [
'date' => $date,
'phpstan_errors' => getPHPStanErrors(),
'phpcs_errors' => getPHPCSErrors(),
'coverage' => getCoverage(),
'complexity' => getComplexity(),
'duplication' => getDuplication(),
];
// Load historical data
$history = file_exists($metricsFile)
? json_decode(file_get_contents($metricsFile), true)
: [];
$history[] = $metrics;
// Save updated history
file_put_contents($metricsFile, json_encode($history, JSON_PRETTY_PRINT));
// Generate trend report
generateTrendReport($history);
function getPHPStanErrors(): int
{
exec('vendor/bin/phpstan analyse src --level=8 --no-progress 2>&1', $output);
return count(array_filter($output, fn($line) => str_contains($line, 'Error')));
}
function getPHPCSErrors(): int
{
exec('vendor/bin/phpcs src --standard=PSR12 --report=json 2>&1', $output);
$json = json_decode(implode("\n", $output), true);
return $json['totals']['errors'] ?? 0;
}
function getCoverage(): float
{
exec('vendor/bin/phpunit --coverage-text --coverage-filter=src 2>&1', $output);
foreach ($output as $line) {
if (preg_match('/Lines:\s+(\d+\.\d+)%/', $line, $matches)) {
return (float)$matches[1];
}
}
return 0.0;
}
function getComplexity(): float
{
exec('vendor/bin/phpmd src json codesize 2>&1', $output);
$json = json_decode(implode("\n", $output), true);
// Calculate average complexity
return 0.0; // Simplified
}
function getDuplication(): float
{
exec('vendor/bin/phpcpd src --min-lines=5 2>&1', $output);
foreach ($output as $line) {
if (preg_match('/(\d+\.\d+)% duplicated/', $line, $matches)) {
return (float)$matches[1];
}
}
return 0.0;
}
function generateTrendReport(array $history): void
{
echo "Quality Metrics Trend Report\n";
echo str_repeat("=", 50) . "\n\n";
foreach ($history as $entry) {
echo "Date: {$entry['date']}\n";
echo " PHPStan Errors: {$entry['phpstan_errors']}\n";
echo " PHPCS Errors: {$entry['phpcs_errors']}\n";
echo " Coverage: {$entry['coverage']}%\n";
echo " Duplication: {$entry['duplication']}%\n";
echo "\n";
}
}Team Dashboards
Create shared quality dashboards.
#!/bin/bash
# scripts/generate-dashboard.sh
# Generate all reports
vendor/bin/phpmetrics --report-html=build/dashboard/metrics src
vendor/bin/phpinsights analyse src --format=html > build/dashboard/insights.html
vendor/bin/phpstan analyse src --level=8 --error-format=table > build/dashboard/phpstan.html
# Create index page
cat > build/dashboard/index.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>Code Quality Dashboard</title>
</head>
<body>
<h1>Code Quality Dashboard</h1>
<ul>
<li><a href="metrics/index.html">PHPMetrics</a></li>
<li><a href="insights.html">PHP Insights</a></li>
<li><a href="phpstan.html">PHPStan</a></li>
</ul>
</body>
</html>
EOF
echo "✅ Dashboard generated at build/dashboard/index.html"Quality Gate Script
<?php
declare(strict_types=1);
/**
* Check if code meets quality standards
*/
$metrics = [
'phpstan' => checkPHPStan(),
'phpcs' => checkPHPCS(),
'coverage' => checkCoverage(),
'complexity' => checkComplexity(),
'security' => checkSecurity(),
'duplication' => checkDuplication(),
];
$passed = array_filter($metrics);
if (count($passed) === count($metrics)) {
echo "✅ Quality gate passed!\n";
exit(0);
} else {
echo "❌ Quality gate failed:\n";
foreach ($metrics as $check => $result) {
echo " " . ($result ? '✅' : '❌') . " {$check}\n";
}
exit(1);
}
function checkPHPStan(): bool
{
exec('vendor/bin/phpstan analyse src --level=8', $output, $code);
return $code === 0;
}
function checkPHPCS(): bool
{
exec('vendor/bin/phpcs src --standard=PSR12', $output, $code);
return $code === 0;
}
function checkCoverage(): bool
{
$xml = simplexml_load_file('coverage/clover.xml');
$metrics = $xml->project->metrics;
$coverage = (int) $metrics['statements'] / (int) $metrics['elements'] * 100;
return $coverage >= 80; // Require 80% coverage
}
function checkComplexity(): bool
{
exec('vendor/bin/phpmd src text codesize', $output, $code);
return $code === 0;
}
function checkSecurity(): bool
{
exec('vendor/bin/security-checker security:check', $output, $code);
return $code === 0;
}
function checkDuplication(): bool
{
exec('vendor/bin/phpcpd src --min-lines=5', $output, $code);
// Check if duplication is below threshold
foreach ($output as $line) {
if (preg_match('/(\d+\.\d+)% duplicated/', $line, $matches)) {
return (float)$matches[1] < 5.0; // Less than 5% duplication
}
}
return true;
}Exercises
Practice code quality tool configuration with these hands-on exercises:
Exercise 1: Set Up Quality Tools
Goal: Install and configure all essential code quality tools for a new project.
Create a new directory called quality-tools-exercise and set up:
- Initialize a Composer project:
mkdir quality-tools-exercise
cd quality-tools-exercise
composer init --no-interaction --name="myproject/quality-demo"- Install PHPStan with level 8:
composer require --dev phpstan/phpstan
vendor/bin/phpstan --init
# Edit phpstan.neon to set level: 8- Install PHP_CodeSniffer with PSR-12:
composer require --dev squizlabs/php_codesniffer
vendor/bin/phpcs --config-set default_standard PSR12- Install PHP CS Fixer:
composer require --dev friendsofphp/php-cs-fixer
vendor/bin/php-cs-fixer init- Install PHPMD:
composer require --dev phpmd/phpmdValidation: Run each tool to verify installation:
vendor/bin/phpstan --version
vendor/bin/phpcs --version
vendor/bin/php-cs-fixer --version
vendor/bin/phpmd --versionExpected output: Version numbers for each tool.
Exercise 2: Create Quality Check Script
Goal: Build a script that runs all quality checks and provides a summary.
Create scripts/check-quality.sh:
#!/bin/bash
# scripts/check-quality.sh
set -e
echo "🔍 Running code quality checks..."
echo ""
ERRORS=0
# PHPStan
echo "📊 Running PHPStan..."
if vendor/bin/phpstan analyse src --level=8 --no-progress; then
echo "✅ PHPStan passed"
else
echo "❌ PHPStan failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# PHP_CodeSniffer
echo "🎨 Running PHP_CodeSniffer..."
if vendor/bin/phpcs src --standard=PSR12; then
echo "✅ PHP_CodeSniffer passed"
else
echo "❌ PHP_CodeSniffer failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# PHPMD
echo "🔨 Running PHPMD..."
if vendor/bin/phpmd src text cleancode,codesize,design,naming,unusedcode; then
echo "✅ PHPMD passed"
else
echo "❌ PHPMD failed"
ERRORS=$((ERRORS + 1))
fi
echo ""
# Summary
if [ $ERRORS -eq 0 ]; then
echo "✅ All quality checks passed!"
exit 0
else
echo "❌ $ERRORS check(s) failed"
exit 1
fiMake it executable:
chmod +x scripts/check-quality.shValidation: Run the script:
./scripts/check-quality.shExpected output: Summary of all checks with pass/fail status.
Exercise 3: Set Up Git Pre-commit Hook
Goal: Create a Git hook that prevents commits if quality checks fail.
Create .git/hooks/pre-commit:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit quality checks..."
# Get staged PHP files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$")
if [ -z "$STAGED_FILES" ]; then
echo "No PHP files to check"
exit 0
fi
# Run PHPStan on staged files
echo "Running PHPStan on staged files..."
vendor/bin/phpstan analyse $STAGED_FILES --level=8 --no-progress
if [ $? -ne 0 ]; then
echo "❌ PHPStan failed. Fix errors before committing."
exit 1
fi
# Run PHP_CodeSniffer on staged files
echo "Running PHP_CodeSniffer on staged files..."
vendor/bin/phpcs $STAGED_FILES --standard=PSR12
if [ $? -ne 0 ]; then
echo "❌ Code style issues found. Run 'vendor/bin/phpcbf' to auto-fix."
exit 1
fi
echo "✅ All pre-commit checks passed!"
exit 0Make it executable:
chmod +x .git/hooks/pre-commitValidation: Try committing a PHP file with style issues:
# Create a file with style issues
echo '<?php class Test{public function test(){}}' > test.php
git add test.php
git commit -m "Test commit"Expected output: Hook should prevent commit and show PHP_CodeSniffer errors.
Common Pitfalls
❌ Running Tools on Vendor Directory
# Bad - Checking vendor code
vendor/bin/phpstan analyse . --level=8
# Good - Only check your code
vendor/bin/phpstan analyse src tests --level=8❌ Ignoring All Warnings
# Bad - Hiding all issues
parameters:
ignoreErrors:
- '#.*#'
# Good - Specific ignores only
parameters:
ignoreErrors:
- '#Call to deprecated method#'❌ Not Fixing Style Issues
# Bad - Just checking without fixing
vendor/bin/phpcs src
# Good - Auto-fix what you can
vendor/bin/phpcbf src
vendor/bin/php-cs-fixer fix srcBest Practices Summary
✅ Start with lower levels - Gradually increase PHPStan/Psalm levels ✅ Automate checks - Use Git hooks and CI/CD ✅ Fix automatically - Use PHP CS Fixer when possible ✅ Track metrics - Monitor code quality over time ✅ Configure IDE - Get instant feedback while coding ✅ Use standards - Follow PSR-12 for consistency ✅ Exclude generated code - Don't check vendor or build directories ✅ Document exceptions - Explain why rules are disabled ✅ Run locally - Catch issues before pushing ✅ Keep tools updated - Benefit from latest improvements
Further Reading
- PHPStan Documentation
- Psalm Documentation
- PHP_CodeSniffer Documentation
- PHP CS Fixer Documentation
- Rector Documentation
- Deptrac Documentation
- PHP Insights Documentation
- Infection PHP Documentation
- PHPBench Documentation
- PSR-12 Extended Coding Style
- Composer Security Checker
- PHPMetrics Documentation
Wrap-up
Congratulations! You've completed Chapter 14 on Code Quality Tools. In this chapter, you've learned how to:
- ✅ Set up static analysis with PHPStan and Psalm to catch bugs before runtime
- ✅ Enforce coding standards with PHP_CodeSniffer and PSR-12
- ✅ Automatically format code with PHP CS Fixer for consistent style
- ✅ Scan for security vulnerabilities with Rector and Security Checker
- ✅ Test architecture boundaries with Deptrac to enforce design rules
- ✅ Assess test quality with Infection PHP mutation testing
- ✅ Benchmark performance with PHPBench to identify regressions
- ✅ Detect code smells with PHPMD to identify maintainability issues
- ✅ Find duplicate code with PHPCPD to improve code reuse
- ✅ Automate quality checks with comprehensive Git hooks (pre-commit, pre-push, commit-msg)
- ✅ Integrate into CI/CD with matrix builds across PHP versions
- ✅ Maintain documentation quality with PHPDoc and API documentation tools
- ✅ Manage dependencies by finding unused packages and checking licenses
- ✅ Configure advanced settings with baselines, custom rules, and performance optimization
- ✅ Configure IDE integration for real-time feedback while coding
- ✅ Measure and track quality with dashboards, trend analysis, and quality gates
These tools form the foundation of a professional PHP development workflow. They help maintain code quality as your project grows, catch bugs early in development, and ensure consistency across team members. In the next chapter, you'll learn about HTTP and request/response handling in PHP.
Chapter Wrap-up Checklist
Before moving to the next chapter, ensure you can:
- [ ] Configure and run PHPStan for static analysis
- [ ] Use PHP_CodeSniffer to enforce coding standards
- [ ] Automatically format code with PHP CS Fixer
- [ ] Set up security scanning with Rector and Security Checker
- [ ] Test architecture with Deptrac
- [ ] Run mutation testing with Infection PHP
- [ ] Benchmark performance with PHPBench
- [ ] Set up Git hooks (pre-commit, pre-push, commit-msg) for automated checks
- [ ] Detect code smells with PHPMD
- [ ] Find duplicate code with PHPCPD
- [ ] Integrate tools into CI/CD pipelines with matrix builds
- [ ] Maintain documentation quality with PHPDoc standards
- [ ] Check dependency quality (unused, outdated, licenses)
- [ ] Customize coding standards with baselines and custom rules
- [ ] Configure IDE integration for real-time feedback
- [ ] Measure and track code quality metrics with dashboards