Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Documentator Laravel Package

tsitsishvili/documentator

Documentator generates interactive API docs for Laravel by inferring endpoints from routes, FormRequests, Resources and return types, with PHP attributes for overrides. Ships an OpenAPI 3.2 spec plus a built-in explorer UI with a live request playground.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and look at the live output before touching config — Documentator is designed to be useful with zero setup on a standard api/* surface.

composer require tsitsishvili/documentator

# Optional: only needed once you want to change matching, auth, grouping, etc.
php artisan vendor:publish --tag=documentator-config

Requirements are PHP 8.2+ and Laravel 12/13. Enable the docs surface explicitly in non-local environments (DOCUMENTATOR_ENABLED=true) rather than relying on defaults.

Then, in order:

  1. Visit /docs — the built-in Aurora explorer. This is the fastest way to see what the package actually inferred.
  2. Fetch /docs/openapi.json — the real artifact. The UI is a view; the JSON is the contract. Judge everything by this file.
  3. Read config/documentator.php — specifically routes.match, routes.exclude, routes.exclude_middleware, security, auth_middleware, grouping, and status-inference settings.
  4. Run php artisan documentator:explain GET /api/whatever on one endpoint you already know well. It shows inference/override provenance, which is the single most useful debugging tool in the package.

First use case: pick one existing, conventionally-written endpoint (FormRequest in, Resource out) and do nothing to it. Open /docs, confirm the summary came from the controller docblock, body params came from rules(), the 200/201 schema came from the Resource, and auth came from middleware. Once that "free" case is correct, you know where the inference boundary is and when you'll need attributes.


Implementation Patterns

Inference-first endpoint shape

The package rewards idiomatic Laravel. The canonical endpoint needs no attributes at all:

/**
 * Create an order.
 *
 * Charges the customer and returns the created order.
 */
public function store(StoreOrderRequest $request): OrderResource
{
    return new OrderResource(Order::create($request->validated()));
}

Docblock first line → summary, remainder → description. StoreOrderRequest::rules() → body params with types, requiredness, enums, formats, bounds, nested wildcards, uploads. OrderResource → success schema. POST → 201 by convention.

Assign each fact to exactly one source

This is the discipline that keeps generated docs maintainable:

Fact Preferred source
Method + URI Laravel route
Summary / description Method or closure docblock
Path input Route placeholder, constraint, or model binding
Query input Request accessor ($request->integer('page')), GET/HEAD validation, Query Builder, #[QueryParam]
Body FormRequest, inline validation, Spatie Data input, #[BodyParam]
Success schema Return type/expression, Resource/Data/model, #[Response]
Auth Middleware/config, global auth, #[Authenticated]
Group/version Controller inference/config, #[Group]
Visibility #[Hidden], #[Deprecated]

Attributes are last-write-wins overrides, not a parallel documentation system. If you find yourself adding #[BodyParam] for a field that's already in the FormRequest, delete one of them.

Place input by HTTP method

  • GET/HEAD — validation rules and request accessors become query parameters, never a body.
  • HTTP QUERY — register via Route::match(['QUERY'], '/orders/search', ...). The FormRequest rules become request content, and the operation lands under OpenAPI 3.2's query Path Item field. Keep URI query params (cursors, locale) separate from the body criteria.
  • Body-bearing methods — file/image rules flip to multipart; otherwise use #[RequestMediaType].

Don't convert a mutating POST into QUERY just to get body support — QUERY is safe/idempotent by contract, and reviewers will notice.

Lean on the optional integrations

If you already use Spatie packages, the docs come free and there is no runtime dependency:

  • spatie/laravel-data — type-hint a Data object as an argument and as a return type and you get request fields + response schema with typed properties, enums, nested Data, collections, mapped names, and Optional/Lazy requiredness.
  • spatie/laravel-query-builder — literal allowedFilters, allowedSorts, defaultSort, allowedIncludes, allowedFields become query params, including custom *QueryBuilder subclasses.
  • Laravel Actions — rules() and handle() return types are read for routes pointing at asController().

The catch: these are source-scanned. Building allowedFilters from a variable or a config array defeats the inference.

Use attributes for the gaps only

Reach for #[Response] when a return type is opaque, #[SchemaName]/#[UsesModel] when naming conventions collide, #[Group(..., version: 'v2')] for versioned surfaces, #[Callback]/#[Webhook] for event-driven flows, and #[Response(stream: true, mediaType: 'text/event-stream')] for NDJSON/SSE endpoints (the declared schema is one item, emitted as itemSchema).

