Troubleshooting
Most Docuccino problems announce themselves twice: once as a diagnostic in the command output, and once as something missing from the document. This page starts from what you can see and works back to the cause.
Start here
Section titled “Start here”Run the export with diagnostics visible and read the first warning, not the last:
php artisan docuccino:export --memory-limit=2GThen match the symptom:
| What you see | Where to go |
|---|---|
Allowed memory size of … exhausted |
The export runs out of memory |
| The export finishes, but takes minutes every time | The build is slow |
Every operation has parameters but no 200 body |
Responses are missing |
| One endpoint is documented, but wrongly | Why is this endpoint documented this way? |
engine.not-installed |
The document is thin |
| A database, Redis or queue exception before analysis starts | The app won’t boot |
engine.boot-failed |
The analyzer won’t start |
| Composer refuses to install on Laravel 13 | Dependency conflicts |
A 4xx or 5xx with no body, or a shared error $ref that disappeared |
An error response lost its body |
An #[ErrorComponent] name nowhere in the document |
#[ErrorComponent] changed nothing |
inference.action-failed on one or two actions |
Reading diagnostics |
| Any other diagnostic code | Diagnostics reference |
The export runs out of memory
Section titled “The export runs out of memory”Symptom. The command dies with a PHP fatal error, often pointing at a file inside PHPStan’s phar, followed by Docuccino’s own note naming the two levers to reach for:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in .../phpstan.phar/resources/functionMap.php on line 59
Docuccino ran out of memory while analyzing your code.… * Raise the ceiling — set engine.memory_limit in docuccino.yaml (e.g. '2G'), or pass --memory-limit=2G to this command. * Narrow the analysis — engine.project_paths, in the same file, bounds interprocedural descent. It is unset by default, which descends into every PSR-4 source root your composer.json declares, so writing it and naming fewer of them costs memory back. Vendor code is never analyzed.PHP can’t catch memory exhaustion, so this is the one failure Docuccino explains on the way out instead of degrading around.
Cause. Docuccino’s inference runs a real static analyzer over your codebase, and analyzers are
memory-hungry. PHP’s default memory_limit of 128M is nowhere near enough for an app of any size.
This has nothing to do with your app’s runtime memory use — the analysis is a separate, much heavier
workload.
Fix. Give the command room. 2G is a sensible starting point:
php artisan docuccino:export --memory-limit=2Gengine: mode: 'in-process' # DOCUCCINO_ENGINE overrides this memory_limit: '2G'Every console entry point picks it up — export, validate, diff, cache — so nobody has to
remember the flag.
- name: Export API docs run: php artisan docuccino:export --memory-limit=2G --fail-on=warning; Still works, and still the right answer if you'd rather not touch config.; Only worth doing on a dev machine or a CI image — never on a production web box,; which serves the committed artifact and runs no analysis at all.memory_limit = 2GBoth levers only ever raise the ceiling — a process already running with more is left alone, and -1
is rejected — and --memory-limit wins over engine.memory_limit. Neither touches a web request, so a
viewer whose source is generate runs under whatever the server gives it; on a large codebase serve it
from artifact or a warm cache instead.
The build is slow
Section titled “The build is slow”Symptom. docuccino:export succeeds, but takes long enough that you avoid running it — and it
takes just as long when you’ve changed one controller.
Cause. By default every build analyzes every route from scratch, and boots a static analyzer to do it. Nothing is remembered between runs.
Fix. Turn on the fragment cache:
cache: enabled: trueSubsequent builds re-analyze only the operations whose inputs changed, and a build where every route is warm never starts the analyzer at all. Speeding up builds explains what invalidates a fragment, how to reuse the store in CI, and when it isn’t worth enabling.
If a first build is the slow one, engine.project_paths
narrows how far interprocedural descent goes — it is unset by default, which descends into every source
root you declare. Read that note before writing it: priming, not descent, is what makes most classes
resolvable, and every throw written in a callee you exclude is documented nowhere.
Responses are missing everywhere
Section titled “Responses are missing everywhere”Symptom. The document looks structurally right — every path, method, parameter and request body is present — but no operation has a success response body. The output carries one warning per action:
[warning] inference.action-failed: Type analysis of App\Http\Controllers\OrderController::update failed: Cannot suspend outside of a fiberCause. A version mismatch between Docuccino’s inference engine and PHPStan. PHPStan changed how it hands scopes to code that walks your source; on the affected versions every response-type lookup throws, so each action degrades to “analysis failed” and loses its response shape. Requests and error responses come from different paths, which is why they survive and make the document look deceptively healthy.
Fix. Update both packages:
composer update docuccino/laravel docuccino/inference-phpstan phpstan/phpstanThen confirm the warnings are gone:
php artisan docuccino:export --memory-limit=2G 2>&1 | grep -c 'Cannot suspend'Zero is the expected answer. If it isn’t, the installed docuccino/inference-phpstan predates the
fix — check the version you actually resolved with composer show docuccino/inference-phpstan.
The document is thin
Section titled “The document is thin”Symptom. A single warning, and a document built almost entirely from your annotations:
[warning] engine.not-installed: The inference engine is not installed; documentation came from docblocks and attributes only. - Install it where you generate: composer require --dev docuccino/inference-phpstan. - Set DOCUCCINO_ENGINE=null to document without inference and silence this.A bulleted line under a diagnostic is its help — what to change, printed wherever the diagnostic is. The unbulleted line beneath it is the tool’s own link to where the code is written up.
Cause. docuccino/inference-phpstan isn’t installed. The adapter is designed to work without
it — it degrades to reading docblocks, attributes, config and overlays rather than failing — so
this is a warning, not an error.
Fix. Install the engine as a development dependency:
composer require --dev docuccino/inference-phpstanThe app won’t boot during export
Section titled “The app won’t boot during export”Symptom. An exception from a driver or service, thrown before any analysis output appears:
RedisClusterException: Couldn't map cluster keyspace using any provided seed…or the equivalent for a database, cache or queue connection.
Cause. Generating a document boots your Laravel application, because Docuccino reads your real route table. Anything your app connects to at boot must therefore be reachable — and in a CI container, usually isn’t.
Fix. Point the connecting services at drivers that need no network. Analysis itself makes no queries, sends no mail and dispatches no jobs, so this costs you nothing:
CACHE_STORE=array SESSION_DRIVER=array QUEUE_CONNECTION=sync BROADCAST_CONNECTION=null \ php artisan docuccino:export --memory-limit=2GFor a permanent CI setup, put those in the environment file your pipeline uses.
The analyzer won’t start
Section titled “The analyzer won’t start”Symptom. The export succeeds, and one error explains why the document is thin:
[error] engine.boot-failed: The inference engine could not start, so documentation came from docblocks and attributes only: Failed to boot the PHPStan/Larastan container: … - Generate from the project root in an environment the application boots in — the analyzer boots it the way an artisan command does — and check the engine package and its analyzer are installed at a supported version. …Cause. The engine is installed, and it couldn’t come up. The analyzer boots your application the
way an artisan command does, from the project root, so the usual causes are the ones above — a
service it can’t reach, a missing bootstrap/app.php because the command ran from elsewhere — plus
an analyzer version this release doesn’t support. The build carries on rather than dying: your
docblocks, attributes, config and overlays still produce a document.
Fix. Read the tail of the message, which is the analyzer’s own. Then run the export from the project root, in an environment your app boots in, and confirm the analyzer resolved:
composer show phpstan/phpstan larastan/larastanNothing that build produced is cached, so fixing the environment and re-running gives you the full document — no need to clear anything first.
Dependency conflicts
Section titled “Dependency conflicts”Symptom. Composer refuses to resolve, naming a transitive package rather than Docuccino:
Problem 1 - docuccino/core … requires symfony/yaml ^7.0 -> found symfony/yaml[v8.0.0, …] but it conflicts with your root composer.json requireCause. A framework upgrade moved a shared dependency to a new major version ahead of the constraint Docuccino declared for it. Laravel 13 pulling Symfony 8 is the common case.
Fix. Update Docuccino first — the constraint is usually already widened in a newer release:
composer update "docuccino/*"If that doesn’t resolve it, the version you need isn’t published yet.
Open an issue with the conflict block; a
constraint widening is a small, fast release. Prefer that over a permanent conflict or
replace entry in your own composer.json, which will outlive the problem.
An error response lost its body
Section titled “An error response lost its body”Symptom. A status that used to publish a body — often every 404, or every 429 on a throttled
route — is suddenly two lines, and the $ref to the shared component it pointed at is gone with the
component:
"404": { "description": "Not Found" }Cause. Docuccino can see that your own handler renders that exception, and it could not read what
your handler renders it to. The body it would otherwise have published is Laravel’s stock
{ "message": string }, which your renderer replaced — so it is withheld rather than asserted over code
that refutes it, and no tier behind fills the gap. There is then no body for a component name to land on,
which is why the $ref and the component go too. Read your diagnostics and you’ll find
inferred-handler.too-dynamic naming the callback; if you gate CI at --fail-on=warning, that is also
what turned the build red.
This is not a regression in what your API does — it is the document stopping short of a claim it could not support. What it costs is a type a generated client had, so it is worth closing.
Fix. Two ways, and either settles it:
-
Make the arm readable. Return a
JsonResponse—response()->json(…), not a plainresponse(), a view or a redirect — with the payload written at that call site and a literal integer status (404, not$e->getCode()or a ternary). The payload is the half that brings the shape back: where only the status or theContent-Typefolds, the response states that media type under a schema constraining nothing — a weaker claim than a shape and a stronger one than silence — and the warning stands. -
State it yourself.
#[Response(status: 404, type: ErrorPayload::class)]on the action publishes the shape over the tier, so the response is complete again. It corrects the document without silencing the diagnostic, which keeps naming the callback.
The whole rule, with the output either way, is in Framework defaults.
#[ErrorComponent] changed nothing
Section titled “#[ErrorComponent] changed nothing”Symptom. You marked something with #[ErrorComponent], regenerated, and components.responses has
no entry under the name you chose. Read your diagnostics first — the three causes are told apart by
whether the build said anything, and by which code it said.
Cause, if it warned attribute.error-component-unread. You marked the controller action. The
attribute names an error where the error is defined, and an action is where several of your errors
meet — it answers at every status its validation, its authorization and its throws produce, and one name
over all of them says nothing about which. So nothing reads it there, and the build says so rather than
leaving you to wonder.
Fix. Move it to the exception class, or to the render method that builds the body. To name one
status of one operation, use #[Response]’s
errorComponent: argument instead — the only anchor that
reaches a body nothing threw.
Cause, if it said nothing. The attribute names a shared error component, and only an error two or
more operations state is shared. Throw that exception from a single endpoint and its body stays inline
on that operation, so there is no component for the name to apply to. That rule is what keeps a
components bucket from filling with one entry per one-off error, and it is the same rule with or
without the attribute — nothing about the marked class is being ignored, which is why there is nothing
to report.
Fix. Nothing, if one endpoint really is the whole story: the response is complete and correct where it is, and the name is published the moment a second operation states the same error. If you expected several endpoints to share it and none did, they are stating different bodies — see Repeated bodies become shared components for what counts as the same one.
Cause, if it warned inferred-handler.too-dynamic. The response has no body to name. Your own
handler renders that exception and Docuccino could not read what it renders it to, so the body was
withheld and the component name is skipped with it — a name needs a shape to sit on. The attribute is
fine; what is missing is the body. See An error response lost its
body.
One more way to see nothing: an #[ErrorComponent] on a base controller your API classes extend
names nothing either, and is reported nowhere — a warning per route of every child would say the same
thing dozens of times over. Only the action’s own declaration is reported, so move a base-class one to
the exceptions themselves and the names appear.
Why is this endpoint documented this way?
Section titled “Why is this endpoint documented this way?”Symptom. One operation is documented, but not the way you expected: a summary you didn’t write, a response body from the wrong class, a parameter that shouldn’t be optional. Nothing is missing, so there is no diagnostic to read.
Cause. Something wrote that field, and something else may have written it first and lost. Docuccino composes each operation from seven precedence layers, and a higher rung overrides a lower one field by field — so a docblock summary can be replaced by an attribute, and an inferred response body by an integration’s.
Fix. Ask:
php artisan docuccino:explain "POST /api/invoices"docuccino:explain prints the stack behind every
field of that one operation: which layer won it, what it displaced, the file:line to open, and — on
a → line under each field — what to change to override it. Name the endpoint however you already
think of it: the method and URI the viewer shows, a route name, an operation id, or a fragment of any
of them, which lists everything that matches. Add --field=<name> to see one field’s values in full.
Reading diagnostics
Section titled “Reading diagnostics”Diagnostics are how Docuccino tells you it recovered less than it wanted to. A handful on a large codebase is normal and usually not worth chasing; the same code on every action is a signal.
Every code Docuccino emits — what it means, how loud it is, and what to do about it — is in the diagnostics reference. This section is about getting more out of them.
Two flags make diagnostics more useful:
-
See what won each field.
docuccino:explainreads that back for one endpoint at the terminal, so you can tell “inference never ran” from “inference ran and lost to something else”.Terminal window php artisan docuccino:explain "POST /api/invoices"For the whole document at once,
--provenance=fullkeeps the same records — every contribution and everything it shadowed — in an exported UIR file:Terminal window php artisan docuccino:export --format=uir --provenance=full -
Make CI care.
--fail-onturns diagnostics into a non-zero exit, so a regression in inference coverage breaks the build instead of quietly thinning your docs. It takes a severity floor — anything that loud or louder fails the run.--fail-on=warningalready catches the loud failures of recovery: an engine that never started, an action whose analysis failed, an attribute that didn’t take, and an exception handler too dynamic to read — which predicts an error response with no body.--fail-on=infoadds the widening diagnostics on top — the ones that say Docuccino recovered less than your code claims and published something broader to stay truthful. That is the gate on inference certainty, and it’s the one that catches a model quietly losing its columns.Terminal window php artisan docuccino:export --fail-on=infoWhich floor to start at, and how to move it, is on the diagnostics reference.
-
Accept what you can’t act on. Some reports are true and unfixable — a vendor model with no
@propertytags to add, a validation rule that really is a closure. List those codes underdiagnostics.acceptand the gate stops counting them, so you can tighten it today instead of after the last one is fixed. They keep printing, markedaccepted, so you still see the day one starts firing somewhere new — see accepting a code.