Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Openapi Cli Laravel Package

spatie/laravel-openapi-cli

Turn any OpenAPI spec into dedicated Laravel artisan commands with typed options for path params, query params, and request bodies. Add auth, caching, retries, and error handling, or pair with Laravel Zero to build standalone CLI tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and locate the entry point first. The package is a standard Spatie Laravel package: composer require spatie/laravel-openapi-cli, then the service provider is auto-discovered. Before writing any code, read https://spatie.be/docs/laravel-openapi-cli — the README is a feature teaser, not reference documentation. The three files worth opening in vendor/spatie/laravel-openapi-cli/ are the facade (OpenApiCli), the config (if publishable via php artisan vendor:publish --tag=openapi-cli-config), and the class that maps an OpenAPI operation to an artisan Command — that last one tells you exactly how option names are derived, which is the source of most surprises.

Register specs in a dedicated service provider, not AppServiceProvider. The registration call returns a fluent config object, and every endpoint becomes a real artisan command at boot. Keep it isolated so it's obvious what the package owns:

// app/Providers/OpenApiCliServiceProvider.php
use Spatie\OpenApiCli\Facades\OpenApiCli;

public function boot(): void
{
    if (! $this->app->runningInConsole()) {
        return;
    }

    OpenApiCli::register(
        spec: base_path('docs/openapi.yaml'), // or a URL
        prefix: 'api',
    )
        ->baseUrl(config('services.internal_api.url'))
        ->cache(ttl: 600)
        ->useOperationIds();
}

First use case: discovery, not invocation. Point at the spec, run php artisan api:list, and you immediately have a browsable, self-documenting surface of every endpoint with typed options — this is the fastest way to sanity-check that path-level parameters, $refs, and multi-line descriptions were parsed correctly. Then invoke one read-only endpoint (php artisan api:get-books --limit=5) to confirm the base URL and auth wiring before you build anything on top of it.

Second use case, and the one the package is really aimed at: wrapping a third-party API in a Laravel Zero binary so your team or your agents get bookstore:get-books instead of hand-rolled curl calls with copy-pasted tokens.

Implementation Patterns

Centralize auth in a resolvable class, pass a closure to auth(). The closure runs per request, so resolve a token manager from the container rather than capturing a token at registration time:

OpenApiCli::register('https://api.bookstore.io/openapi.yaml', 'bookstore')
    ->baseUrl('https://api.bookstore.io')
    ->auth(fn () => app(OAuthTokenManager::class)->token())
    ->cache(ttl: 600);

Pair this with retryOn() for the common "token expired mid-flight" case. The hook receives the failed Response and the Command; return true to re-issue, false to let normal error handling proceed:

->retryOn(function (Response $response, Command $command) {
    if ($response->status() !== 401) {
        return false;
    }

    return app(OAuthTokenManager::class)->refresh();
}, maxRetries: 1)

Fail loudly and early when credentials are absent. Since 1.4.0 you can throw AuthenticationException from inside auth() or retryOn() to abort before the HTTP request is even sent, with a friendly message and an optional hint:

->auth(function () {
    $token = app(OAuthTokenManager::class)->token();

    if (! $token) {
        throw new AuthenticationException(
            'Not logged in to Bookstore.',
            hint: 'Run `php artisan bookstore:login` first.',
        );
    }

    return $token;
})

Handle rate limits declaratively with onError. The pattern from the README is the right default — return false to fall through to the package's built-in error rendering, return anything else to own the handling:

->onError(function (Response $response, Command $command) {
    return match ($response->status()) {
        429 => $command->warn('Rate limited. Retry after '.$response->header('Retry-After').'s.'),
        default => false,
    };
})

Lean on shell redirection for payload extraction. php artisan api:get-books > books.json works cleanly since 1.5.0, and the same holds for binary downloads (> report.pdf). This is the key integration pattern for scripting and for agent tooling: the command emits raw body to stdout and human-facing notices to the console, so a wrapper can capture the artifact without post-processing. Pair with ->yamlOutput() only when a human is reading the output — YAML is worse for jq.

