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.
Defining documents
Section titled “Defining documents”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:
'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.
Assigning routes to documents
Section titled “Assigning routes to documents”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.
// Restrict a route (or a whole controller) to named documentsuse Docuccino\Attributes\InDocs;
#[InDocs('public', 'partner')]public function webhook(Request $request): JsonResponse { /* … */ }Without the attribute, a route goes into every document whose globs match it.
#[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.
Building them
Section titled “Building them”Every command takes an optional {document} argument. Omit it to run over all documents; pass a key
to run one:
php artisan docuccino:export # export every documentphp artisan docuccino:export admin # export just the admin documentphp artisan docuccino:cache # warm the runtime cache for every documentTwo behaviors worth knowing:
docuccino:diffis the exception. With no{document}it diffs thedefaultdocument only, never all of them — pass the key explicitly for any other document.--outand 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 configureexport.pathper document.
Per-document results aggregate — if any one document fails, the command exits non-zero. See the commands reference for the full behavior.
The cost of a second document
Section titled “The cost of a second document”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.