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 executes your application code.

Routediscovery Staticanalysis UIR 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 analyser 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.

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 — engine.project_paths says where to descend, and vendor/ is never analyzed.

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').

Everything inferred is assembled into the UIR — an OpenAPI-shaped document that also carries 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 A route-name operation id, 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, @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 config/docuccino.php 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.

Emitters transcode the UIR into what you ship: OpenAPI 3.2, a 3.1 or 3.0 downlevel, or the raw UIR itself, as JSON or YAML. Because the UIR 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 Scalar 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, your config, the resolved extensions, and a content hash of every file the analysis actually read. Edit one controller and only that operation is rebuilt; change a file it depends on three hops away and the key changes too, so a stale fragment can’t survive.