php artisan boost:add-skill symkit/form-bundle
Save this content to: .cursor/rules/bundle-quality.mdc
# Bundle Quality Tooling
## Makefile First
The Makefile is the SINGLE entry point for all quality operations.
Never call `vendor/bin/` directly — always use `make` targets:
- `make cs-fix` — fix code style
- `make cs-check` — check without fixing
- `make phpstan` — static analysis level 9
- `make test` — run PHPUnit
- `make deptrac` — architecture enforcement
- `make lint` — `composer validate --strict`
- `make infection` — mutation testing
- `make security-check` — `composer audit --abandoned=report`
- `make quality` — full pipeline (cs-check + phpstan + deptrac + lint + test + infection)
- `make ci` — security-check + quality
Never commit code that fails `make quality`.
## Commits (no self-attribution)
Do not add Co-authored-by, Signed-off-by from the AI, or `git commit --trailer`.
## Cleanup — Legacy commit-msg hook
If the project contains `scripts/git-hooks/commit-msg`, an `install-hooks` target in the Makefile, or `make install-hooks` references in README or docs, remove them. These artefacts are obsolete. See symfony-bundle.mdc § Cleanup.
## PHPStan Level 9
- **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
- No `@phpstan-ignore` without explanatory comment
- No `mixed` without justification
- Typed collections: `/** @var Collection<int, Entity> */`
- Typed repository returns: `@return Entity[]`
- `@param array{key: type}` for shaped arrays (especially `loadExtension()` config)
## PHP-CS-Fixer
- `@Symfony` + `@PHP82Migration`
- `declare_strict_types` enforced
- `native_function_invocation` with `@compiler_optimized`
- `trailing_comma_in_multiline` on arguments, parameters, match, arrays
- `ordered_imports` alphabetical
- Always use `.php-cs-fixer.dist.php` (not `.php-cs-fixer.php`)
## GrumPHP
Pre-commit hooks must mirror CI (except security): phpstan, phpcsfixer, phpunit, deptrac, infection. Same thresholds as Makefile (e.g. infection `min_covered_msi: 65`). This way a commit that would fail CI (e.g. 0% MSI) is blocked locally.
Note: `composer_audit` is NOT a valid GrumPHP task — use `make security-check` separately.
### Base 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
shell:
scripts:
- ['-c', 'make infection']
triggered_by: [php]
```
> **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.
## Composer allow-plugins
Always configure in `composer.json`:
- `infection/extension-installer: true`
- `phpro/grumphp: true`
## Deptrac
> **NEVER use `qossmic/deptrac`** — it is abandoned. Always use `deptrac/deptrac`.
- Entity depends on nothing (or Contract)
- Service never imports Controller
- Contract imports nothing internal
- Controller goes through Service, not Repository directly
## Infection
- MSI >= 70% (or 65% with documented justification, e.g. mutants escaping scope such as prependExtension path)
- `--only-covered`, threads max
- Always run after `make test`
## CI (GitHub Actions)
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
```
Rules:
- `fail-fast: false` so all matrix combinations are always reported
- PHP 8.4 + Symfony 8.0 is the only valid combination for Symfony 8 (exclude 8.2 and 8.3)
- Symfony deps in `composer.json`: always `^7.0 || ^8.0`
- `make ci` covers: `composer audit` + cs-check + phpstan + deptrac + lint + test + infection
## .gitignore essentials
```
/vendor/
composer.lock
.phpstan-cache/
.php-cs-fixer.cache
.deptrac.cache
.phpunit.result.cache
infection.log
infection-summary.log
phpunit.xml
phpstan.neon
config/reference.php
*.log
.agent/
.claude/
```
## Dependencies
- Deptrac: use `deptrac/deptrac` (not the abandoned `qossmic/deptrac`)
- Always use `composer audit --abandoned=report` so abandoned packages are reported but only security vulnerabilities fail the build
## Anti-Patterns
- Calling `vendor/bin/` directly instead of Makefile targets
- `phpstan.neon` instead of `.dist` (overridden locally)
- MSI < 50% (tests don't verify behavior)
- Ignoring deprecations
- No `composer audit` in CI
- No Deptrac (architecture erodes)
- `composer_audit` in grumphp.yml (task does not exist)
- `composer audit` without `--abandoned=report` (fails CI on abandoned transitive deps)
- Using `qossmic/deptrac` instead of `deptrac/deptrac` (abandoned)
- Using `phpstan-baseline.neon` to ignore errors instead of fixing them
The Makefile is the SINGLE entry point for all quality operations.
Never call vendor/bin/ directly — always use make targets:
make cs-fix — fix code stylemake cs-check — check without fixingmake phpstan — static analysis level 9make test — run PHPUnitmake deptrac — architecture enforcementmake lint — composer validate --strictmake infection — mutation testingmake security-check — composer audit --abandoned=reportmake quality — full pipeline (cs-check + phpstan + deptrac + lint + test + infection)make ci — security-check + quality
Never commit code that fails make quality.Do not add Co-authored-by, Signed-off-by from the AI, or git commit --trailer.
If the project contains scripts/git-hooks/commit-msg, an install-hooks target in the Makefile, or make install-hooks references in README or docs, remove them. These artefacts are obsolete. See symfony-bundle.mdc § Cleanup.
phpstan-baseline.neon) to ignore errors — fix them instead@phpstan-ignore without explanatory commentmixed without justification/** @var Collection<int, Entity> */@return Entity[]@param array{key: type} for shaped arrays (especially loadExtension() config)@Symfony + @PHP82Migrationdeclare_strict_types enforcednative_function_invocation with @compiler_optimizedtrailing_comma_in_multiline on arguments, parameters, match, arraysordered_imports alphabetical.php-cs-fixer.dist.php (not .php-cs-fixer.php)Pre-commit hooks must mirror CI (except security): phpstan, phpcsfixer, phpunit, deptrac, infection. Same thresholds as Makefile (e.g. infection min_covered_msi: 65). This way a commit that would fail CI (e.g. 0% MSI) is blocked locally.
Note: composer_audit is NOT a valid GrumPHP task — use make security-check separately.
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
shell:
scripts:
- ['-c', 'make infection']
triggered_by: [php]
Deptrac via
maketask: Use the GrumPHPmaketask withtask: deptracto delegate to the Makefile target. This avoids needing a separate shell script.
Always configure in composer.json:
infection/extension-installer: truephpro/grumphp: trueNEVER use
qossmic/deptrac— it is abandoned. Always usedeptrac/deptrac.
--only-covered, threads maxmake testDefault 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
Rules:
fail-fast: false so all matrix combinations are always reportedcomposer.json: always ^7.0 || ^8.0make ci covers: composer audit + cs-check + phpstan + deptrac + lint + test + infection/vendor/
composer.lock
.phpstan-cache/
.php-cs-fixer.cache
.deptrac.cache
.phpunit.result.cache
infection.log
infection-summary.log
phpunit.xml
phpstan.neon
config/reference.php
*.log
.agent/
.claude/
deptrac/deptrac (not the abandoned qossmic/deptrac)composer audit --abandoned=report so abandoned packages are reported but only security vulnerabilities fail the buildvendor/bin/ directly instead of Makefile targetsphpstan.neon instead of .dist (overridden locally)composer audit in CIcomposer_audit in grumphp.yml (task does not exist)composer audit without --abandoned=report (fails CI on abandoned transitive deps)qossmic/deptrac instead of deptrac/deptrac (abandoned)phpstan-baseline.neon to ignore errors instead of fixing themHow can I help you explore Laravel packages today?