Skip to content

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.

Run the export with diagnostics visible and read the first warning, not the last:

Terminal window
php artisan docuccino:export --memory-limit=2G

Then 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

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:

Terminal window
php artisan docuccino:export --memory-limit=2G

Both 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.


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:

docuccino.yaml
cache:
enabled: true

Subsequent 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.


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 fiber

Cause. 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:

Terminal window
composer update docuccino/laravel docuccino/inference-phpstan phpstan/phpstan

Then confirm the warnings are gone:

Terminal window
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.


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:

Terminal window
composer require --dev docuccino/inference-phpstan

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:

Terminal window
CACHE_STORE=array SESSION_DRIVER=array QUEUE_CONNECTION=sync BROADCAST_CONNECTION=null \
php artisan docuccino:export --memory-limit=2G

For a permanent CI setup, put those in the environment file your pipeline uses.


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:

Terminal window
composer show phpstan/phpstan larastan/larastan

Nothing that build produced is cached, so fixing the environment and re-running gives you the full document — no need to clear anything first.


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 require

Cause. 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:

Terminal window
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.


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:

  1. Make the arm readable. Return a JsonResponseresponse()->json(…), not a plain response(), 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 the Content-Type folds, 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.

  2. 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.


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.


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:

Terminal window
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.


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:

  1. See what won each field. docuccino:explain reads 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=full keeps the same records — every contribution and everything it shadowed — in an exported UIR file:

    Terminal window
    php artisan docuccino:export --format=uir --provenance=full
  2. Make CI care. --fail-on turns 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=warning already 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=info adds 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=info

    Which floor to start at, and how to move it, is on the diagnostics reference.

  3. Accept what you can’t act on. Some reports are true and unfixable — a vendor model with no @property tags to add, a validation rule that really is a closure. List those codes under diagnostics.accept and the gate stops counting them, so you can tighten it today instead of after the last one is fixed. They keep printing, marked accepted, so you still see the day one starts firing somewhere new — see accepting a code.