Skip to content

14: Code Quality Tools

Code Quality Tools

Intermediate 90-120 min

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

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

::: info Time Estimate ⏱️ 90-120 minutes to complete this chapter :::

Before starting this chapter, you should be comfortable with:

By the end of this chapter, you will be able to:

  1. Configure PHPStan for static analysis
  2. Use PHP_CodeSniffer to enforce coding standards
  3. Format code automatically with PHP CS Fixer
  4. Set up security scanning with Rector and Security Checker
  5. Test architecture with Deptrac
  6. Assess test quality with Infection PHP mutation testing
  7. Benchmark performance with PHPBench
  8. Set up Git hooks for pre-commit, pre-push, and commit-msg checks
  9. Detect code smells with PHPMD
  10. Find duplicate code with PHPCPD
  11. Integrate tools into CI/CD pipelines with matrix builds
  12. Manage documentation quality with PHPDoc and API tools
  13. Check dependency quality (unused, outdated, licenses)
  14. Customize coding standards with baselines and custom rules
  15. Configure IDE integration for real-time feedback
  16. Measure and track code quality metrics with dashboards

PHPStan finds bugs without running your code.

Terminal window
# 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=8

::: code-group

<?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.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: true
Terminal window
# 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
- tests
<?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

Section titled “Section 2: Psalm - Alternative Static Analyzer”

Psalm is another powerful static analyzer with different strengths.

Terminal window
# Install Psalm
composer require --dev vimeo/psalm
# Initialize configuration
vendor/bin/psalm --init
# Run analysis
vendor/bin/psalm
<?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>
<?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');
}
}
}

Security is a critical aspect of code quality. These tools help identify vulnerabilities and security issues.

Rector automates PHP upgrades and code modernization.

Terminal window
# 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 src
rector.php
<?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',
]);

Check for known vulnerabilities in dependencies.

Terminal window
# 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:check
#!/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 has security-focused analysis.

Terminal window
# 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.php
Terminal window
# 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:
- src

Section 4: PHP_CodeSniffer - Coding Standards

Section titled “Section 4: PHP_CodeSniffer - Coding Standards”

PHP_CodeSniffer enforces coding standards.

Terminal window
# 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 tests
<?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>
StandardDescriptionUse Case
PSR-1Basic Coding StandardMinimum requirements
PSR-12Extended Coding StyleRecommended for new projects
PSR-2DeprecatedReplaced by PSR-12
<?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

Section titled “Section 5: PHP CS Fixer - Automatic Formatting”

PHP CS Fixer automatically fixes code style issues.

Terminal window
# 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 --diff
.php-cs-fixer.php
<?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);

::: code-group

<?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;
}
}

:::


PHPMD finds potential problems in your code.

Terminal window
# Install PHPMD
composer require --dev phpmd/phpmd
# Run mess detector
vendor/bin/phpmd src text cleancode,codesize,controversial,design,naming,unusedcode
<?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>
<?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,
};
}
}

Find duplicate code blocks.

Terminal window
# 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 src
Terminal window
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 clone
<?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');
}
}

Beyond PHPStan and Psalm, there are specialized tools for architecture, insights, and testing quality.

Deptrac enforces architectural boundaries and dependency rules.

Terminal window
# Install Deptrac
composer require --dev qossmic/deptrac-shim
# Initialize configuration
vendor/bin/deptrac init
# Analyze dependencies
vendor/bin/deptrac analyse
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 combines multiple analyzers in one tool.

Terminal window
# Install PHP Insights
composer require --dev nunomaduro/phpinsights
# Analyze code
vendor/bin/phpinsights analyse src
# Fix issues automatically
vendor/bin/phpinsights fix src
phpinsights.php
<?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 tests the quality of your test suite by introducing mutations.

Terminal window
# Install Infection
composer require --dev infection/infection
# Run mutation testing
vendor/bin/infection
# With coverage
vendor/bin/infection --coverage=coverage
{
"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 measures and compares performance of your code.

Terminal window
# Install PHPBench
composer require --dev phpbench/phpbench
# Run benchmarks
vendor/bin/phpbench run
# Compare results
vendor/bin/phpbench report --report=default
<?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);
}
}

Automate quality checks before commits.

.git/hooks/pre-commit
#!/bin/bash
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 0
{
"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"
}
}
Terminal window
# 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": []
}
]
}
}
}
}

Beyond pre-commit, there are other useful Git hooks for quality checks.

Run comprehensive checks before pushing to remote.

.git/hooks/pre-push
#!/bin/bash
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 0

Enforce commit message standards.

.git/hooks/commit-msg
#!/bin/bash
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 0

Update dependencies after merge.

.git/hooks/post-merge
#!/bin/bash
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"

Integrate quality tools into your pipeline.

.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.xml
scripts/check-quality.sh
#!/bin/bash
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
fi
.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/phpunit

Maintaining high-quality documentation is part of code quality.

Enforce proper PHPDoc comments.

Terminal window
# Install PHPDoc Checker
composer require --dev phpstan/phpstan-strict-rules
# PHPStan checks PHPDoc completeness
vendor/bin/phpstan analyse src --level=8
<?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
}
}

Generate API documentation from code.

