oxidmod/rest-bundle
oxidmod/rest-bundle is a Symfony bundle that simplifies building REST APIs, offering reusable controllers, serialization helpers, and request handling utilities so you can expose clean, consistent JSON endpoints with less boilerplate.
Install and publish
composer require --dev vendor/package:0.2.4
php artisan vendor:publish --tag=package-config
Pin the exact version — 0.2.x is pre-1.0, so config keys and CLI flags can shift between minors.
Where to look first
Mutation testing section of the README and the 0.2.4 changelog entry — this is the only behaviour change in this release.phpunit.xml / pest.php — the feature depends on a working coverage driver, so confirm coverage runs before touching mutation config.First use case (15 minutes)
Run mutation testing against a single, already well-covered class rather than the whole app:
php artisan package:mutation --class="App\\Services\\InvoiceCalculator"
# or the underlying binary if the package only ships a wrapper
vendor/bin/infection --filter=InvoiceCalculator --threads=max
Read two numbers: MSI (all mutants killed) and Covered MSI (mutants in covered code killed). Covered MSI is the one that tells you about assertion quality; plain MSI is dragged down by untested files and is not actionable on day one.
Do not wire this into the default composer test script. It is a separate, slow, opt-in command.
Local inner loop — one class at a time
After writing or changing a service class, run mutation testing scoped to that class with --filter (or --git-diff-lines if supported). Full-suite mutation runs are a PR/CI activity; per-class runs are the day-to-day use.
Coverage → mutation → assertion ratchet The practical workflow for new code:
php artisan test --coverage --min=100 for the happy path.return).--id=<mutant-id> to confirm the single kill without waiting for the full class again.Coverage proves lines execute; mutation proves the assertions would fail if the logic changed. Treat step 4 as the actual review of the test, not the implementation.
Triage escaped mutants by category
CI integration
Give mutation testing its own job — never inline it with the unit-test job:
mutation:
needs: [tests]
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@v2
with: { php: 8.3, coverage: pcov }
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/infection --threads=max --git-diff-lines --logger-github
Start with --git-diff-lines / changed-files scoping so the job stays under a few minutes, then widen as the baseline stabilises. Publish the log as a build artifact; the HTML/JSON report is what people actually read when a mutant survives.
Gate by ratchet, not by absolute score
Set minMsi slightly below your current measured value and bump it as you fix survivors. A hard 100% gate on day one gets disabled within a week.
Config layering
Keep mutation config environment-aware: strict thresholds locally, lenient-by-default but diff-scoped in CI, and fully disabled in the deployed app (it's a require-dev tool; verify nothing in the service provider registers routes, migrations, or commands that ship to production).
Mutation testing needs coverage, and coverage needs a driver. PCOV is dramatically faster than Xdebug for this — if mutation runs take 20+ minutes, check xdebug.mode before anything else. Missing/incorrect coverage config produces mutants that are never killed, which looks like a test problem but isn't.
config:cache in CI will bite you. If the package reads settings through Laravel's config repository and your CI runs php artisan config:cache before the mutation step, env-driven thresholds or paths can silently revert to defaults. Run config:clear in that job, or bypass config and use the CLI flags.
Timeouts masquerade as kills. Slow tests (database, HTTP fakes that actually sleep) can time out under parallel workers, and some tools count a timeout as killed. Set an explicit per-test timeout and check the report for a timedOut count — a nonzero value means your MSI is inflated.
Don't chase 100% MSI. Getters, framework glue, __toString, and defensive ?? fallbacks generate mutants that no reasonable test kills. Exclude them in config (ignoreSource / ignore patterns) and accept a covered-MSI target in the 80–90% band for domain code.
Equivalent mutants are permanent. Mark them explicitly with an inline ignore annotation (e.g. @infection-ignore-all) or a config ignore list, with a one-line comment explaining why. Otherwise every future run re-surfaces them and people learn to ignore the report entirely.
Mocks can cause false survivors. A Facade mock returning a fixed value means mutants inside the real collaborator are never exercised. Prefer fakes/real implementations for the units you mutate; save mocks for genuinely external boundaries.
Pre-commit hooks are the wrong place. Full mutation runs belong on PRs. If you want local protection, restrict the hook to changed files with a hard timeout and fail open (warn, don't block).
Pre-1.0 upgrade discipline. 0.2.4 is additive (mutation testing), so no breakage — but treat each 0.2.x bump as potentially config-changing: read the changelog, re-run the publish diff (vendor:publish --force into a scratch dir) and diff the config against yours.
Extension points to hook into before hacking the package. Most mutation tooling exposes custom mutators, custom test-framework adapters, and event/report listeners. If you need project-specific mutant suppression (e.g. generated code, Illuminate attributes), use the ignore-list config or a custom mutator set rather than forking — you'll want those upgrades.
Debugging a suspiciously high score. Run with a single thread and verbose output on one class, then confirm mutants are actually being generated against the file you think — an incorrect source.directories path results in "no mutants" and a vacuous pass.
How can I help you explore Laravel packages today?