nova-horizons/laravel-openapi-sdk-generator
Generate PHP SDKs from OpenAPI specifications directly within Laravel. Turn your OpenAPI docs into typed client classes for faster, less error-prone integrations, with configurable output, Laravel conventions, and artisan commands.
Install the generator as a dev dependency — it is a build-time tool, not something that needs to ship to production:
composer require --dev nova-horizons/laravel-openapi-sdk-generator
php artisan vendor:publish --tag="openapi-sdk-config"
The first place to look is the published config file (config/openapi-sdk.php). It is where you declare the spec source (local path or remote URL), the output namespace/directory, the base URL, and any auth wiring. Everything else in the package is driven from there.
Point it at a spec and generate:
// config/openapi-sdk.php
'specs' => [
'acme' => [
'source' => base_path('openapi/acme.yaml'),
'namespace' => 'App\\Sdk\\Acme',
'output' => app_path('Sdk/Acme'),
'base_url' => env('ACME_API_URL'),
],
],
php artisan openapi:generate acme
First use case: you consume a third-party or internal HTTP API and are tired of hand-writing request/response classes. Drop the vendor's openapi.yaml into openapi/, generate once, and you get typed request builders and response DTOs instead of untyped Http::get() arrays. Start with a single small spec (one or two endpoints) before pointing it at a 400-endpoint monster — you want to see the shape of the generated code before committing to it.
Spec-first, regenerate in CI. Treat the OpenAPI document as a checked-in source of truth alongside your code. Add a CI step that runs the generator and fails on a dirty diff (git diff --exit-code). This catches the case where someone edits generated files by hand or forgets to regenerate after a spec bump.
Never edit generated output. Put generated code in a dedicated directory (e.g. app/Sdk/) that is either gitignored entirely or clearly marked with a header. All customisation goes through config, custom templates/stubs, or hand-written wrapper classes that compose the generated client. If you find yourself patching a generated DTO, that is a signal to open an issue or use an extension point.
Bind the client in the container. Rather than new AcmeClient(...) scattered around, register a singleton in a service provider that reads base URL and credentials from config:
$this->app->singleton(AcmeClient::class, fn ($app) => new AcmeClient(
baseUrl: config('services.acme.url'),
token: config('services.acme.token'),
));
Then inject it into jobs, controllers, and commands. This keeps credentials in config/services.php and makes the client swappable in tests.
Use generated DTOs at the boundary. Map HTTP responses into generated DTOs immediately and pass those around internally. It gives you IDE completion, static analysis coverage, and a single place to adapt when the upstream API changes shape.
Test with Http::fake(). Because the generated client almost certainly sits on Laravel's HTTP client, you can assert on the outgoing request and feed it a fixture response. Pair this with a contract test that validates your recorded fixtures still conform to the checked-in spec — that is where most real breakage gets caught.
Version-pin the spec, not just the package. A generator upgrade and a spec upgrade fail in different ways. Bump them separately so you can tell which one broke your build.
v0.2.0 is a pre-1.0 minor bump. The release notes are only a compare link, so read the actual diff before upgrading — under semver, 0.x minor releases are permitted to contain breaking changes, and this one bundles everything from v0.1.2 onward. Pin an exact version in composer.json and upgrade deliberately.
Handle regeneration churn. Generated code diffs noisily on every spec change (reordered properties, reformatted docblocks). Consider gitignoring the output and generating during build/deploy, or at minimum isolate it in its own commit so real changes stay reviewable.
Spec edge cases produce loose types. oneOf/anyOf, nullable unions, free-form objects, and enums-as-strings frequently degrade to array or string in the generated layer. Do not assume a generated property is a strict type just because the spec looks precise — check what was emitted.
OpenAPI 3.1 vs 3.0 matters. 3.1 aligns with JSON Schema and changes nullable semantics; older generators silently mis-handle it. Verify which dialect the package supports before feeding it a 3.1 document, and downgrade the spec if needed.
Auth is usually your job. Generators typically emit the transport and method signatures but leave bearer tokens, HMAC signing, and OAuth refresh out of the generated code. Wire them through client constructor args or a middleware/handler extension point — check the config for a hook before writing your own.
Large specs are slow and memory-hungry. Generating from a several-megabyte spec can take a while and produce thousands of files. Generate per-domain if the package supports multiple named specs, and keep generation out of the request path.
Debugging a bad generation: run the command with verbosity (-v) to see which spec node it choked on, and inspect the emitted file tree before wiring anything up. If a class is missing, it is usually a schema under components/schemas that is never referenced from a path — many generators only emit reachable types.
Extension points to check first: custom stubs/templates for output formatting, namespace and suffix overrides, hooks/events fired around generation, and any post-generation callback in config. These solve most "I wish it generated X" problems without forking the package.
How can I help you explore Laravel packages today?