Skip to content

How it works

Docuccino turns your application into documentation in five stages. The most important thing to know about that pipeline is what it doesn’t do: it never runs your application to find out what it does.

Routediscovery Staticanalysis Document Emitters Viewer &export

Docuccino asks Laravel’s router which routes exist, then keeps the ones your document selects — the routes.include / routes.exclude globs and an optional closure filter. Routes whose controller lives under vendor/ are skipped unless you set routes.include_vendor, matching php artisan route:list --except-vendor.

Each surviving route carries its real context: the URI template and its path parameters, route-model bindings, middleware, and the controller method behind it. A route registered for several verbs becomes one operation per method, so GET and POST on the same URI are documented independently.

This is the heart of Docuccino, and the reason it’s safe to run anywhere. An embedded static-analysis engine (PHPStan + Larastan) reads the types in your code — controller return types, validation rules, resource toArray() shapes, exception handlers, query builders — and infers the contract. It ships as a separate dev dependency, docuccino/inference-phpstan, because analysis is a build-time job: production serves the finished document with no analyzer installed.

It never runs your code. No controller is invoked, no database is touched, no queue job fires, no email is sent. That’s the trust argument: documenting a payment endpoint can’t charge a card, and generating docs in CI needs no seed data and has no side effects. It’s also what makes the output deterministic — there’s no runtime state to vary between runs.

That holds for the whole pipeline, including the one place real payloads get in. If you record your test suite’s responses as examples, the running happens in your suite, where it already happened; the build reads the committed file and nothing else.

Because the analysis follows types rather than executing calls, it traces through your helper methods: a query builder assembled a few methods deep is still understood, an inline Validator::make(...) still yields real parameters, and an exception thrown by a service the controller calls still becomes a documented error response. The walk is bounded and stays inside your own code — it descends into the PSR-4 source roots your composer.json declares, so a modular Modules\… root counts, and vendor/ is never analyzed. engine.project_paths narrows that where you want less.

Because it really is PHPStan, the analyzer extensions you already maintain apply here too: point engine.config at your own phpstan.neon and whatever it registers shapes your documentation, with no Docuccino-specific API to write.

Each route is analyzed in isolation, so one route that can’t be understood never breaks the build. It leaves behind a skeleton operation and an error diagnostic (or is dropped entirely, with on_route_error: 'omit'). The diagnostics reference covers what each code means and which ones are worth chasing.

Everything inferred is assembled into one document: OpenAPI 3.2, plus the Docuccino extension carrying a stable identity for every operation and schema and a record of where each detail came from. This is also where your Overlays and your Markdown content tree are folded in, and where the document’s content hash is stamped.

Several sources can describe the same operation, so they’re merged by a fixed order of precedence, field by field:

lower precedence higher precedence — wins the field fallback inference integration docblock attribute overlay config 5 10 20 30 40 45 50
Layer Comes from Typical use
fallback Docuccino’s own defaults An operation id from the route name or, where there is none, from the method and path; an OK response description; an untyped path parameter
inference The static-analysis engine Return types, validation rules, resource shapes, thrown exceptions
integration A package integration, e.g. integration:query-builder Filters and sorts, pagination envelopes, security schemes, Data object schemas
docblock PHPDoc on the action or class A summary and description — the leading prose, or @summary/@description where the prose is for maintainers — plus @param/@return detail
attribute A Docuccino attribute #[Response], #[QueryParameter], #[Group], #[Hidden]
overlay An OpenAPI Overlay file Corrections to routes you don’t own, spec-side polish
config docuccino.yaml info, servers, security schemes, tag definitions, representation policies

Higher layers win individual fields without discarding the rest, and every contribution is recorded in the document’s provenance — including the value it replaced — so you can always answer why a detail is documented the way it is. docuccino:explain is that question at the terminal: point it at one endpoint and it prints the stack behind every field, which rung won it, and the file:line it came from.

Emitters transcode that document into what you ship: OpenAPI 3.2, a 3.1 or 3.0 downlevel, a Postman collection, or the full document itself, as JSON or YAML. A build feeds as many emitters as the document configures export targets, so several artifacts cost one analysis rather than one run each. Because the document is canonically ordered and free of timestamps, identical code always produces byte-for-byte identical output — which is what makes the semantic diff and CI version gating possible.

The OpenAPI emitters strip every x-docuccino member on the way out, so provenance and identities stay an internal detail and what you publish is a clean, standard spec.

Finally the document is written to disk by docuccino:export, and served by the bundled viewer straight from your own app. From there it’s yours to commit, diff, and publish — see Deploying to production.

A full build re-analyzes every route, which on a large application is the slow part. Turn on the fragment cache (cache.enabled) and Docuccino stores each operation’s result keyed by the route — its name, URI, action and middleware — your config, the resolved extensions, the build environment (which engine is installed and how it’s configured, plus your composer.lock), and a content hash of every file the analysis actually read. Edit one controller and only that operation is rebuilt; rename a route, install the engine, upgrade a package, or change a file the analysis reached three hops away, and the key changes too, so a stale fragment can’t survive. If you ever want to start over, docuccino:clear --fragments empties the store.

Speeding up builds has the rest: what a warm build skips, what invalidates a fragment, and when the cache isn’t worth turning on.