ovac/idoc
IDoc is a Laravel package that generates API documentation from your routes and controller annotations. It provides a clean, searchable interface with minimal setup, helping keep your API reference accurate and in sync with your codebase.
Install it as a dev dependency so it never ships to production:
composer require ovac/idoc --dev
php artisan vendor:publish --provider="Ovac\Idoc\IdocServiceProvider"
That publishing step is where the real work starts — open config/idoc.php before running anything. The three keys that matter on day one are the models/source path it should scan, the output path for the generated artifacts (the interactive HTML and the OpenAPI 3.0 JSON/YAML), and the app metadata (title, version, description, servers). Get those right and the first run is useful; get them wrong and you'll spend an hour wondering why half your API is missing.
Then generate:
php artisan idoc:generate
You should get two things: a browsable HTML doc and a spec file you can feed to Swagger UI, Redoc, Postman, or Insomnia. Look at the JSON first, not the HTML. The HTML hides omissions; the raw spec exposes them immediately (missing paths, empty properties, "type": "string" on everything).
First use case worth doing: pick one resource — say Post with index/show/store/update/destroy — annotate or configure it, regenerate, and diff the spec against what the endpoint actually returns. That single loop teaches you 80% of how the package thinks, and it's the loop you'll repeat for every other resource.
Model-first, then controller. The package derives request/response shapes primarily from your Eloquent models (columns, casts, $fillable, $hidden) and from your route definitions. The most reliable workflow is: make the model honest first (correct casts, correct $hidden, real column types from migrations), then layer request validation rules on top. If published_at is cast to datetime you get string + format: date-time; if it's an untyped attribute you get mush.
Treat validation rules as the request-body source of truth. FormRequest rules are the highest-signal input you can give the generator:
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:180'],
'body' => ['required', 'string'],
'status' => ['required', Rule::in(['draft', 'published'])],
'published_at'=> ['nullable', 'date'],
'tags' => ['array'],
'tags.*' => ['integer', 'exists:tags,id'],
];
}
Anything statically readable (strings, arrays, in: lists) converts cleanly. Closures and custom Rule objects do not — see Gotchas.
Generate in CI, commit the spec, gate the PR. The pattern that ages best:
- run: php artisan idoc:generate
- run: npx @redocly/cli lint storage/app/idoc/openapi.json
- run: git diff --exit-code storage/app/idoc/ # fail if docs drifted
This turns "someone forgot to update the docs" into a red build. It's the single highest-value habit with any spec generator.
Serve docs in-app, but only behind a gate. Expose the generated HTML on a dev-only route (or local env check) rather than a public /docs. If you need it in staging, wrap it in auth + a feature flag. Docs leak endpoint topology, field names, and sometimes example payloads with real-looking data.
Customize through published views/templates, not by editing output. If the default HTML template doesn't fit your product, publish the views and edit those; regeneration then overwrites the output without overwriting your customizations. Same principle for any per-entity description or grouping: put it in config/annotations, not in the generated file.
Use tags/grouping deliberately. Group by domain (Billing, Catalog, Auth) rather than by controller name. It's cheap now and makes the generated HTML navigable at 60+ endpoints. Pair with servers per environment so the "Try it" panel actually hits something.
Versioned docs = separate output paths. If you ship /api/v1 and /api/v2, generate twice into distinct output directories with distinct info.version values rather than trying to merge.
Import into Postman as a smoke test. A generated OpenAPI file is an instant collection. Run it against a seeded local DB after generation to catch specs that are syntactically valid but semantically wrong (wrong status codes, missing required fields).
$request->all(), and mixed returns produce empty schemas. The fix is always the same: type-hint the request class, return a Resource/JsonResource, or add an explicit override. Budget time for this; it's not a bug.Rule::in([...]) usually works; new UniqueEmailRule() or a closure inside rules() yields a naked string. Replace with in: syntax where possible, or annotate manually.idoc:generate, or your columns will be missing or stale.php artisan config:clear && php artisan route:clear. If routes were cached when you generated, endpoints may be absent entirely — this is the #1 "why is my new route not in the docs" cause.Carbon mutators, array/json casts, and accessors often resolve to string or object with no detail. If a field is an enum, list the values explicitly — consumers use that for codegen.JsonResource output (data: [...] + meta/links) is rarely inferred correctly. Either document the envelope in config or add it to a response schema manually; otherwise every "Try it" response looks like a bug to a frontend dev.multipart/form-data, which many generators default to JSON. If an endpoint takes a file, verify the content type in the raw spec, not the rendered page.null types. Test your actual consumer before committing to a doc pipeline.How can I help you explore Laravel packages today?