Terminal window
# Install phpDocumentor
composer require --dev phpdocumentor/phpdocumentor
# Generate documentation
vendor/bin/phpdoc -d src -t docs/api
Terminal window
# Install Swagger PHP
composer require --dev zircote/swagger-php
# Generate OpenAPI spec
vendor/bin/openapi src -o openapi.yaml
#!/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"

Ensure your dependencies are well-managed and secure.

Find unused Composer packages.

Terminal window
# Install Composer Unused
composer require --dev icanhazstring/composer-unused
# Check for unused dependencies
vendor/bin/composer-unused

Standardize composer.json format.

Terminal window
# Install Composer Normalize
composer require --dev ergebnis/composer-normalize
# Normalize composer.json
vendor/bin/composer-normalize
# Check without modifying
vendor/bin/composer-normalize --dry-run
Terminal window
# Install License Checker
composer require --dev composer/composer
# Check licenses
composer licenses
Terminal window
# Install Composer Outdated
composer require --dev composer/composer
# Check for outdated packages
composer outdated
# Show security advisories
composer audit
{
"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"
}
}

Optimize and customize your quality toolchain.

Manage existing technical debt with baselines.

Terminal window
# 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\\.*#'

Create project-specific rules.

src/Rules/CustomSniff.php
<?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'
);
}
}
}

Share quality configurations across teams.

Terminal window
# Create quality-config package
mkdir quality-config
cd quality-config
# Include configurations
# - phpstan.neon
# - phpcs.xml
# - .php-cs-fixer.php
# - deptrac.yaml
# - infection.json

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
Terminal window
# Use result cache
vendor/bin/phpstan analyse --memory-limit=2G

Set up pass/fail thresholds.

scripts/quality-gate.php
<?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);

Configure your IDE for real-time feedback.

Enable PHPStan:

  1. Settings → PHP → Quality Tools → PHPStan
  2. Configuration: Point to vendor/bin/phpstan
  3. Configuration file: phpstan.neon
  4. Enable inspection: Settings → Editor → Inspections → PHPStan validation

Enable PHP_CodeSniffer:

  1. Settings → PHP → Quality Tools → PHP_CodeSniffer
  2. Configuration: Point to vendor/bin/phpcs
  3. Coding standard: PSR12
  4. Enable inspection: Settings → Editor → Inspections → PHP Code Sniffer validation

Enable PHP CS Fixer:

  1. Settings → PHP → Quality Tools → PHP CS Fixer
  2. PHP CS Fixer path: vendor/bin/php-cs-fixer
  3. Ruleset: .php-cs-fixer.php
  4. Enable on save: Settings → Tools → Actions on Save → Reformat code
{
"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"
}
}

Measure and track code quality over time.

Terminal window
# Install PHPMetrics
composer require --dev phpmetrics/phpmetrics
# Generate report
vendor/bin/phpmetrics --report-html=build/metrics src
# 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/**

Generate visual quality dashboards.

Terminal window
# 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.html

Track quality metrics over time.

scripts/quality-trend.php
<?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";
}
}

Create shared quality dashboards.

scripts/generate-dashboard.sh
#!/bin/bash
# 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"
<?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;
}

Practice code quality tool configuration with these hands-on exercises:

Goal: Install and configure all essential code quality tools for a new project.

Create a new directory called quality-tools-exercise and set up:

  1. Initialize a Composer project:
Terminal window
mkdir quality-tools-exercise
cd quality-tools-exercise
composer init --no-interaction --name="myproject/quality-demo"
  1. Install PHPStan with level 8:
Terminal window
composer require --dev phpstan/phpstan
vendor/bin/phpstan --init
# Edit phpstan.neon to set level: 8
  1. Install PHP_CodeSniffer with PSR-12:
Terminal window
composer require --dev squizlabs/php_codesniffer
vendor/bin/phpcs --config-set default_standard PSR12
  1. Install PHP CS Fixer:
Terminal window
composer require --dev friendsofphp/php-cs-fixer
vendor/bin/php-cs-fixer init
  1. Install PHPMD:
Terminal window
composer require --dev phpmd/phpmd

Validation: Run each tool to verify installation:

Terminal window
vendor/bin/phpstan --version
vendor/bin/phpcs --version
vendor/bin/php-cs-fixer --version
vendor/bin/phpmd --version

Expected output: Version numbers for each tool.

Goal: Build a script that runs all quality checks and provides a summary.

Create scripts/check-quality.sh:

scripts/check-quality.sh
#!/bin/bash
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
fi

Make it executable:

Terminal window
chmod +x scripts/check-quality.sh

Validation: Run the script:

Terminal window
./scripts/check-quality.sh

Expected output: Summary of all checks with pass/fail status.

Goal: Create a Git hook that prevents commits if quality checks fail.

Create .git/hooks/pre-commit:

.git/hooks/pre-commit
#!/bin/bash
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 0

Make it executable:

Terminal window
chmod +x .git/hooks/pre-commit

Validation: Try committing a PHP file with style issues:

Terminal window
# 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.


❌ Running Tools on Vendor Directory

Terminal window
# 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

Terminal window
# 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 src

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



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.

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

Previous: ← Chapter 13