Controller Patterns Guide
Last Updated: 2026-01-08 Status: Active Audience: Developers
This guide documents controller conventions and patterns for the Client Portal application.
Table of Contents
- Overview
- Resource Controllers
- Controller Organization
- Request Handling
- Response Patterns
- Authorization
- API Controllers
Overview
Controllers handle HTTP requests, delegate to services for business logic, and return responses. Keep controllers thin by moving business logic to services.
Controller Responsibilities
| Do | Don't |
|---|---|
| Validate input (Form Requests) | Complex business logic |
| Authorize actions | Database queries beyond simple CRUD |
| Call services | Send emails directly |
| Return responses | Format data for views |
| Handle HTTP concerns | Access external APIs |
Resource Controllers
Generating Resource Controllers
php artisan make:controller Admin/ClientController --resource
Standard Resource Methods
| Method | URI | Action | Route Name |
|---|---|---|---|
| index | GET /clients | List all | clients.index |
| create | GET /clients/create | Show form | clients.create |
| store | POST /clients | Create new | clients.store |
| show | GET /clients/{client} | Show one | clients.show |
| edit | GET /clients/{client}/edit | Show edit form | clients.edit |
| update | PUT /clients/{client} | Update | clients.update |
| destroy | DELETE /clients/{client} | Delete | clients.destroy |
Basic Resource Controller
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Models\Client;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ClientController extends Controller
{
/**
* Display a listing of clients.
*/
public function index(): View
{
$clients = Client::query()
->withCount(['projects', 'invoices'])
->orderBy('company_name')
->paginate(15);
return view('admin.clients.index', compact('clients'));
}
/**
* Show the form for creating a new client.
*/
public function create(): View
{
return view('admin.clients.create');
}
/**
* Store a newly created client.
*/
public function store(StoreClientRequest $request): RedirectResponse
{
$client = Client::create($request->validated());
return redirect()
->route('admin.clients.show', $client)
->with('success', 'Client created successfully.');
}
/**
* Display the specified client.
*/
public function show(Client $client): View
{
$client->load(['projects', 'invoices' => fn ($q) => $q->latest()->limit(5)]);
return view('admin.clients.show', compact('client'));
}
/**
* Show the form for editing the client.
*/
public function edit(Client $client): View
{
return view('admin.clients.edit', compact('client'));
}
/**
* Update the specified client.
*/
public function update(UpdateClientRequest $request, Client $client): RedirectResponse
{
$client->update($request->validated());
return redirect()
->route('admin.clients.show', $client)
->with('success', 'Client updated successfully.');
}
/**
* Remove the specified client.
*/
public function destroy(Client $client): RedirectResponse
{
$client->delete();
return redirect()
->route('admin.clients.index')
->with('success', 'Client deleted successfully.');
}
}
Route Registration
// routes/web.php
Route::middleware(['auth', 'verified', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Route::resource('clients', Admin\ClientController::class);
Route::resource('projects', Admin\ProjectController::class);
Route::resource('invoices', Admin\InvoiceController::class);
});
Controller Organization
Directory Structure
app/Http/Controllers/
├── Controller.php # Base controller
├── Admin/ # Admin controllers
│ ├── ClientController.php
│ ├── ProjectController.php
│ ├── InvoiceController.php
│ └── DashboardController.php
├── Portal/ # Client portal controllers
│ ├── DashboardController.php
│ ├── ProjectController.php
│ └── InvoiceController.php
└── Api/ # API controllers
└── V1/
├── ClientController.php
└── ProjectController.php
Single Action Controllers
For actions that don't fit resource conventions:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Services\InvoiceService;
use Illuminate\Http\RedirectResponse;
class SendInvoiceController extends Controller
{
/**
* Send the invoice to the client.
*/
public function __invoke(Invoice $invoice, InvoiceService $service): RedirectResponse
{
$this->authorize('send', $invoice);
try {
$service->sendInvoice($invoice);
return redirect()
->back()
->with('success', 'Invoice sent successfully.');
} catch (\Exception $e) {
return redirect()
->back()
->with('error', $e->getMessage());
}
}
}
// Route registration
Route::post('invoices/{invoice}/send', SendInvoiceController::class)
->name('admin.invoices.send');
Nested Resource Controllers
// routes/web.php
Route::resource('clients.projects', Admin\ClientProjectController::class)
->shallow(); // Uses project ID directly for show/edit/update/destroy
// Generates:
// GET /clients/{client}/projects -> index
// GET /clients/{client}/projects/create -> create
// POST /clients/{client}/projects -> store
// GET /projects/{project} -> show (shallow)
// GET /projects/{project}/edit -> edit (shallow)
// PUT /projects/{project} -> update (shallow)
// DELETE /projects/{project} -> destroy (shallow)
Request Handling
Form Request Validation
Always use Form Request classes:
// Good - Form Request handles validation
public function store(StoreClientRequest $request): RedirectResponse
{
$client = Client::create($request->validated());
return redirect()->route('admin.clients.show', $client);
}
// Bad - Validation in controller
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'company_name' => 'required|string|max:255',
// ...
]);
// ...
}
Accessing Validated Data
public function store(StoreClientRequest $request): RedirectResponse
{
// All validated fields
$data = $request->validated();
// Specific field
$name = $request->validated('company_name');
// Safe access (returns null if not validated)
$optional = $request->safe()->only(['company_name', 'email']);
}
Route Model Binding
// Automatic binding - Laravel resolves {client} to Client model
public function show(Client $client): View
{
return view('admin.clients.show', compact('client'));
}
// Custom key
// Route: /clients/{client:slug}
public function show(Client $client): View
{
// Resolved by slug instead of id
}
// Scoped binding
// Route: /clients/{client}/projects/{project}
public function show(Client $client, Project $project): View
{
// Project is automatically scoped to client
}
Response Patterns
View Responses
// Simple view
return view('admin.clients.index');
// With data
return view('admin.clients.index', [
'clients' => $clients,
'stats' => $stats,
]);
// Using compact
return view('admin.clients.show', compact('client', 'projects'));
Redirect Responses
// To named route
return redirect()->route('admin.clients.index');
// With route parameters
return redirect()->route('admin.clients.show', $client);
// or
return redirect()->route('admin.clients.show', ['client' => $client->id]);
// Back to previous page
return redirect()->back();
// With flash message
return redirect()
->route('admin.clients.index')
->with('success', 'Client created successfully.');
// With errors
return redirect()
->back()
->withErrors(['email' => 'This email is already taken.'])
->withInput();
Flash Messages
// Success message
return redirect()->route('admin.clients.index')
->with('success', 'Client created successfully.');
// Error message
return redirect()->back()
->with('error', 'Unable to delete client with active projects.');
// Multiple messages
return redirect()->route('admin.clients.index')
->with([
'success' => 'Client created.',
'info' => 'Welcome email sent.',
]);
Displaying Flash Messages (Blade)
@if (session('success'))
<div class="bg-green-100 text-green-800 p-4 rounded">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="bg-red-100 text-red-800 p-4 rounded">
{{ session('error') }}
</div>
@endif
Authorization
Using Policies in Controllers
class ProjectController extends Controller
{
public function __construct()
{
// Authorize all resource actions
$this->authorizeResource(Project::class, 'project');
}
// Or authorize individually
public function show(Project $project): View
{
$this->authorize('view', $project);
return view('portal.projects.show', compact('project'));
}
public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{
$this->authorize('update', $project);
$project->update($request->validated());
return redirect()->route('admin.projects.show', $project);
}
}
Policy Method Mapping
| Controller Method | Policy Method |
|---|---|
| index | viewAny |
| show | view |
| create | create |
| store | create |
| edit | update |
| update | update |
| destroy | delete |
Manual Authorization
public function archive(Project $project): RedirectResponse
{
// Custom policy method
$this->authorize('archive', $project);
$project->update(['status' => 'archived']);
return redirect()->back();
}
// Check without exception
public function show(Project $project): View
{
$canEdit = auth()->user()->can('update', $project);
return view('projects.show', compact('project', 'canEdit'));
}
API Controllers
API Resource Controller
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\StoreClientRequest;
use App\Http\Resources\ClientResource;
use App\Http\Resources\ClientCollection;
use App\Models\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class ClientController extends Controller
{
/**
* Display a listing of clients.
*/
public function index(): ClientCollection
{
$clients = Client::query()
->withCount(['projects', 'invoices'])
->paginate(15);
return new ClientCollection($clients);
}
/**
* Store a newly created client.
*/
public function store(StoreClientRequest $request): JsonResponse
{
$client = Client::create($request->validated());
return (new ClientResource($client))
->response()
->setStatusCode(Response::HTTP_CREATED);
}
/**
* Display the specified client.
*/
public function show(Client $client): ClientResource
{
return new ClientResource($client->load(['projects', 'invoices']));
}
/**
* Update the specified client.
*/
public function update(UpdateClientRequest $request, Client $client): ClientResource
{
$client->update($request->validated());
return new ClientResource($client);
}
/**
* Remove the specified client.
*/
public function destroy(Client $client): Response
{
$client->delete();
return response()->noContent();
}
}
API Response Consistency
// Success with data
return response()->json([
'success' => true,
'data' => $client,
'message' => 'Client created successfully.',
], 201);
// Success without data
return response()->json([
'success' => true,
'message' => 'Client deleted successfully.',
]);
// Error response
return response()->json([
'success' => false,
'message' => 'Client not found.',
'errors' => [],
], 404);
Best Practices
Do
- Use Form Requests for validation
- Use Route Model Binding
- Keep controllers thin (delegate to services)
- Use resource controllers for CRUD
- Return appropriate HTTP status codes
- Use policies for authorization
- Type hint return types
Don't
- Put business logic in controllers
- Use
Requestinstead of Form Requests - Mix web and API responses
- Skip authorization checks
- Hard-code strings (use translations)
- Return views from API controllers
Controller Method Order
class ClientController extends Controller
{
// 1. Constructor (if needed)
public function __construct() {}
// 2. Resource methods in order
public function index() {}
public function create() {}
public function store() {}
public function show() {}
public function edit() {}
public function update() {}
public function destroy() {}
// 3. Additional methods
public function archive() {}
public function restore() {}
}