Class-level #[Group], #[Authenticated], #[Hidden], #[Deprecated] set defaults across a controller — use these instead of repeating attributes on every method.

Inline PHPDoc is the nicest feature for messy validation arrays:

$request->validate([
    /** @var uuid @example 9f40d932-... @default 9f40d932-... */
    'user\.uuid' => ['required', Rule::exists('users', 'uuid')],

    /** @ignoreParam */
    'internal_only' => ['string'],
]);

Wire the contract into CI

Treat the OpenAPI document as a versioned artifact:

php artisan documentator:check --against=openapi.json --fail-on=breaking
php artisan documentator:export openapi.json
php artisan documentator:typescript api-client.ts
php artisan documentator:postman

And validate responses in feature tests rather than trusting the document:

$this->getJson('/api/orders/42')->assertMatchesDocumentation();

recordAsDocumentationExample('order found') records a real, redacted example from an existing test — it validates first and never dispatches an endpoint by itself.

Sections for multi-surface apps

When you have api/* and app/*, configure grouping.sections and each surface gets its own UI and split spec (/docs/api, /docs/api/openapi.json). This is often the difference between "one giant unusable spec" and something partners can actually navigate.


Gotchas and Tips

Silent omission is the default failure mode. An endpoint can just not appear. Work the checklist in order: php artisan route:list → routes.match → routes.exclude / routes.exclude_middleware → #[Hidden] → confirm the action is an introspectable controller method or closure. Closures with poor typing, Route::resource magic, or actions built by a package may be unreadable — documentator:check flags these; move them to a real controller.

Inference is static, not runtime. Anything dynamic is invisible: rules assembled in a loop, Rule::when with computed conditions, filters built from config, validators constructed in a service. Keep rules literal where you want them documented, or add an explicit attribute.

documentator:check is not a linter for everything. It audits action introspectability, success schemas, health warnings, and OpenAPI conformance, and optionally compares drift. It will not catch "your per_page accessor has no bounds" — you still need to read /docs/openapi.json for the changed operations.

Status conventions will surprise you. POST → 201, DELETE → 204, everything else 200. If your POST legitimately returns 200, or your DELETE returns a body, override with #[Response]. Also remember distinct response shapes sharing a status collapse into oneOf — that's usually right, occasionally not.

Parameter-in-the-wrong-place is almost always a method-semantics bug. GET/HEAD validation goes to query, QUERY validation goes to content. If a body shows up on a GET, look at how the FormRequest is type-hinted and whether the route is actually GET.

Escaped dots and wildcards. Validation keys split on unescaped dots (items.*.id → nested), and user\.uuid is preserved as a literal property name. Get this wrong and you'll ship a schema with a phantom user object.

Auth can over- or under-apply. auth:<guard> maps to the scheme whose key matches the guard, falling back to default. Map custom middleware aliases via documentator.auth_middleware. documentator.authenticate => true emits a top-level security requirement for every operation — endpoints not behind auth opt out automatically, but verify that on the JSON, because a wrongly global auth requirement breaks every external consumer at once.

Schema name collisions. Two Resources called OrderResource in different namespaces will fight over components.schemas. Pin them with #[SchemaName('PublicOrder')]. Similarly, model resolution for Resources is by naming convention (models_namespace) — use #[UsesModel(Order::class)] when the convention misses, or field types will silently degrade to string.

Never ship 3.2 blindly to a 3.1-only toolchain. documentator:export openapi.json --openapi=3.1 produces a projection for downstream consumers. Unsupported QUERY or streaming constructs are reported as actionable errors rather than silently rewritten — read those reports, don't suppress them.

Try-it cross-origin calls need CORS. The explorer runs from the docs origin; if your API doesn't allow that origin, the playground fails while the spec looks perfect. Easy to mistake for a Documentator bug.

Regenerate on deploy. php artisan documentator:generate builds the cached document. If you commit an exported spec, make regeneration part of the pipeline, otherwise the committed contract rots the moment a FormRequest changes.

Protect the docs surface. /docs is public by default once enabled. Put route middleware on it and/or Documentator::auth(...), and consider documentator:check --suggest-hidden to flag operational/internal routes that shouldn't be in a partner-facing spec.

Maturity caveat. At 17 stars with no dependents, this is a young package. Pin the version in composer.json, keep a committed OpenAPI export as the source of truth for consumers, and exercise assertMatchesDocumentation() in tests so an upstream inference change shows up as a test failure rather than a partner complaint. The upgrade guide (1.x → 2.0, OpenAPI 3.2) is worth reading before bumping constraints — the 3.2 requirement is a real toolchain commitment, not a cosmetic one.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource