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.
What it documents
Section titled “What it documents”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
403response is documented whenever the action definesauthorize(), 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.
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.phpRoute::post('/articles', PublishArticle::class);"post": { "summary": "Publish a draft article.", // The action class is the body's single named source, so it hoists to a component. "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublishArticle" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ArticleResource" } }, "required": ["data"] } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } } } }, "422": { "description": "Unprocessable Entity", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" }, "errors": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } } }, "required": ["message", "errors"] } } } } }},// components.schemas"PublishArticle": { "type": "object", "properties": { "title": { "type": "string", "maxLength": 120 }, "body": { "type": "string" } }, "required": ["title", "body"]}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.
How the dispatched method is resolved
Section titled “How the dispatched method is resolved”The method Docuccino reads matches the package’s own precedence:
asController(), if the action defines it;- otherwise
handle(); - 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)]; }}"responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ArticleResource" } } } } } }}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.
Configuration
Section titled “Configuration”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.
Refining the output
Section titled “Refining the output”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.