Skip to content

Laravel Actions

Activates automatically when lorisleiva/laravel-actions is installed. When you register an action as a route, Docuccino documents it exactly as the package runs it — no annotations, no wrapper controller.

Any class using AsController counts, including through the umbrella AsAction trait, and including when the trait comes from a parent class.

An action registered as a controller doesn’t run through __invoke directly — the package dispatches asController() if you’ve defined one, otherwise handle(). Docuccino resolves the same method, so everything is read from the real signature rather than the trait’s generic forwarder:

  • The operation summary comes from the resolved method’s docblock.
  • The request body comes from the action’s own rules() method, turned into schema constraints through the same validation pipeline Form Requests use.
  • A 403 response is documented whenever the action defines authorize(), rendered in the same style as the rest of your error responses (framework defaults, or the Problem Details preset).
  • The response body is inferred from the resolved method’s return type — or, when the action defines a jsonResponse() transformer, from that method’s return type (see below), because that is what a JSON client actually receives.
app/Actions/PublishArticle.php
class PublishArticle
{
use AsController;
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:120'],
'body' => ['required', 'string'],
];
}
public function authorize(ActionRequest $request): bool
{
return $request->user()->can('publish', Article::class);
}
/** Publish a draft article. */
public function handle(): ArticleResource
{
// ...
}
}
// routes/api.php
Route::post('/articles', PublishArticle::class);

One class, no annotations — and the summary, the title/body body (with max:120 as maxLength), the ArticleResource response, the 403 from authorize(), and the 422 that any validated request can return are all documented. The body component is named after the action class, so an action reused across several routes is one shared schema.

On a read verb (GET/HEAD) there’s no body to send, so rules() becomes query parameters instead — the same rule as everywhere else in Docuccino.

The method Docuccino reads matches the package’s own precedence:

  1. asController(), if the action defines it;
  2. otherwise handle();
  3. otherwise __invoke().

If you register a specific method explicitly — Route::post('/articles', [PublishArticle::class, 'handle']) — that method is honored as-is: Docuccino documents its own signature and return type. But the package does not run the action’s rules() or authorize() for an explicitly-registered method (nor for a WithAttributes action), so no request body is inferred from rules() and no 403 from authorize() there — documenting them would describe validation the endpoint never performs. Register the action as a controller (its asController()/handle()) to get the rules() body and the authorize() 403.

Registration Summary & response from rules() body authorize() 403
Route::post('/x', Action::class) asController(), else handle(), else __invoke() Yes Yes
Route::post('/x', [Action::class, 'store']) store() No No
An action using WithAttributes The dispatched method No No

Response transformers: jsonResponse() and htmlResponse()

Section titled “Response transformers: jsonResponse() and htmlResponse()”

Actions can shape their response per content type. When the client asks for JSON, the package hands handle()’s value to jsonResponse($result, $request) and returns that — so jsonResponse()’s return type, not handle()’s, is the real 200 body. Docuccino documents the transformed shape:

class PublishArticle
{
use AsController;
public function handle(): Article
{
return Article::create(/* ... */);
}
/** Wrap the model in a JSON:API-style envelope for JSON clients. */
public function jsonResponse(Article $article): array
{
return ['data' => new ArticleResource($article)];
}
}

The 200 body is the { data } envelope from jsonResponse() — the Article that handle() returns is what the transformer wrapped, so it never appears on its own.

An action that defines htmlResponse() serves HTML to browser clients. Docuccino records that as a text/html representation on the same success response — a content-type note alongside the JSON body, rather than an attempt to type rendered HTML as a JSON schema:

"200": {
"description": "OK",
"content": {
"application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" } } } },
"text/html": { "schema": { "type": "string" } }
}
}

An action can define both: the JSON body comes from jsonResponse(), and text/html is listed too. This mapping applies however the route is registered — invokable or an explicit method — because the package’s decorator wraps every dispatch, so the transformer always runs.

There’s nothing to configure. The only switch is the shared enabled opt-out, per document:

// config/docuccino.php → documents.default.integrations
'laravel_actions' => ['enabled' => false],

With it off, the rules() body, the authorize() 403, the jsonResponse() redirect and the text/html note are all omitted. Operations still resolve to the dispatched method, so summaries and return types stay correct. See the integrations reference.

Everything here is inferred at the integration layer, so the usual attributes still apply — add a #[Group], an #[Example], or an extra #[Response] on the resolved action method and it wins over the inferred values.