Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Development

Enter the reproducible development environment and install ordinary mutable Composer dependencies for interactive work:

nix develop
composer install

Before submitting a change, run the complete normal validation suite:

nix flake check --keep-going -L

That command runs the supported-PHP PHPUnit matrix, PHPStan, php-cs-fixer, Composer and PHP linting, documentation formatting, the mdBook build and link check, Larastan and minimum-PHPStan consumer checks, and the pinned Laravel runtime audits. It does not run mutation testing.

Focused Composer commands remain useful while developing:

composer exec phpunit
composer exec phpstan analyse
composer cs
composer validate --strict

Choose a test layer with Testing and Runtime Verification.

Documentation

Build and check the mdBook:

composer docs
composer docs:check
composer docs:serve

composer docs:check builds the book and fails on broken same-site relative links in the generated HTML.

The sidebar keeps every page’s h2/h3 outline open. mdBook only injects headings for the active page, so docs/theme/phpstan-laravel-validation.js adds the remaining outlines from headingsByChapter. Update that map when public headings change.

The optional Heliogenesis control is mounted from docs/theme/phpstan-laravel-validation.js. The unmodified Doctrine runtime lives under docs/pages/assets/heliogenesis/. The theme marks the reading plane so the event can light the article and run document tomography.

The Document Looks Back integration is mounted separately from docs/pages/assets/document-looks-back/. After it mounts, window.documentLooksBack is the Doctrine controller, so window.documentLooksBack.summon() requests one immediate eye. A mount or renderer failure of either integration leaves the documentation usable.

The copied-runtime tests live in the documentation PHPUnit group. They compare those assets to the root Composer Doctrine pin. Laravel-matrix and minimum-PHPStan Nix jobs exclude the group because those lockfiles do not install that pin.

Akashi formats inline PHP fences in the README, changelog, and docs/:

composer docs:format
composer docs:format:fix

composer cs:fix formats PHP source and those documentation fences.

Akashi checks formatting. It does not execute illustrative fragments as standalone programs.

The published site is https://jbboehr.github.io/phpstan-laravel-validation/. .github/workflows/pages.yml builds the book on develop and master, and deploys only from master.

Mutation testing

Mutation testing uses an isolated toolchain because Infection requires PHP 8.3 or newer while this package supports PHP 8.1. It is excluded from nix flake check. Run it explicitly:

nix build -L .#mutation

The package supplies PHP 8.5 with PCOV and preserves the thresholds, timeouts, test exclusions, and worker count from infection.json5.dist. It divides the source into five cached shard derivations, each using four Infection workers, then aggregates the project-wide thresholds. The exhaustive GitHub workflow schedules those derivations as independent jobs and checks their uploaded summaries in a final aggregate job. Each runner therefore uses at most four Infection workers while the five shards can progress in parallel. The aggregate JSON and a failed timeout-budget check identify each timed-out mutant by shard, source location, mutator, and Infection ID.

PHPStan type-inference fixtures that cover extension code must run their first gatherAssertTypes() analysis inside the test body. Data providers run before PHPUnit starts coverage and can warm PHPStan’s process-level caches. The test-only AssertsFixtureUnderCoverage trait implements this pattern. Infection runs only the individual test cases that cover each mutant.

Tests in the subprocess group remain in the normal suite but are excluded from mutation testing because child processes cannot observe the active in-process mutant. The property group is also excluded: rerunning the complete finite catalogs for each mutant would be disproportionate. Promoted deterministic regressions remain available to Infection.

The Infection configuration also ignores mutations that make RuleTreeNode::resolvePath() recurse without consuming input. Those mutants can exhaust PHP’s native stack before Infection’s timeout can stop the process.

The php85 Nix development shell includes PCOV for focused manual investigation.

Nix dependency hashes

Nix builds offline Composer repositories from committed lockfiles. When Composer dependencies change, update the hashes in nix/vendor-hashes.nix as described in CONTRIBUTING.md.

CI

.github/workflows/ci.yml has two surfaces:

  • a conventional PHP baseline job on PHP 8.5 (Composer, PHPUnit, PHPStan, php-cs-fixer);
  • an exhaustive Nix matrix generated from flake checks, plus mutation.

The conventional job uploads PHPUnit’s JUnit XML even when the test step fails, preserving per-test outcomes and timings for diagnosis and feedback-loop audits.

Nix matrix jobs use the daemon’s default build directory under /nix/var/nix/builds. A directory under the runner’s temporary directory can be inaccessible to Nix build users and fail before the check starts. The report collector runs with sudo on the hosted Ubuntu runner so it can recover reports from daemon-owned failed-build directories as well as successful outputs. PHPUnit and mutation jobs retain failed build directories. Mutation jobs upload infection.log, infection-summary.json, and infection-summary.log when available, including after a timeout-threshold failure. These artifacts preserve mutant identities and diagnostics for reproduction; an incomplete report still causes aggregation to fail.

Before the Nix matrix fans out, one job builds every pinned Composer vendor closure and saves the resulting Nix store under a derivation-specific cache key. Every matrix job requires an exact cache restore. This keeps the jobs independent without making each runner download the same Composer archives.

A newer run supersedes an older run for the same branch or pull-request ref. Push and pull-request refs remain separate because they validate different Git states.

A documentation failure is a flake-check failure. It is not silent.

Downstream investigations

Pinned application investigations live under docs/development/. They are evidence for specific experiments, not user-facing support promises.