From Laravel toolkit
Thin HTTP-layer controllers with zero domain logic. Use when creating or modifying controllers, handling HTTP concerns, or structuring web vs API layers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/laravel:laravel-controllersThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Controllers are **extremely thin**. They handle **HTTP concerns only** and contain **zero domain logic**.
Controllers are extremely thin. They handle HTTP concerns only and contain zero domain logic.
Related guides:
Controllers should ONLY:
Controllers should NEVER:
Controllers should be named using the PLURAL form of the main resource:
// ✅ CORRECT - Plural resource names
CalendarsController // manages calendar resources
EventsController // manages event resources
OrdersController // manages order resources
UsersController // manages user resources
// ❌ INCORRECT - Singular form
CalendarController
EventController
For nested resources, combine both resource names (parent + child):
// Route: /calendars/{calendar}/events
CalendarEventsController // manages events within a calendar
// Route: /orders/{order}/items
OrderItemsController // manages items within an order
Pattern: {ParentSingular}{ChildPlural}Controller
Routes:
// Standard resource routes
Route::resource('calendars', CalendarsController::class);
// Nested resource routes
Route::resource('calendars.events', CalendarEventsController::class);
Controllers must only use Laravel's standard RESTful method names.
For web applications (with forms):
index - Display a listing of the resourcecreate - Show the form for creating a new resourcestore - Store a newly created resourceshow - Display the specified resourceedit - Show the form for editing the resourceupdate - Update the specified resourcedestroy - Remove the specified resourceFor APIs (no form views):
index, show, store, update, destroycreate or edit methods (those are for HTML forms)Never use custom method names in resource controllers:
// ❌ INCORRECT
class OrdersController extends Controller
{
public function all() { } // Use index
public function get() { } // Use show
public function add() { } // Use store
public function remove() { } // Use destroy
public function cancel() { } // Extract to CancelOrderController
}
If you need an endpoint that doesn't fit standard RESTful methods, extract it to its own invokable controller:
// app/Http/Api/V1/Controllers/CancelOrderController.php
class CancelOrderController extends Controller
{
public function __invoke(
Order $order,
CancelOrderAction $action
): OrderResource {
$order = $action($order);
return OrderResource::make($order);
}
}
Routes:
Route::apiResource('orders', OrdersController::class);
Route::post('/orders/{order:uuid}/cancel', CancelOrderController::class);
Why invokable controllers for non-RESTful actions?
Purpose: Serve your application's web layer (API for separate frontend, Blade views, or Inertia)
Location: app/Http/Web/Controllers/
Routes: routes/web.php
Characteristics:
Purpose: For external/third-party consumption
Location: app/Http/Api/V1/Controllers/
Routes: routes/api/v1.php
Characteristics:
/api/v1, /api/v2)Key difference: Namespace (Http\Web vs Http\Api\V1). Controller structure is identical.
<?php
declare(strict_types=1);
namespace App\Http\Web\Controllers;
use App\Actions\Order\CreateOrderAction;
use App\Actions\Order\DeleteOrderAction;
use App\Actions\Order\UpdateOrderAction;
use App\Data\Transformers\Web\OrderDataTransformer;
use App\Http\Controllers\Controller;
use App\Http\Web\Queries\OrderIndexQuery;
use App\Http\Web\Requests\CreateOrderRequest;
use App\Http\Web\Requests\UpdateOrderRequest;
use App\Http\Web\Resources\OrderResource;
use App\Models\Order;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
class OrdersController extends Controller
{
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
public function show(Order $order): OrderResource
{
return OrderResource::make($order->load('items', 'customer'));
}
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
public function update(
UpdateOrderRequest $request,
Order $order,
UpdateOrderAction $action
): OrderResource {
$order = $action(
$order,
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
public function destroy(
Order $order,
DeleteOrderAction $action
): Response {
$action($order);
return response()->noContent();
}
}
For API controllers: Same structure, different namespace (App\Http\Api\V1\Controllers).
For API filtering, sorting, and includes, use Query Objects with Spatie Query Builder:
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
→ Complete query objects guide: query-objects.md
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$this->authorize('create', Order::class);
$order = $action(user(), OrderDataTransformer::fromRequest($request));
return OrderResource::make($order);
}
Or use route middleware:
Route::post('/orders', [OrdersController::class, 'store'])
->can('create', Order::class);
Use route model binding for cleaner controllers:
Route::get('/orders/{order}', [OrdersController::class, 'show']);
Route::get('/orders/{order:uuid}', [OrdersController::class, 'show']); // Custom key
Controller automatically receives model:
public function show(Order $order): OrderResource
{
return OrderResource::make($order->load('items', 'customer'));
}
Feature tests for controllers:
use function Pest\Laravel\actingAs;
use function Pest\Laravel\postJson;
it('creates an order', function () {
$user = User::factory()->create();
$data = CreateOrderData::testFactory()->make();
actingAs($user)
->postJson('/orders', $data->toArray())
->assertCreated()
->assertJsonStructure(['data' => ['id', 'status']]);
});
it('requires authentication', function () {
postJson('/orders', [])->assertUnauthorized();
});
it('validates required fields', function () {
actingAs(User::factory()->create())
->postJson('/orders', [])
->assertUnprocessable()
->assertJsonValidationErrors(['customer_email', 'items']);
});
// BAD
public function store(Request $request)
{
$order = Order::create($request->validated());
$order->items()->createMany($request->items);
$total = $order->items->sum('total');
$order->update(['total' => $total]);
}
// GOOD
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
// BAD
public function index()
{
$orders = Order::with('items')
->where('status', 'pending')
->latest()
->paginate();
}
// GOOD
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
npx claudepluginhub noppu-labs/ai-toolkit --plugin laravelGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.