fissible / accord
OpenAPI contract validation for PHP — PSR-7/15 core with Laravel, Slim, and Mezzio drivers
Requires
- php: ^8.2
- cebe/php-openapi: ^1.7
- nyholm/psr7: ^1.8
- psr/http-message: ^1.1|^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^2.0|^3.0
- psr/simple-cache: ^2.0|^3.0
- symfony/psr-http-message-bridge: ^2.0|^7.0
- symfony/yaml: ^6.0|^7.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- illuminate/http: ^10.0|^11.0|^12.0 — required for the Laravel driver
README
OpenAPI contract validation for PHP. PSR-7/15 core with first-party drivers for Laravel, Slim, and Mezzio.
Start here. accord is the foundation of the Fissible suite — the other packages build on top of it.
The Fissible suite
Fissible is a family of focused PHP packages for keeping your API and its documentation honest with each other.
[forge] ──────────────────────────────► [accord] ◄── [watch] ◄── [fault]
generate / update spec validate at cockpit UI exception
▲ runtime │ (bolt-on) tracking
│ ▼
└────────────────────────────────── [drift]
detect drift, bump version
fissible/forge
Scaffolds an OpenAPI spec from your existing routes, inferring request body schemas from your FormRequest validation rules. Use it to get started with a spec, and again whenever a new API version needs documenting.
Depends on: nothing from the suite (standalone spec generator)
fissible/accord ← you are here
The runtime enforcer. Validates every request and response against the spec in real time. Lives in your application permanently — the spec it validates against evolves, but accord itself stays put.
Depends on: nothing from the suite (foundation package)
fissible/drift
Detects when the routes your application actually serves have drifted from what the spec describes. Recommends a semver bump, generates a changelog entry, and closes the loop — signalling that it's time to update the spec.
Depends on: accord (reads specs via SpecSourceInterface)
fissible/watch
A Telescope-style bolt-on that mounts a live cockpit dashboard, route browser, drift detector, spec manager, version tracker, and API explorer (Trace) at /watch in any existing Laravel application.
Depends on: accord + drift + forge (requires all three)
fissible/fault
Exception tracking and triage for the watch cockpit. Captures exceptions via the Laravel exception handler, deduplicates them by fingerprint, and surfaces them in the /watch/faults UI with status management, developer notes, and regression test generation.
Depends on: watch
Integrating with an existing Laravel API
accord is the only package you need to install for runtime validation. forge and drift are optional companions you can add as your needs grow.
Step 1 — Install accord
composer require fissible/accord
The service provider registers automatically via Laravel's package discovery.
Step 2 — Get a spec
Don't have a spec yet? scaffold one from your existing routes with fissible/forge:
composer require --dev fissible/forge
php artisan accord:generate --title="My API"
This writes resources/openapi/v1.yaml with every route documented and request body schemas inferred from your FormRequest classes. Response schemas are scaffolded as empty objects — you fill those in to describe what your API actually returns.
Already have a spec? Drop it at resources/openapi/v1.yaml (or configure a different path — see Spec files below).
Step 3 — Register the middleware
Add the middleware to your API route group. For a new Laravel 11+ app, the cleanest place is bootstrap/app.php:
use Fissible\Accord\Drivers\Laravel\Http\Middleware\ValidateApiContract; ->withMiddleware(function (Middleware $middleware) { $middleware->appendToGroup('api', ValidateApiContract::class); })
Or scope it to a specific route group in routes/api.php:
Route::middleware(ValidateApiContract::class)->group(function () { require __DIR__ . '/api/v1.php'; });
Step 4 — Choose a failure mode for adoption
If you're adopting accord on an API that has been running without a spec, start with log mode so violations surface as warnings without breaking anything:
ACCORD_FAILURE_MODE=log
Review the logged violations, fix the gaps in your spec (or your API), then switch to exception once you're confident the spec reflects reality:
ACCORD_FAILURE_MODE=exception
See Failure modes for the full list of options.
Step 5 — Lock it in with drift detection
Add fissible/drift so that future route changes are caught before they reach production:
composer require --dev fissible/drift
php artisan accord:validate # check for drift locally
Then add accord:validate to your CI pipeline — see CI / CD below.
CI / CD
accord:validate exits with a non-zero status code when drift is detected, making it a natural CI gate. Add it alongside your test suite to catch undocumented route changes before they merge.
GitHub Actions
name: API contract on: [push, pull_request] jobs: contract: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: pdo, pdo_sqlite - name: Install dependencies run: composer install --no-interaction --prefer-dist - name: Prepare environment run: | cp .env.example .env php artisan key:generate php artisan migrate --force - name: Check API contract (drift) run: php artisan accord:validate - name: Check implementation coverage run: php artisan drift:coverage
accord:validate reports every route that has been added to the app but not yet documented in the spec, or removed from the app but still present in the spec. Either condition fails the build.
drift:coverage is an optional second check — it verifies that every registered route has a controller implementation (not just a closure), catching skeleton routes that were never wired up.
GitLab CI
contract: stage: test image: php:8.3-cli before_script: - composer install --no-interaction --prefer-dist - cp .env.example .env - php artisan key:generate - php artisan migrate --force script: - php artisan accord:validate - php artisan drift:coverage
Pinning to a specific version
If your repository contains multiple API versions (v1, v2…), you can validate each independently:
php artisan accord:validate --api-version=v1 php artisan accord:validate --api-version=v2
Running without --api-version validates all detected versions in one pass.
Why API contracts matter
Every API makes a promise to the apps, services, and teams that depend on it: send me this shape of data, and I'll return that shape of data. That promise is the contract. When it breaks — a field goes missing, a type changes, a response shifts structure — the clients depending on your API fail, often in ways that are hard to trace and expensive to fix.
accord holds your API to its promises automatically. You describe the contract once in an OpenAPI spec file (a standard, human-readable document describing what your API accepts and returns). Accord then validates every request and response against that spec in real time — catching violations the moment they occur, whether in development before code ships or in production before downstream clients are impacted.
The earlier a breach is caught, the cheaper it is to fix. Accord makes catching it free.
Requirements
- PHP ^8.2
- OpenAPI 3.0.x spec files (YAML or JSON)
Installation
composer require fissible/accord
Laravel auto-discovery
The service provider registers automatically. Publish the config to customise it:
php artisan vendor:publish --tag=accord-config
How it works
Accord extracts the API version from the request URI (/v1/ → v1), loads the corresponding spec file (resources/openapi/v1.yaml), and validates request bodies and response bodies against the schemas defined in that spec.
Requests and responses with no matching operation, or whose operation defines no schema for the content type, pass silently. Accord only enforces what the spec describes — making it safe to adopt incrementally on existing APIs.
Spec files
Place your OpenAPI 3.0 specs at:
resources/openapi/v1.yaml ← preferred (hand-authored)
resources/openapi/v2.yaml
JSON is also supported. When no extension is given in the path pattern, Accord tries .yaml, .yml, and .json in that order. Specs are loaded once per version per process and cached in memory.
Laravel
Middleware
Register the middleware in your route file or kernel:
// routes/api.php Route::middleware(\Fissible\Accord\Drivers\Laravel\Http\Middleware\ValidateApiContract::class) ->group(function () { Route::get('/v1/users', [UserController::class, 'index']); });
Or globally in bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) { $middleware->appendToGroup('api', ValidateApiContract::class); })
Configuration
config/accord.php:
return [ 'failure_mode' => env('ACCORD_FAILURE_MODE', 'exception'), // exception | log | callable 'failure_callable' => null, 'version_pattern' => '/^\/v(\d+)(?:\/|$)/', 'spec_source' => env('ACCORD_SPEC_SOURCE', 'file'), // file | url 'spec_pattern' => env('ACCORD_SPEC_PATTERN', '{base}/resources/openapi/{version}'), 'spec_cache_ttl' => env('ACCORD_SPEC_CACHE_TTL', 3600), ];
Loading specs from a URL
Set spec_source to url and provide a URL pattern with a {version} token:
ACCORD_SPEC_SOURCE=url ACCORD_SPEC_PATTERN=https://api.example.com/openapi/{version}.yaml
This is useful when specs are managed externally or when multiple services validate against a shared central spec. Fetched specs are cached in memory per process; configure a PSR-16 cache in the service provider for persistence across restarts in serverless environments.
Testing
Add the AssertsApiContracts trait to your test case and call assertResponseMatchesContract after any API call:
use Fissible\Accord\Drivers\Laravel\Testing\AssertsApiContracts; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class UserApiTest extends TestCase { use RefreshDatabase, AssertsApiContracts; public function test_index_matches_contract(): void { $response = $this->getJson('/v1/users'); $response->assertOk(); $this->assertResponseMatchesContract($response); } }
Slim
use Fissible\Accord\Drivers\Slim\AccordMiddleware; $app->add(AccordMiddleware::fromConfig([ 'failure_mode' => 'log', 'spec_pattern' => '{base}/openapi/{version}', ], __DIR__));
Or use the core middleware directly if you're wiring the validator yourself:
use Fissible\Accord\AccordMiddleware; $app->add(new AccordMiddleware($validator));
Mezzio
// config/pipeline.php use Fissible\Accord\Drivers\Mezzio\AccordMiddleware; $app->pipe(AccordMiddleware::fromConfig([ 'failure_mode' => 'exception', ], __DIR__));
Or register via your container:
// config/autoload/accord.global.php return [ 'dependencies' => [ 'factories' => [ AccordMiddleware::class => fn() => AccordMiddleware::fromConfig( $config['accord'] ?? [], __DIR__ . '/../..', ), ], ], ];
Failure modes
| Mode | Behaviour |
|---|---|
exception |
Throws ContractViolationException (default) |
log |
Logs a warning via PSR-3; request continues |
callable |
Calls your callable with the ValidationResult; request continues |
Callable example
// config/accord.php 'failure_mode' => 'callable', 'failure_callable' => function (\Fissible\Accord\ValidationResult $result): void { // report to your error tracker, queue a job, send an alert, etc. \Sentry\captureMessage(implode(', ', $result->errors)); },
Spec sources
FileSpecSource (default)
Loads specs from the local filesystem. The pattern omits the extension — Accord tries .yaml, .yml, and .json in that order:
use Fissible\Accord\FileSpecSource; $source = new FileSpecSource('/var/www/app', '{base}/resources/openapi/{version}');
UrlSpecSource
Fetches specs from a remote URL. Ideal for APIs whose specs are managed externally:
use Fissible\Accord\UrlSpecSource; $source = new UrlSpecSource( pattern: 'https://specs.example.com/openapi/{version}.yaml', cache: $psrCache, // optional PSR-16 — recommended for serverless ttl: 3600, );
Custom sources
Implement SpecSourceInterface to load specs from anywhere — a database, a registry, or a remote API:
use Fissible\Accord\SpecSourceInterface; use cebe\openapi\spec\OpenApi; class RemoteSpecSource implements SpecSourceInterface { public function load(string $version): ?OpenApi { ... } public function exists(string $version): bool { ... } }
Custom framework drivers
Implement DriverInterface to integrate Accord with any framework not covered by the bundled drivers:
use Fissible\Accord\DriverInterface; use Fissible\Accord\FailureMode; class MyFrameworkDriver implements DriverInterface { public function resolveSpecPath(string $version): string { return sprintf('/path/to/specs/%s.yaml', $version); } public function getFailureMode(): FailureMode { return FailureMode::Exception; } public function getFailureCallable(): ?callable { return null; } }
Version extraction
By default, the version is extracted from the URI path:
| URI | Extracted version | Spec file |
|---|---|---|
/v1/users |
v1 |
resources/openapi/v1.yaml |
/v2/orders/99 |
v2 |
resources/openapi/v2.yaml |
/users |
(none — passes unconstrained) | — |
The pattern is configurable via version_pattern. Capture group 1 must match the version number.
License
MIT