Skip to content

The spec is the source of truth. Your code follows it.

One OpenAPI spec in, a working typed Laravel slice out. A single command generates spatie/laravel-data classes with explicit, spec-derived rules() methods, native PHP enums, abstract controllers per tag, and a routes file, out of the box. The drift gate (openapi:check) fails CI the moment code and contract disagree, and the generated validation is differentially tested against the spec. Readable, deterministic PHP that lives in your repo.
From OpenAPI spec to generated Data classes, validation rules and controllers; you write only business logic; openapi:check fails CI on drift and regeneration never touches your files From OpenAPI spec to generated Data classes, validation rules and controllers; you write only business logic; openapi:check fails CI on drift and regeneration never touches your files

Full flow: generate models, rules, controllers and routes from an OpenAPI spec, catch drift in CI, regenerate without touching hand-written code

A single object schema in your spec becomes a laravel-data class with an explicit rules() method, plus a native backed enum for any enum it references.

// generated from components.schemas.Customer
final class CustomerData extends Data
{
public function __construct(
public readonly int $id,
public readonly string $name,
#[MapName('email_address')]
public readonly ?string $emailAddress = null,
public readonly ?CustomerStatus $status = null,
) {}
public static function rules(): array
{
return [
'id' => ['required', 'integer', 'min:1'],
'name' => ['required', 'string', 'max:255'],
'email_address' => ['sometimes', 'nullable', 'string', 'email'],
'status' => ['sometimes', Rule::enum(CustomerStatus::class)],
];
}
}

openapi.yaml
└── openapi-laravel → app/Data/CustomerData.php (laravel-data class + rules())
app/Data/CustomerStatus.php (native backed enum)
app/Data/CustomerWritableData.php (write variant, when spec uses readOnly/writeOnly)
app/Http/Controllers/Api/AbstractPetController.php (abstract controller per tag)
routes/api.generated.php (generated routing table)

You write your business logic. The DTOs, validation, controller signatures, and routing table stay in sync when the spec changes. Re-run the generator and the diff tells you exactly what moved.

The whole loop is proven end to end: one petstore spec drives a generated Laravel backend and a TypeScript SPA built with openapi-zod-ts, exercised by a Playwright suite over real HTTP. See the end-to-end demo guide.

The e2e demo SPA listing pets served by the generated Laravel backend


Spec-first, not code-first

The OpenAPI document drives the code, not the other way around. This is the opposite of annotation tools like l5-swagger or scramble, where your PHP generates the spec.

Code you own

Output is readable PHP in your repo. Review it, commit it, read it in a diff. No opaque runtime and no reflection magic you cannot follow.

Explicit validation

Rules are emitted verbatim from spec constraints (required, nullable, maxLength, pattern, format, minimum, maximum, enum), not guessed from property types at runtime.

Server scaffold, out of the box

The default run already emits abstract controllers per tag and a routes file, with typed and validated query, path, and header parameters and typed request and response bodies. The routing table derives from the spec, so path-level drift is structurally impossible. Opt out with --no-controllers / --no-routes.

Drift gate for CI

openapi:check recomputes the full file set in memory and compares it byte-for-byte against disk: exit 1 fails the build the moment committed code and spec disagree. Generate and check share one planner, so they are always in lockstep.

Validation that is differentially tested, not assumed

A differential oracle runs spec-valid and spec-invalid payloads through the generated rules() with the real Laravel Validator: what the spec rejects, the generated rules() must reject. The test corpus is 135 published real-world OpenAPI documents that must parse, generate, and compile on every CI run. See Philosophy: quality is the feature.

Deterministic

Stable ordering everywhere. The same spec in produces byte-identical files out, so the generator is safe to run in CI and commit.

Runs without Laravel

The same generator ships as a framework-free binary, so non-Laravel projects and CI can run it too.


  1. Install the package.

    Terminal window
    composer require --dev codewithagents/openapi-laravel
  2. Point it at your spec and generate. One command, full output.

    Terminal window
    php artisan openapi:generate --spec=openapi.yaml --output=app/Data

    Out of the box this emits the Data classes with spec-derived rules(), the native enums, one abstract controller per tag, and a routes file, all typed against each other. Models only? Pass --no-controllers --no-routes.

  3. Implement the controllers and keep it in sync.

    Extend each abstract controller with your business logic; an unimplemented operation is a PHP fatal, not silent drift. Add the drift gate to CI so the committed code can never quietly diverge from the spec:

    Terminal window
    php artisan openapi:check # exit 0 in sync, 1 drift, 2 config error