Skip to content

Multiple documents

One application often has more than one API: a public API and an admin API, or a v1 you still support alongside a v2. Docuccino models each as an independent document — its own route filters, info, security, viewer, and export target — all in one config file.

documents is a map, and the keys are yours. The published config ships one entry, default; add as many as you need. Each is a full, self-contained pipeline:

config/docuccino.php
'documents' => [
'public' => [
'info' => ['title' => 'Public API', 'version' => '2.0.0'],
'routes' => ['include' => ['api/*'], 'exclude' => ['api/admin/*']],
'error_responses' => 'default',
'viewer' => ['route' => '/docs/api'],
'export' => ['path' => 'docs/public.openapi.json'],
],
'admin' => [
'info' => ['title' => 'Admin API', 'version' => '1.4.0'],
'routes' => ['include' => ['api/admin/*']],
'error_responses' => 'default',
'viewer' => ['route' => '/docs/admin', 'gate' => 'viewAdminDocs'],
'export' => ['path' => 'docs/admin.openapi.json'],
],
],

Here the two documents split by route glob, expose their viewers on different routes (the admin one gated), and export to different files. The same pattern covers API versioning — a v1 and a v2 document over api/v1/* and api/v2/*, each with its own info.version.

What’s per document, and what’s global

Section titled “What’s per document, and what’s global”
Per document (documents.<key>.*) Global (top level)
info, servers, routes, security enabled — the master switch
error_responses, tags, representation engine — inference mode and project paths
content, overlays, versioning extensions — your own extension classes
integrations — per-document toggles lint — document lint rules
export, viewer on_route_error, cache

So the analysis engine, your extensions and the lint rules are shared, while everything about what a document contains and where it goes is yours to set per document.

Route selection is a filter chain, and every step must pass:

use Docuccino\Core\Extensions\Context\RouteDescriptor;
// documents.<key>.routes — include, then exclude, then an optional closure
'routes' => [
'include' => ['api/v2/*'],
'exclude' => ['api/v2/internal/*'],
'closure' => fn (RouteDescriptor $route): bool => $route->name !== null,
],

Patterns match the URI without its leading slash, using Laravel’s Str::is wildcards. The closure runs last and receives the discovered route — its methods, uri, name, action and fully-gathered middleware.

#[InDocs] narrows: it’s applied after the globs, so a route still has to match the document’s routes.include to appear at all. Use it for an endpoint that several documents’ globs would otherwise sweep up — a shared webhook that belongs in the public and partner docs but not the admin one.

Two more filters run for every document: #[ExcludeFromDocs] drops a route everywhere, and routes whose controller lives under vendor/ are skipped unless the document sets routes.include_vendor => true.

Every command takes an optional {document} argument. Omit it to run over all documents; pass a key to run one:

Terminal window
php artisan docuccino:export # export every document
php artisan docuccino:export admin # export just the admin document
php artisan docuccino:cache # warm the runtime cache for every document

Two behaviors worth knowing:

  • docuccino:diff is the exception. With no {document} it diffs the default document only, never all of them — pass the key explicitly for any other document.
  • --out and multiple documents don’t mix. docuccino:export --out=... across every document would have each write clobber the last, so Docuccino refuses: name a document, or configure export.path per document.

Per-document results aggregate — if any one document fails, the command exits non-zero. See the commands reference for the full behavior.

Cheaper than you’d expect. Every document in a single command run is built in the same process by the same analysis engine, and the engine keeps its parse of each file — so the second document reuses the expensive work rather than booting PHPStan and re-reading your controllers from scratch. What each document does pay for is its own assembly: its own operations, components, overlays and content tree.