OpenAPI drift detection and version analysis for PHP. Compares your live API routes against their spec, recommends semver bumps, and generates changelogs.
Part of the Fissible suite. Depends on: fissible/accord (reads specs via SpecSourceInterface).
[forge] ──────────────────────────────► [accord] ◄── [watch] ◄── [fault]
generate spec validate at cockpit UI exception
▲ runtime │ (bolt-on) tracking
│ ▼
└────────────────────────────────── [drift] ← you are here
detect drift, bump version
As an API evolves, its spec and its actual routes can quietly fall out of sync. A route gets added but never documented. A route gets removed but the spec still describes it. Clients that rely on the spec to understand your API get a false picture of what's actually available.
drift surfaces that gap before it causes problems. It compares the routes your application actually serves against the routes your OpenAPI spec describes, tells you exactly what has been added or removed, and recommends how to version the change — so clients always know what to expect.
- PHP ^8.2
- OpenAPI 3.0.x spec files (YAML or JSON)
- fissible/accord ^1.0
composer require fissible/driftThe service provider registers automatically. Console commands are registered with Artisan:
php artisan accord:validate
php artisan accord:version
php artisan drift:coverageDrift enumerates your application's routes via a RouteInspectorInterface implementation and compares them against the paths defined in your OpenAPI spec. Each route is normalised to a canonical form (GET /v1/users/{param}) so that parameter names don't cause false positives.
The result is a DriftReport describing:
- Matched routes — present in both the app and the spec
- Added routes — live in the app but not yet documented
- Removed routes — in the spec but no longer served by the app
From that report, drift recommends a semver bump and generates a changelog entry.
Checks for drift between your live routes and your OpenAPI spec:
php artisan accord:validate
php artisan accord:validate --api-version=v2Output is a table showing each route's status. Exits with a non-zero code if any drift is detected — useful in CI pipelines to catch undocumented or removed routes before they ship.
Version Method Path Status
v1 GET /v1/users PASS
v1 POST /v1/users PASS
v1 GET /v1/users/{id} WARN (undocumented — not in spec)
v1 DELETE /v1/orders/{id} FAIL (removed — in spec but not routed)
Runs the full drift-analyse-changelog pipeline:
php artisan accord:version
php artisan accord:version --api-version=v1 --dry-run
php artisan accord:version --yes # skip confirmation prompt- Detects drift for the given version
- Reads the current
info.versionfrom the spec - Recommends a semver bump (major / minor / patch / none)
- Confirms with you before writing any changes
- Updates the spec's
info.versionin place - Prepends a changelog entry to
CHANGELOG.md
When drift introduces breaking changes (removed routes), the command also notes that a new URI version (/v2/) should be considered.
Checks that every registered route resolves to an existing controller class and method:
php artisan drift:coverage
php artisan drift:coverage --api-version=v1Output is a table showing each route's implementation status. Exits with a non-zero code if any routes are unimplemented, making it suitable for CI.
Coverage Method Path Action
IMPLEMENTED GET /api/v1/posts App\Http\Controllers\V1\PostController@index
IMPLEMENTED POST /api/v1/posts App\Http\Controllers\V1\PostController@store
MISSING DELETE /api/v1/posts/{id} App\Http\Controllers\V1\PostController@destroy
UNKNOWN GET /api/v1/ping (closure)
- IMPLEMENTED — controller class and method both exist
- MISSING — class or method cannot be found; the route would throw a server error if called
- UNKNOWN — route uses a closure or has no resolvable action string
The bundled LaravelRouteInspector enumerates routes in your application's api middleware group, skipping HEAD routes:
// Registered automatically by DriftServiceProvider
use Fissible\Drift\Drivers\Laravel\Inspectors\LaravelRouteInspector;To filter routes differently, bind your own RouteInspectorInterface implementation in a service provider:
$this->app->singleton(RouteInspectorInterface::class, function () {
return new LaravelRouteInspector(
router: $this->app['router'],
filter: fn($route) => str_starts_with($route->uri, 'api/'),
);
});use Fissible\Drift\DriftDetector;
use Fissible\Accord\FileSpecSource;
$source = new FileSpecSource('/var/www/app');
$detector = new DriftDetector($source);
$report = $detector->detect($routes, 'v1');
$report->isClean(); // true if no drift
$report->hasBreakingChanges(); // true if routes were removed
$report->hasAdditiveChanges(); // true if routes were added
$report->summary(); // human-readable stringuse Fissible\Drift\VersionAnalyser;
$analyser = new VersionAnalyser($source);
$recommendation = $analyser->analyse($report);
$recommendation->bumpType; // 'major' | 'minor' | 'patch' | 'none'
$recommendation->recommendedVersion; // '1.2.0'
$recommendation->requiresNewUriVersion; // true when breaking changes are present
$recommendation->label(); // '1.1.0 → 1.2.0 (minor)'use Fissible\Drift\ChangelogGenerator;
$generator = new ChangelogGenerator();
$entry = $generator->generate($report, $recommendation);
// Prepend the entry to CHANGELOG.md (creates the file if missing)
$generator->prepend($entry, base_path('CHANGELOG.md'));Implement RouteInspectorInterface to enumerate routes from any framework:
use Fissible\Drift\RouteInspectorInterface;
use Fissible\Drift\RouteDefinition;
class MyFrameworkInspector implements RouteInspectorInterface
{
public function getRoutes(): array
{
return array_map(
fn($route) => new RouteDefinition($route->method, $route->path),
$this->router->getRoutes(),
);
}
}RouteDefinition normalises parameter syntax automatically — :id, {id}, and <id> all resolve to the same canonical path for comparison.
Add both commands to your CI pipeline:
# .github/workflows/ci.yml
- name: Check API drift
run: php artisan accord:validate
- name: Check for unimplemented routes
run: php artisan drift:coverageaccord:validate fails the build if any routes are undocumented or have been removed from the spec without a version bump. drift:coverage fails the build if any route's controller or method is missing — catching spec-first development gaps before they reach production.
MIT