Laravel Controllers and Request Handling
The controller is Laravel's "business commander"—it receives requests, coordinates models and views, and returns responses; all business logic is orchestrated from here.
1. What You'll Learn
- Basic Controllers and Single-Action Controllers
__invoke - Resource Controller:
--resourceMapping of Flags to CRUD Methods - API Resource Controller:
--apiFlag Bound to a Route - Dependency Injection: Constructor-Level and Method-Level DI
- Controller Middleware Allocation
2. A True Story of a Backend Developer
(1) Pain Point: A 2,000-line "God Controller"
Early on, Bob crammed all of ShopMetrics' logic into a single ShopController—product management, order processing, user authentication, and report generation were all bundled together. Once the code ballooned to 2,000 lines, changing one feature could potentially break another. When Charlie took over, it took him three days just to untangle the code structure, and Alice had to wait two weeks for a minor feature request.
(2) Solution for the Resource Controller
The Laravel Resource Controller breaks down CRUD operations into seven separate methods, with each method performing a single task. When combined with route bindings, the URLs and methods are automatically mapped to one another.
# One command generates a complete CRUD controller
php artisan make:controller ShopController --resource
# Creates: index(), create(), store(), show(), edit(), update(), destroy()
(3) Revenue
After Bob refactored the code using the resource controller, each method was limited to 30 lines or fewer; Alice's small feature was completed in two days instead of two weeks; and Charlie no longer worried about breaking existing functionality when taking on new features.
3. Basic Controller
(1) Creation and Structure
php artisan make:controller HomeController
# Creates: app/Http/Controllers/HomeController.php
// app/Http/Controllers/HomeController.php
class HomeController extends Controller
{
public function index(): View
{
return view('home.index');
}
public function about(): View
{
return view('home.about');
}
}
(2) Single-Action Controller __invoke
When a controller needs only one method, use __invoke instead of a named method.
php artisan make:controller GenerateReportController --invokable
// app/Http/Controllers/GenerateReportController.php
class GenerateReportController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$report = ReportGenerator::create($request->all());
return redirect()->route('reports.show', $report->id);
}
}
// Route registration
Route::post('/reports/generate', GenerateReportController::class);
| Dimension | Standard Controller | Single-Action Controller |
|---|---|---|
| Number of methods | Multiple | 1 __invoke |
| Route Registration | [Ctrl::class, 'method'] |
Ctrl::class |
| Use Cases | Related Operations | Single-Responsibility Operations |
| Example | ShopController | GenerateReportController |
(1) ▶ Example: ShopMetrics Single-Action Controller
// app/Http/Controllers/ExportOrdersController.php
class ExportOrdersController extends Controller
{
public function __invoke(Request $request): StreamedResponse
{
$shop = Shop::findOrFail($request->shop_id);
$csv = OrderExporter::toCsv($shop->orders);
return response()->streamDownload(
callback: fn () => print($csv),
name: "orders-{$shop->slug}.csv",
headers: ['Content-Type' => 'text/csv'],
);
}
}
// routes/web.php
Route::post('/shops/{shop}/export', ExportOrdersController::class)
->name('shops.export');
Output:
// Execution Successful
4. Resource Controller
(1) Create a Resource Controller
php artisan make:controller ShopController --resource
Automatically generate 7 CRUD methods:
| HTTP Verb | URI | Method | Purpose |
|---|---|---|---|
| GET | /shops | index | list |
| GET | /shops/create | create | Create Form |
| POST | /shops | store | Save New Record |
| GET | /shops/{shop} | show | Details |
| GET | /shops/{shop}/edit | edit | Edit Form |
| PUT/PATCH | /shops/{shop} | update | Update |
| DELETE | /shops/{shop} | destroy | Delete |
(2) Route Registration
// Single line registers all 7 routes
Route::resource('shops', ShopController::class);
// Limit to specific methods only
Route::resource('shops', ShopController::class)->only([
'index', 'show', 'store', 'update', 'destroy',
]);
// Exclude specific methods
Route::resource('shops', ShopController::class)->except([
'create', 'edit',
]);
(1) ▶ Example: ShopMetrics Shop Resource Controller
// app/Http/Controllers/ShopController.php
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('tenant.resolve')->except('index', 'show');
}
public function index(): View
{
$shops = Shop::with('tenant')->paginate(15);
return view('shops.index', compact('shops'));
}
public function create(): View
{
return view('shops.create');
}
public function store(StoreShopRequest $request): RedirectResponse
{
$shop = Shop::create($request->validated());
return redirect()->route('shops.show', $shop)
->with('success', 'Shop created successfully.');
}
public function show(Shop $shop): View
{
$shop->load('products', 'orders');
return view('shops.show', compact('shop'));
}
public function edit(Shop $shop): View
{
return view('shops.edit', compact('shop'));
}
public function update(UpdateShopRequest $request, Shop $shop): RedirectResponse
{
$shop->update($request->validated());
return redirect()->route('shops.show', $shop)
->with('success', 'Shop updated successfully.');
}
public function destroy(Shop $shop): RedirectResponse
{
$shop->delete();
return redirect()->route('shops.index')
->with('success', 'Shop deleted successfully.');
}
}
Output:
// Execution Successful
5. API Resource Controller
(1) Create an API controller
php artisan make:controller Api/ShopController --api
--api is equivalent to --resource --except=create,edit, because the API does not require a form page.
| Method | Web Resources | API Resources |
|---|---|---|
| index | ✅ | ✅ |
| create | ✅ | ❌ |
| store | ✅ | ✅ |
| show | ✅ | ✅ |
| edit | ✅ | ❌ |
| update | ✅ | ✅ |
| destroy | ✅ | ✅ |
(1) ▶ Example: ShopMetrics API Controller
// app/Http/Controllers/Api/ShopController.php
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth:sanctum');
}
public function index(Request $request): JsonResponse
{
$shops = Shop::query()
->when($request->search, fn($q, $search) => $q->where('name', 'like', "%{$search}%"))
->paginate($request->per_page ?? 15);
return ShopResource::collection($shops);
}
public function store(StoreShopRequest $request): JsonResponse
{
$shop = Shop::create($request->validated());
return new ShopResource($shop);
}
public function show(Shop $shop): JsonResponse
{
return new ShopResource($shop->load('products', 'orders'));
}
public function update(UpdateShopRequest $request, Shop $shop): JsonResponse
{
$shop->update($request->validated());
return new ShopResource($shop);
}
public function destroy(Shop $shop): Response
{
$shop->delete();
return response()->noContent();
}
}
Output:
// Execution Successful
6. Dependency Injection
(1) Constructor Injection
class OrderController extends Controller
{
public function __construct(
private OrderService $orderService,
private PaymentGateway $payment,
) {}
public function store(StoreOrderRequest $request): RedirectResponse
{
$order = $this->orderService->create($request->validated());
$this->payment->charge($order);
return redirect()->route('orders.show', $order);
}
}
(2) Method-Level Injection
class ReportController extends Controller
{
public function show(Request $request, Report $report): View
{
// $request injected by container
// $report resolved via Route Model Binding
return view('reports.show', compact('report'));
}
}
(3) Route Model Binding
// Implicit binding — type-hint in controller method
Route::get('/shops/{shop}', [ShopController::class, 'show']);
public function show(Shop $shop): View
{
// $shop is automatically fetched from DB by {shop}
// Equivalent to: Shop::findOrFail($shop)
return view('shops.show', compact('shop'));
}
// Custom key — bind by slug instead of id
Route::get('/shops/{shop:slug}', [ShopController::class, 'show']);
// Now: /shops/alice-store → Shop where slug = 'alice-store'
| Injection Method | Use Cases | Lifecycle |
|---|---|---|
| Constructor | Required by all controller methods | Entire request |
| Method-level | Required only for specific methods | Single method |
| Route Model Binding | Automatically Retrieve Models from URLs | Single Method |
(1) ▶ Example: ShopMetrics Order Controller with Dependency Injection
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
public function __construct(
private OrderService $orderService,
) {
$this->middleware('auth');
}
public function index(Request $request): View
{
$orders = $request->user()->orders()
->with('shop', 'products')
->latest()
->paginate(15);
return view('orders.index', compact('orders'));
}
public function show(Order $order): View
{
$this->authorize('view', $order);
$order->load('items.product', 'shop', 'payment');
return view('orders.show', compact('order'));
}
}
Output:
// Execution Successful
7. Controller Middleware
(1) Constructor Assignment
class ShopController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('tenant.resolve')->except('index');
$this->middleware('can:update,shop')->only('update', 'edit');
}
}
(2) Route-Level Allocation
Route::middleware(['auth', 'admin'])->group(function () {
Route::resource('plans', PlanController::class);
});
| Allocation Location | Granularity | Use Cases |
|---|---|---|
| Constructor | Method-level | Different methods within a controller require different middleware |
| Route Definition | Route Group | A group of routes that share middleware |
| Kernel Global | Global | Must be executed for all requests |
(1) ▶ Example: ShopMetrics Admin Panel Controller
// app/Http/Controllers/Admin/PlanController.php
class PlanController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'role:admin']);
}
public function index(): View
{
$plans = Plan::withCount('subscriptions')->get();
return view('admin.plans.index', compact('plans'));
}
public function store(StorePlanRequest $request): RedirectResponse
{
Plan::create($request->validated());
return redirect()->route('admin.plans.index')
->with('success', 'Plan created.');
}
}
Output:
// Execution Successful
8. Request → Controller → Model → View Response Chain
sequenceDiagram
participant C as Client
participant R as Router
participant M as Middleware
participant Ctrl as Controller
participant Model as Model
participant V as View
C->>R: HTTP Request
R->>M: Run middleware pipeline
M->>Ctrl: Call controller method
Ctrl->>Model: Query data
Model-->>Ctrl: Return results
Ctrl->>V: Pass data to view
V-->>Ctrl: Rendered HTML
Ctrl-->>C: HTTP Response
9. Comprehensive Example: ShopMetrics Product CRUD Controller
// ============================================
// Comprehensive: ShopMetrics ProductController
// Covers: resource controller, DI, middleware, model binding
// ============================================
// app/Http/Controllers/ProductController.php
class ProductController extends Controller
{
public function __construct(
private ProductService $productService,
) {
$this->middleware('auth');
$this->middleware('tenant.resolve');
}
public function index(Request $request): View
{
$products = Product::query()
->where('tenant_id', tenant()->id)
->when($request->search, fn($q, $s) => $q->where('name', 'like', "%{$s}%"))
->when($request->category, fn($q, $c) => $q->where('category_id', $c))
->with('category')
->orderBy($request->sort ?? 'created_at', $request->direction ?? 'desc')
->paginate(20);
return view('products.index', compact('products'));
}
public function create(): View
{
$categories = Category::forTenant(tenant()->id)->get();
return view('products.create', compact('categories'));
}
public function store(StoreProductRequest $request): RedirectResponse
{
$product = $this->productService->create(
tenant()->id,
$request->validated(),
);
return redirect()->route('products.show', $product)
->with('success', 'Product created successfully.');
}
public function show(Product $product): View
{
$this->authorize('view', $product);
$product->load('category', 'orderItems.order');
return view('products.show', compact('product'));
}
public function edit(Product $product): View
{
$this->authorize('update', $product);
$categories = Category::forTenant(tenant()->id)->get();
return view('products.edit', compact('product', 'categories'));
}
public function update(UpdateProductRequest $request, Product $product): RedirectResponse
{
$this->authorize('update', $product);
$product->update($request->validated());
return redirect()->route('products.show', $product)
->with('success', 'Product updated successfully.');
}
public function destroy(Product $product): RedirectResponse
{
$this->authorize('delete', $product);
$product->delete();
return redirect()->route('products.index')
->with('success', 'Product deleted successfully.');
}
}
❓ FAQ
Route::bind() in RouteServiceProvider to define custom parsing logic, or by overriding the resolveRouteBindingQuery() method on the model.new?new manually creates objects, requiring you to manage the dependency chain yourself; with DI, the Laravel container automatically resolves and injects dependencies, supporting interface binding, singleton management, and mock testing.Resource instead of JSON directly?Resource class standardizes the JSON output format, allowing you to hide sensitive fields, rename fields, and nest related data. Returning the model directly would expose all fields and result in an inconsistent format.DB::raw to write SQL in the controller?DB::raw bypasses Eloquent's security layer, making it prone to SQL injection.📖 Summary
- The controller is responsible for receiving requests, coordinating the model and view, and returning responses
- The single-action controller uses
__invoketo handle single operations that are not CRUD operations - The resource controller automatically maps the seven CRUD methods to HTTP verbs
- The API controller omits
createandeditand is used in conjunction withapiResource - Dependency injection eliminates the need to use
newwhen creating objects in controllers; the container automatically resolves dependencies. - Route Model Binding automatically parses URL parameters into model instances
📝 Exercises
-
Basic Exercise (⭐): Use
make:controller --resourceto create the ProductController for ShopMetrics, register resource routes, and implement theindexandshowmethods to return simple views. -
Advanced Exercise (⭐⭐): Create a
ExportOrdersControllersingle-action controller that implements CSV export functionality, injects the OrderService to handle data conversion, and usesstreamDownloadto return the file. -
Challenge (⭐⭐⭐): Design a
TenantProductControllerthat uses Route Model Binding to parse{tenant}and{product}, and implement tenant-isolated product CRUD operations to ensure that users can only manage products within their own tenant.



