Configure quality assurance tooling for Symfony bundles: Makefile, Deptrac architecture enforcement, GrumPHP pre-commit hooks, PHPStan level 9, PHP-CS-Fixer strict @Symfony, Infection mutation testing, composer audit security checks, GitHub Actions CI for Symfony 7+8. Trigger when: 'quality', 'CI', 'makefile', 'deptrac', 'phpstan', 'grumphp', 'infection', 'composer audit', 'security check', 'coding standards', 'architecture', 'publish bundle', 'open source bundle'.
php artisan boost:add-skill symkit/faq-bundle
Save this content to: .claude/skills/symfony-bundle-quality/SKILL.md
# Symfony Bundle Quality — Tooling & CI
This skill covers the complete quality assurance pipeline for distributable Symfony bundles.
---
## 1. Makefile (Single Entry Point)
```makefile
.PHONY: help install test phpstan cs-fix cs-check infection deptrac quality security-check lint ci
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
install: ## Install dependencies
composer install
test: ## Run PHPUnit tests
vendor/bin/phpunit
phpstan: ## Static analysis (level 9)
vendor/bin/phpstan analyse --memory-limit=512M
cs-fix: ## Fix code style
vendor/bin/php-cs-fixer fix
cs-check: ## Check code style (no fix)
vendor/bin/php-cs-fixer fix --dry-run --diff
infection: ## Mutation testing
vendor/bin/infection --only-covered --show-mutations --threads=max --min-msi=70
deptrac: ## Architecture enforcement
vendor/bin/deptrac analyse
security-check: ## Dependency security audit
composer audit
lint: ## Lint config files
@test -f bin/console && php bin/console lint:yaml config/ --parse-tags 2>/dev/null || true
@test -f bin/console && php bin/console lint:xliff translations/ 2>/dev/null || true
@test -d config && find config -name '*.xml' -exec xmllint --noout {} + 2>/dev/null || true
quality: cs-check phpstan deptrac lint test infection ## Full quality pipeline
ci: security-check quality ## Full CI pipeline (security + quality)
```
Always use `make quality` after every significant change. Never commit code that fails.
---
## 2. PHPStan Level 9
### Configuration (`phpstan.neon.dist`)
```neon
parameters:
level: 9
paths:
- src/
tmpDir: .phpstan-cache
```
### Rules
- **Always fix all PHPStan errors**, even pre-existing ones not introduced by the current change — leave the codebase in a better state
- **Never use baselines** (`phpstan-baseline.neon`) to ignore errors — fix them instead
- Type everything: parameters, return types, properties. No implicit `void`.
- No `mixed` except justified with a comment explaining why.
- No `@phpstan-ignore` without an explanatory comment.
- Doctrine collections: `/** @var Collection<int, Entity> */`
- Repository methods: `@return Entity[]` or exact union type, never `mixed[]`
- Custom PHPStan rules can enforce naming conventions (ADRs).
- After every change: `make phpstan`
---
## 3. PHP-CS-Fixer
### Configuration (`.php-cs-fixer.dist.php`)
```php
<?php
declare(strict_types=1);
$finder = (new PhpCsFixer\Finder())
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@PHP82Migration' => true,
'declare_strict_types' => true,
'native_function_invocation' => [
'include' => ['@compiler_optimized'],
'scope' => 'namespaced',
'strict' => true,
],
'trailing_comma_in_multiline' => [
'elements' => ['arguments', 'parameters', 'match', 'arrays'],
],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'global_namespace_import' => [
'import_classes' => true,
'import_functions' => false,
'import_constants' => false,
],
])
->setFinder($finder)
->setRiskyAllowed(true)
;
```
After every change: `make cs-fix`
---
## 4. GrumPHP
### Configuration (`grumphp.yml`)
```yaml
grumphp:
tasks:
make:
task: deptrac
triggered_by: [php]
phpstan:
configuration: phpstan.neon.dist
memory_limit: 512M
use_grumphp_paths: false
phpcsfixer:
config: .php-cs-fixer.dist.php
phpunit:
config_file: phpunit.xml.dist
infection:
min_covered_msi: 65
only_covered: true
threads: 4
test_framework: phpunit
```
Pre-commit hooks prevent non-conforming commits.
> **Deptrac via `make` task**: Use the GrumPHP `make` task with `task: deptrac` to delegate to the Makefile target. This avoids needing a separate shell script.
> **Note**: `composer_audit` is NOT a valid GrumPHP task — do not add it. Run `make security-check` separately.
---
## 5. Infection (Mutation Testing)
### Configuration (`infection.json5`)
```json5
{
"$schema": "https://raw.githubusercontent.com/infection/infection/0.29.0/resources/schema.json",
"source": { "directories": ["src/"] },
"logs": {
"text": "infection.log",
"summary": "infection-summary.log"
},
"minMsi": 70,
"minCoveredMsi": 80,
"threads": "max",
"testFramework": "phpunit",
"testFrameworkOptions": "--testsuite=Unit"
}
```
Run: `make infection` (requires xdebug or pcov for coverage).
---
## 6. Deptrac (Architecture Enforcement)
> **NEVER use `qossmic/deptrac`** — it is abandoned. Always install `deptrac/deptrac`.
> If you see `qossmic/deptrac` anywhere (composer.json, Makefile, grumphp.yml, docs), replace it immediately.
### Configuration (`deptrac.yaml`)
```yaml
deptrac:
paths:
- ./src
layers:
- name: Contract
collectors:
- type: directory
value: src/Contract/.*
- name: Entity
collectors:
- type: directory
value: src/Entity/.*
- name: Event
collectors:
- type: directory
value: src/Event/.*
- name: Repository
collectors:
- type: directory
value: src/Repository/.*
- name: Service
collectors:
- type: directory
value: src/Service/.*
- name: Controller
collectors:
- type: directory
value: src/Controller/.*
- name: DependencyInjection
collectors:
- type: directory
value: src/DependencyInjection/.*
ruleset:
Contract: []
Entity: [Contract]
Event: [Contract, Entity]
Repository: [Contract, Entity]
Service: [Contract, Entity, Repository, Event]
Controller: [Contract, Service, Entity]
DependencyInjection: [Contract, Service, Entity, Repository, Event, Controller]
```
Rules: Entity depends on nothing (or Contract). Service never imports Controller. Contract imports nothing internal.
---
## 7. Security
`composer audit` in Makefile, CI, and GrumPHP. Replaces the deprecated `symfony check:security`.
---
## 8. GitHub Actions CI
Default template — single `ci` job running `make ci` over the full PHP × Symfony matrix:
```yaml
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
ci:
name: PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
symfony: ['7.1.*', '7.2.*', '8.0.*']
exclude:
- { php: '8.2', symfony: '8.0.*' }
- { php: '8.3', symfony: '8.0.*' }
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: intl, mbstring, xml, dom
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer update --no-progress --prefer-dist
- name: Install Symfony version
run: composer require "symfony/framework-bundle:${{ matrix.symfony }}" --no-update
- name: Update with Symfony constraint
run: composer update symfony/* --no-progress --prefer-dist
- name: Run CI pipeline
run: make ci
```
Key points:
- `fail-fast: false` so all matrix combinations always report
- PHP 8.2 and 8.3 are excluded from Symfony 8.0 (requires PHP ≥ 8.4)
- `make ci` covers: `composer audit` + cs-check + phpstan + deptrac + lint + test + infection
- Composer cache with `actions/cache@v4` for faster runs
---
## 9. .editorconfig
```ini
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.php]
indent_style = space
indent_size = 4
[*.{yaml,yml,json,js,ts,css}]
indent_style = space
indent_size = 2
[*.xml]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
```
---
## 10. README Badges
```markdown
[](https://github.com/VENDOR/BUNDLE/actions)
[](https://packagist.org/packages/vendor/bundle)
[](https://phpstan.org/)
```
---
## 11. Anti-Patterns
| Anti-Pattern | Impact |
|---|---|
| **`qossmic/deptrac` instead of `deptrac/deptrac`** | Abandoned package, no longer maintained |
| `phpstan.neon` instead of `.dist` | Overridden by local config, inconsistent CI |
| `@phpstan-ignore` without comment | Masks real problems |
| Using `phpstan-baseline.neon` | Hides errors instead of fixing them |
| MSI < 50% | Tests don't actually verify behavior |
| Ignoring deprecations until upgrade | Debt accumulation |
| Committing code failing `make quality` | Broken CI for all contributors |
| No `composer audit` in CI | Vulnerable dependencies shipped |
| No Deptrac | Architecture erodes over time |
| `composer_audit` in `grumphp.yml` | Invalid task, breaks pre-commit hooks |
This skill covers the complete quality assurance pipeline for distributable Symfony bundles.
.PHONY: help install test phpstan cs-fix cs-check infection deptrac quality security-check lint ci
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
install: ## Install dependencies
composer install
test: ## Run PHPUnit tests
vendor/bin/phpunit
phpstan: ## Static analysis (level 9)
vendor/bin/phpstan analyse --memory-limit=512M
cs-fix: ## Fix code style
vendor/bin/php-cs-fixer fix
cs-check: ## Check code style (no fix)
vendor/bin/php-cs-fixer fix --dry-run --diff
infection: ## Mutation testing
vendor/bin/infection --only-covered --show-mutations --threads=max --min-msi=70
deptrac: ## Architecture enforcement
vendor/bin/deptrac analyse
security-check: ## Dependency security audit
composer audit
lint: ## Lint config files
@test -f bin/console && php bin/console lint:yaml config/ --parse-tags 2>/dev/null || true
@test -f bin/console && php bin/console lint:xliff translations/ 2>/dev/null || true
@test -d config && find config -name '*.xml' -exec xmllint --noout {} + 2>/dev/null || true
quality: cs-check phpstan deptrac lint test infection ## Full quality pipeline
ci: security-check quality ## Full CI pipeline (security + quality)
Always use make quality after every significant change. Never commit code that fails.
phpstan.neon.dist)parameters:
level: 9
paths:
- src/
tmpDir: .phpstan-cache
phpstan-baseline.neon) to ignore errors — fix them insteadvoid.mixed except justified with a comment explaining why.@phpstan-ignore without an explanatory comment./** @var Collection<int, Entity> */@return Entity[] or exact union type, never mixed[]make phpstan.php-cs-fixer.dist.php)<?php
declare(strict_types=1);
$finder = (new PhpCsFixer\Finder())
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@PHP82Migration' => true,
'declare_strict_types' => true,
'native_function_invocation' => [
'include' => ['@compiler_optimized'],
'scope' => 'namespaced',
'strict' => true,
],
'trailing_comma_in_multiline' => [
'elements' => ['arguments', 'parameters', 'match', 'arrays'],
],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'global_namespace_import' => [
'import_classes' => true,
'import_functions' => false,
'import_constants' => false,
],
])
->setFinder($finder)
->setRiskyAllowed(true)
;
After every change: make cs-fix
grumphp.yml)grumphp:
tasks:
make:
task: deptrac
triggered_by: [php]
phpstan:
configuration: phpstan.neon.dist
memory_limit: 512M
use_grumphp_paths: false
phpcsfixer:
config: .php-cs-fixer.dist.php
phpunit:
config_file: phpunit.xml.dist
infection:
min_covered_msi: 65
only_covered: true
threads: 4
test_framework: phpunit
Pre-commit hooks prevent non-conforming commits.
Deptrac via
maketask: Use the GrumPHPmaketask withtask: deptracto delegate to the Makefile target. This avoids needing a separate shell script.
Note:
composer_auditis NOT a valid GrumPHP task — do not add it. Runmake security-checkseparately.
infection.json5){
"$schema": "https://raw.githubusercontent.com/infection/infection/0.29.0/resources/schema.json",
"source": { "directories": ["src/"] },
"logs": {
"text": "infection.log",
"summary": "infection-summary.log"
},
"minMsi": 70,
"minCoveredMsi": 80,
"threads": "max",
"testFramework": "phpunit",
"testFrameworkOptions": "--testsuite=Unit"
}
Run: make infection (requires xdebug or pcov for coverage).
NEVER use
qossmic/deptrac— it is abandoned. Always installdeptrac/deptrac. If you seeqossmic/deptracanywhere (composer.json, Makefile, grumphp.yml, docs), replace it immediately.
deptrac.yaml)deptrac:
paths:
- ./src
layers:
- name: Contract
collectors:
- type: directory
value: src/Contract/.*
- name: Entity
collectors:
- type: directory
value: src/Entity/.*
- name: Event
collectors:
- type: directory
value: src/Event/.*
- name: Repository
collectors:
- type: directory
value: src/Repository/.*
- name: Service
collectors:
- type: directory
value: src/Service/.*
- name: Controller
collectors:
- type: directory
value: src/Controller/.*
- name: DependencyInjection
collectors:
- type: directory
value: src/DependencyInjection/.*
ruleset:
Contract: []
Entity: [Contract]
Event: [Contract, Entity]
Repository: [Contract, Entity]
Service: [Contract, Entity, Repository, Event]
Controller: [Contract, Service, Entity]
DependencyInjection: [Contract, Service, Entity, Repository, Event, Controller]
Rules: Entity depends on nothing (or Contract). Service never imports Controller. Contract imports nothing internal.
composer audit in Makefile, CI, and GrumPHP. Replaces the deprecated symfony check:security.
Default template — single ci job running make ci over the full PHP × Symfony matrix:
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
ci:
name: PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4']
symfony: ['7.1.*', '7.2.*', '8.0.*']
exclude:
- { php: '8.2', symfony: '8.0.*' }
- { php: '8.3', symfony: '8.0.*' }
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: intl, mbstring, xml, dom
coverage: none
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer update --no-progress --prefer-dist
- name: Install Symfony version
run: composer require "symfony/framework-bundle:${{ matrix.symfony }}" --no-update
- name: Update with Symfony constraint
run: composer update symfony/* --no-progress --prefer-dist
- name: Run CI pipeline
run: make ci
Key points:
fail-fast: false so all matrix combinations always reportmake ci covers: composer audit + cs-check + phpstan + deptrac + lint + test + infectionactions/cache@v4 for faster runsroot = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.php]
indent_style = space
indent_size = 4
[*.{yaml,yml,json,js,ts,css}]
indent_style = space
indent_size = 2
[*.xml]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
[](https://github.com/VENDOR/BUNDLE/actions)
[](https://packagist.org/packages/vendor/bundle)
[](https://phpstan.org/)
| Anti-Pattern | Impact |
|---|---|
qossmic/deptrac instead of deptrac/deptrac |
Abandoned package, no longer maintained |
phpstan.neon instead of .dist |
Overridden by local config, inconsistent CI |
@phpstan-ignore without comment |
Masks real problems |
Using phpstan-baseline.neon |
Hides errors instead of fixing them |
| MSI < 50% | Tests don't actually verify behavior |
| Ignoring deprecations until upgrade | Debt accumulation |
Committing code failing make quality |
Broken CI for all contributors |
No composer audit in CI |
Vulnerable dependencies shipped |
| No Deptrac | Architecture erodes over time |
composer_audit in grumphp.yml |
Invalid task, breaks pre-commit hooks |
How can I help you explore Laravel packages today?