laravel-controllers
Thin HTTP layer controllers. Controllers contain zero domain logic, only HTTP concerns. Use when working with controllers, HTTP layer, web vs API patterns, or when user mentions controllers, routes, HTTP responses.
$ Installieren
git clone https://github.com/leeovery/claude-laravel /tmp/claude-laravel && cp -r /tmp/claude-laravel/skills/laravel-controllers ~/.claude/skills/claude-laravel// tip: Run this command in your terminal to install the skill
name: laravel-controllers description: Thin HTTP layer controllers. Controllers contain zero domain logic, only HTTP concerns. Use when working with controllers, HTTP layer, web vs API patterns, or when user mentions controllers, routes, HTTP responses.
Laravel Controllers
Controllers are extremely thin. They handle HTTP concerns only and contain zero domain logic.
Related guides:
- query-objects.md - Query objects for API filtering/sorting
- Actions - Actions contain the domain logic
- form-requests.md - Validation layer
- DTOs - DTOs for data transfer
- structure.md - Web vs API organization
Philosophy
Controllers should ONLY:
- Type-hint dependencies
- Validate (via Form Requests)
- Call actions
- Return responses (resources, redirects, views)
Controllers should NEVER:
- Contain domain logic
- Make database queries directly
- Perform calculations
- Handle complex business rules
Controller Naming Conventions
Controllers should be named using the PLURAL form of the main resource:
Standard Resource Controllers
// ✅ 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
Nested Resource Controllers
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);
RESTful Methods Only
Controllers must only use Laravel's standard RESTful method names.
Standard RESTful Methods
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 resource
For APIs (no form views):
index,show,store,update,destroy- APIs must NOT include
createoreditmethods (those are for HTML forms)
Forbidden Method Names
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
}
Non-RESTful Actions: Extract to Invokable Controllers
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?
- Single Responsibility Principle
- Clear intent from controller name
- Independently testable
- Prevents bloated resource controllers
Web Layer vs Public API
Web Layer Controllers
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:
- Not versioned
- Can change freely
- Private (only your app consumes)
Public API Controllers
Purpose: For external/third-party consumption
Location: app/Http/Api/V1/Controllers/
Routes: routes/api/v1.php
Characteristics:
- Versioned (
/api/v1,/api/v2) - Stable contract
- Breaking changes require new version
Key difference: Namespace (Http\Web vs Http\Api\V1). Controller structure is identical.
Full Controller Example
<?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).
Query Objects
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
Authorization
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);
Response Types
JSON Resource
public function show(Order $order): OrderResource
{
return OrderResource::make($order);
}
Collection Resource
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
201 Created
return OrderResource::make($order)->response()->setStatusCode(201);
204 No Content
return response()->noContent();
Redirect
return redirect()->route('orders.show', $order);
Route Model Binding
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'));
}
Controller Testing
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']);
});
Common Mistakes
❌ Domain Logic in Controller
// 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]);
}
✅ Delegate to Action
// GOOD
public function store(
CreateOrderRequest $request,
CreateOrderAction $action
): OrderResource {
$order = $action(
user(),
OrderDataTransformer::fromRequest($request)
);
return OrderResource::make($order);
}
❌ Database Queries in Controller
// BAD
public function index()
{
$orders = Order::with('items')
->where('status', 'pending')
->latest()
->paginate();
}
✅ Use Query Object
// GOOD
public function index(OrderIndexQuery $query): AnonymousResourceCollection
{
return OrderResource::collection($query->jsonPaginate());
}
Summary
Controllers are HTTP adapters:
- Receive HTTP request
- Validate via Form Request
- Call Action (with DTO if needed)
- Return HTTP response via Resource
Every line of domain logic belongs in an Action, not a Controller.
Repository