Prefer ->useOperationIds() whenever the spec author set them. It gives you stable, intention-revealing names (bookstore:list-books) instead of names derived from paths, which change when someone renames a path segment or a parameter. On specs without operationIds, you're stuck with the path-derived scheme — see the gotchas.

Treat php artisan <prefix>:list as a contract test. Run it in CI against the checked-in spec (or against the remote URL with a short cache TTL). It catches upstream spec drift — a renamed parameter, a removed endpoint — at build time rather than when an agent calls a command that silently no longer accepts its arguments.

For agent-facing use, expose discovery plus single-invocation. Two things make these commands pleasant tool calls: a :list command to enumerate capabilities, and one command per endpoint so the argument schema in --help doubles as the tool's JSON schema. Cache the spec aggressively (->cache(ttl: 600)) so a boot of the CLI doesn't hang on a network round-trip when an agent shells out repeatedly.

Testing your registrations. Commands registered this way are still first-class artisan commands, so $this->artisan('bookstore:get-books', ['--limit' => 5])->assertExitCode(0) works. To assert against a fake API, fake the underlying HTTP client (Http::fake([...])) if your spec URL and API both resolve through Laravel's HTTP client — verify the package's actual transport in vendor/ before relying on it, and if it bypasses the client, point the spec/base URL at a local fixture server instead.

Gotchas and Tips

Path parameters are stripped from command names, which can collide. GET /books/{book_id}/reviews becomes bookstore:get-books-reviews — the {book_id} segment vanishes. Two paths that differ only by a parameter, or a GET /books/reviews sitting alongside it, can produce ambiguous or clashing names. ->useOperationIds() is the durable fix; otherwise audit :list output after every spec change.

Remote specs are a boot-time dependency. register('https://...') means the spec is fetched when the provider boots (subject to ->cache()). A slow or down API turns every artisan command in the app — including migrate — into a slow one. Guard registration with runningInConsole(), cache with a TTL, and consider a scheduled job that warms the cache. For anything long-lived, prefer a checked-in local spec and use the remote URL only in a refresh job.

Version-pin this package, and read the changelogs before bumping. The 1.3.1 fix ("options being dropped when a parameter description is multi-line") and the 1.1.0 fix ("merge path-level parameters into operation parameters") are both bugs that silently changed the generated CLI surface. If you pin ^1.0 and your spec uses multi-line descriptions or path-level parameters, some endpoints will simply be missing options. Require ^1.5 and audit :list output when you upgrade.

1.5.0 changed output for non-JSON bodies. Binary and other non-printable bodies are now written raw with nothing added — no notice, no newline. If you had tooling parsing that Response is not JSON (...) line, it's gone. Separately, HTML bodies are suppressed by default with a notice reading HTML response (status: 500, content-length: 1234). Use --output-html to see it. — add ->showHtmlBody() only for debugging, since HTML will wreck any pipe.

retryOn's maxRetries is your only guard. A retryOn closure that keeps returning true after a refresh that didn't actually help will burn one retry and stop, but the closure still runs per attempt — keep the token refresh idempotent and cheap.

Don't log the auth() closure's return value. The package passes it straight into a header; if you add request logging or a ->onError that dumps headers for debugging, you'll leak a bearer token into laravel.log and into any agent transcript that captures command output.

Extension points are limited to the fluent builder — plan accordingly. There's no plugin system for reshaping requests before they're sent; if a spec needs a computed header or a body transform the builder doesn't offer, the escape hatch is a hand-written artisan command for that endpoint, living alongside the generated ones. That's a fine outcome, but decide it up front rather than fighting the fluent API.

Adoption risk is real at 42 stars and zero dependents. The API surface has churned through three minor releases with behavior-affecting fixes, and you are one of very few users — good for getting issues noticed, bad for the bus factor. Mitigate by confining registration to one provider behind a feature check, so ripping it out means deleting one file, and keep the spec itself under version control so you're never dependent on the package to know what your API looks like.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk