Skip to content

Drift check

The generated Data classes, abstract controllers, and routes file are deterministic: the same spec in produces byte-identical files out. openapi:check, the drift gate, turns that property into an enforceable guarantee. It computes exactly what openapi:generate would write, then compares it against the files on disk. If anything is missing or different, the command fails. Generate and check share one planner internally, so they are always in lockstep.

This is the enforcement mechanism behind the project’s promise that spec/code drift is structurally impossible. Generation makes drift easy to remove; the drift check makes drift impossible to merge.

A generator only prevents drift if someone reruns it. openapi:check removes the “if”. Run it in CI on every pull request and a stale Data class, a hand-edited routes file, or a forgotten regenerate all fail the build before they reach main.

openapi.yaml ──► openapi:generate ──► files written to disk (you commit these)
openapi.yaml ──► openapi:check ──► compares plan vs disk (CI gate, writes nothing)

openapi:check never writes anything. It plans the same file set openapi:generate would produce, in memory, and compares each expected file byte-for-byte against the file currently on disk.

openapi:check only compares generator-owned files:

  • the generated Data classes and enums,
  • the abstract controllers (Abstract*Controller.php), on by default, skipped with --no-controllers,
  • the generated routes file, on by default, skipped with --no-routes.

It never inspects your concrete controllers (hand-written or scaffolded once with openapi:scaffold) or any unrelated file. A file is reported as drifted when it is either:

  • missing, no file exists at the path the generator would write, or
  • changed, a file exists but its content differs from the freshly generated content.

Stale-file detection (files on disk that the generator would no longer produce) is out of scope: the check focuses on the expected file set.

openapi:check honours the same config and the same flags as openapi:generate (--spec, --output, --namespace, --no-controllers, --no-routes), with the same precedence rules, so it always checks exactly what generate would produce: models, controllers, and routes by default.

Terminal window
# Full check: models, controllers, routes (uses config/openapi-laravel.php)
php artisan openapi:check
# Override the spec and output, and check the models only
php artisan openapi:check \
--spec=openapi.yaml \
--output=app/Data \
--no-controllers \
--no-routes

When everything matches, the command prints:

Generated code is in sync with the spec.

When something has drifted, it lists each affected file with a status tag:

Drift detected in 2 file(s):
[changed] app/Data/Pet/PetData.php
[missing] app/Data/TagData.php

The exit code is what makes the command a CI gate.

CodeMeaning
0Every generator-owned file on disk matches the spec. In sync.
1Drift detected. One or more files are missing or changed.
2Configuration or spec error (no spec configured, an unparseable spec, a missing output path, an illegal namespace, conflicting flags such as --controllers --no-controllers).

Exit code 2 is distinct on purpose: a broken spec or misconfiguration is a different failure than genuine drift, and CI logs make that distinction clear.

By default the command lists the drifted files. Add --diff to print a concise unified diff (expected versus on-disk) for each changed file, so you can see precisely what regenerating would do.

Terminal window
php artisan openapi:check --diff
Drift detected in 1 file(s):
[changed] app/Data/Pet/PetData.php
public readonly int $id,
-public readonly string $name,
+public readonly ?string $name,
public readonly ?string $tag,

Lines prefixed with - are what the generator would write; lines prefixed with + are what is currently on disk. The diff is line-based and bounded, so a wholesale rewrite will not flood your terminal. Missing files are not diffed (there is nothing on disk to compare).

Add a step that regenerates nothing and simply checks. If the spec and the committed code have drifted, the step exits non-zero and fails the build.

.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
drift-check:
name: Spec/code drift
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-interaction --no-progress
- name: Verify generated code is in sync with the spec
run: php artisan openapi:check

The standalone binary works the same way for non-Laravel projects:

- name: Verify generated code is in sync with the spec
run: |
vendor/bin/openapi-laravel check \
--spec=openapi.yaml \
--output=src/Data

Code generators tend to rot: someone edits a generated file “just this once”, or changes the spec and forgets to regenerate, and the committed code quietly stops matching the contract. openapi:check closes that gap. Combined with deterministic generation, it makes the spec the single enforceable source of truth: the only way to change the generated code is to change the spec and regenerate, and CI proves it.

openapi:check compares all generator-owned files, including openapi-laravel.unsupported.json (the fidelity report). That file lists every OpenAPI construct in your spec the generator cannot faithfully represent. Because it is deterministic and byte-compared like the Data classes, a spec change that changes the gap list shows up as drift, surfacing the impact at review time.

See Unsupported constructs report for the file shape, the list of recorded constructs, and how to opt out.