Open source · MIT
API documentation for Laravel that documents change and provenance, not just endpoints.
Docuccino reads the code you already wrote — controllers, form requests, resources, exception handling — and compiles it into a deterministic, identity-carrying document. OpenAPI comes out of that. So do a semantic diff your build can fail on, and an answer to why is it documented this way.
- Requires
- PHP 8.3+
- Supports
- Laravel 12 & 13
- Emits
- OpenAPI 3.2 / 3.1 / 3.0
$ composer require docuccino/laravel
$ composer require --dev docuccino/inference-phpstan
$ php artisan docuccino:install
✔ Published config/docuccino.php and docuccino.yaml
✔ Matched 38 routes against the default api/* pattern
✔ Analysis engine installed
✔ Wrote docs/openapi.json
→ Your viewer is live at /docs/api
Analysis is a build-time job. Your app is never executed to produce a document, and the engine never has to run on a production host.
How it works
Your application in. A document that knows its own history out.
Most generators read a spec file you maintain, or annotations you remember to write. Docuccino reads the application, and keeps hold of where every detail came from.
-
01
It reads your application
Routes, controllers, form requests, resources, DTOs, enums and exception handling — analysed with an embedded PHPStan and Larastan engine at build time. Your code is never executed to produce a document.
-
02
It compiles to the UIR
An OpenAPI-3.2-shaped document where every operation, schema and parameter carries a stable identity and a record of the file, line and layer that produced it.
-
03
It emits what you need
OpenAPI 3.2, 3.1 or 3.0, a bundled interactive viewer at /docs/api, a semantic diff your build can fail on, and a machine-readable surface for the agents now reading your docs.
Precedence, lowest to highest
Higher layers win field by field — so one attribute overrides one inferred description, and everything else it got right stays.
What you get
Accurate on the first export, not after a week of annotating.
Everything below happens with no configuration beyond pointing Docuccino at your routes. Annotations exist for the things your code genuinely cannot say.
Deep type inference
Request and response shapes come from your real types — array shapes, generics and conditional returns included. The type hints and PHPStan extensions you already have make your docs better for free.
What it readsQuery parameters, for free
Filters, sorts, includes and pagination are recovered by constant-folding through helper methods several calls deep. No hand-written parameter lists to keep in step with the builder.
Query buildersError responses, automatically
Docuccino reads the exception handling you actually wrote — render callbacks, exception render(), Responsable::toResponse() — with the thrown type narrowed, so 422, 404 and your own error shapes document themselves.
ErrorsByte-deterministic output
The same code on the same version produces a byte-identical document. No timestamps, no absolute paths, no shuffling — commit the artifact and your day-to-day diffs stay about the API.
How it worksA diff you can gate CI on
docuccino:diff --enforce compares two artifacts over stable identities and applies a versioning policy. Ship a breaking change without the version bump to match and the build fails.
CommandsA format you can build on
The UIR is a documented format with a published JSON Schema. Export it, validate it, read every field back to the line that produced it — the same document the emitters, the diff and the viewer all run on.
The UIRCode in, docs out
This controller. No annotations. That document.
A list endpoint built the way Laravel applications are actually built — a query builder behind a helper method, a resource, a form request. Here is what comes out.
// app/Http/Controllers/Api/InvoiceController.php
final class InvoiceController {
public function index(IndexInvoices $request): ResourceCollection
{
$invoices = QueryBuilder::for(Invoice::class)
->allowedFilters($this->filters()) // folded through
->allowedSorts(['issued_at', 'total'])
->paginate();
return InvoiceResource::collection($invoices);
}
/** @return list<string> */
private function filters(): array
{
return ['status', 'customer_id', 'issued_at'];
}
}
"/api/invoices": {
"get": {
"operationId": "invoices.index",
"summary": "List invoices",
"parameters": [
{ "name": "filter[status]", "in": "query" },
{ "name": "filter[customer_id]", "in": "query" },
{ "name": "filter[issued_at]", "in": "query" },
{ "name": "sort", "schema": { "enum": ["issued_at", "-issued_at",
"total", "-total"] } },
{ "name": "page", "schema": { "type": "integer", "minimum": 1 } }
],
"responses": {
"200": { "$ref": "#/.../InvoiceCollection" },
"401": { "$ref": "#/.../Unauthenticated" },
"422": { "$ref": "#/.../ValidationError" }
}
}
}
// Nothing on the left was annotated. Pagination appears because
// the call graph reaches a paginating terminal; 401 and 422 come
// from the application's own exception handling.
Ask any field where it came from
Because every node carries its own provenance, docuccino:explain
can read the trail back: which layer won, what it shadowed, the file and line behind it, and
exactly what to change to override it.
$ php artisan docuccino:explain invoices.index --field=responses.200
GET /api/invoices → invoices.index
responses.200.content.application/json.schema
✔ won inference InvoiceResource.php:22
InvoiceResource::toArray()
✖ shadowed fallback generic 200 skeleton
responses.200.description
✔ won docblock InvoiceController.php:14
→ To change it: #[Response(200, ...)] on InvoiceController::index(),
or an overlay entry — an overlay outranks every layer above.
$ php artisan docuccino:diff docs/openapi.json build/openapi.json --enforce
2 breaking · 5 additive · 1 cosmetic
BREAKING invoices.update request property `currency` became required
BREAKING invoices.destroy operation removed
ADDITIVE invoices.index query parameter `filter[customer_id]` added
ADDITIVE credits.store new operation
COSMETIC invoices.show description reworded
Policy semver · 1.4.2 → 2.0.0 required, found 1.5.0
FAILED A breaking change needs a major version bump.
$ # exit code 1 — the build stops here, not in your consumer's integration.
Break the build, not the integration
The diff compares two artifacts over stable identities, so a renamed route or a reordered
file is not mistaken for a change. Pick a versioning policy — semver, date, or none — and
--enforce makes it a required check.
The surface
A handful of commands, and one config file.
Configuration lives in a single published file, organised around named documents. Everything else is a command you run in development or in CI.
-
docuccino:installFirst run. Publishes config, reports how many of your routes each document matches, and offers a first export. -
docuccino:exportBuilds the document and writes it where you point it — OpenAPI or raw UIR. -
docuccino:diffSemantic diff between two artifacts, with --enforce to apply your versioning policy in CI. -
docuccino:explainReads the provenance trail back for one operation: which layer won each field, and what it shadowed. -
docuccino:validateChecks an artifact against the published UIR JSON Schema. -
docuccino:coverageMerges what your test suite exercised and gates on undocumented or unverified endpoints. -
docuccino:watchRebuilds as you edit, so the viewer keeps up with the code. -
docuccino:cacheWarms the analysis cache ahead of a build; docuccino:clear throws it away.
Packages
One monorepo, split into the pieces you need.
Install the adapter and the analysis engine and you are done — but the core, the emitters and the attributes are separate, documented packages, because the UIR is not a Laravel-only idea.
docuccino/laravel
The Laravel adapter: service provider, config, artisan commands, bundled viewer and package integrations.
php/laravel
docuccino/core
Framework-agnostic UIR model, canonicalizer, identities, emitters, diff and contracts.
php/core
docuccino/inference-phpstan
PHPStan and Larastan embedded behind core's TypeEngine. Install it as a dev dependency.
php/inference-phpstan dev only
docuccino/attributes
Dependency-free PHP attribute classes, for when the code cannot say it all.
php/attributes
The versioned UIR JSON Schema is served from its own $id URLs.
Three commands, and your API documents itself.
Install it into an existing Laravel application and export. If the result is thin, the troubleshooting guide starts from what you can see and works back to the fix.