Table of Contents
Introduction to Controllers
What Are Controllers?
Why Use Controllers?
Types of Controllers
Pros and Cons of Controllers
Real-Life Scenarios
Creating Controllers
Setting Up a Basic Controller
Resource Controllers
Single-Action Controllers
Example: Task Management System
Best Practices and Standards
Controller Methods & Dependency Injection
Writing Controller Methods
Dependency Injection in Controllers
Example: E-commerce Product Management
Pros, Cons, and Alternatives
Middleware Overview
What Is Middleware?
How Middleware Works in Laravel
Built-in Middleware
Real-Life Use Cases
Applying Middleware to Routes & Groups
Route-Specific Middleware
Group Middleware
Example: Role-Based Dashboard Access
Best Practices for Middleware Application
Creating Custom Middleware
Building Custom Middleware
Example: Restricting Access Based on User Subscription
Advanced Middleware Scenarios
Pros, Cons, and Alternatives
Conclusion
Recap of Key Concepts
Next Steps in Your Laravel Journey
1. Introduction to Controllers
What Are Controllers?
In Laravel, controllers are PHP classes that handle the logic for processing HTTP requests and returning responses. They act as the intermediary between your application’s routes and the business logic (models, services, etc.). By organizing related request-handling logic into controllers, you keep your codebase clean, modular, and maintainable.
For example, in a blog application, a PostController might handle tasks like displaying posts, creating new posts, or updating existing ones. Instead of cluttering your routes/web.php file with logic, controllers centralize it for better organization.
Why Use Controllers?
Separation of Concerns: Controllers separate routing from business logic, adhering to the MVC (Model-View-Controller) pattern.
Reusability: Common logic (e.g., fetching posts) can be reused across multiple routes.
Scalability: As your application grows, controllers make it easier to manage complex logic.
Testability: Controllers can be unit-tested independently, ensuring robust code.
Types of Controllers
Basic Controllers: Handle general request logic for various routes.
Resource Controllers: Automatically provide methods for CRUD (Create, Read, Update, Delete) operations.
Single-Action Controllers: Handle a single action, ideal for simple tasks.
Pros and Cons of Controllers
Pros:
Organized and modular code.
Easy to test and maintain.
Simplifies complex route logic.
Supports dependency injection for better flexibility.
Cons:
Overuse can lead to bloated controllers (e.g., "God Controllers").
Requires discipline to keep logic lean and delegate to services or repositories.
Learning curve for beginners unfamiliar with MVC.
Real-Life Scenarios
Task Management App: A TaskController could handle creating, updating, and deleting tasks for users.
E-commerce Platform: A ProductController might manage product listings, details, and cart interactions.
Social Media Dashboard: A DashboardController could display user-specific data based on their role (admin, user, etc.).
2. Creating Controllers
Setting Up a Basic Controller
Let’s start by creating a simple controller for a Task Management System. We’ll use Laravel’s Artisan CLI to generate a controller.
php artisan make:controller TaskController
This command creates a TaskController.php file in the app/Http/Controllers directory:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
return view('tasks.index');
}
}
In your routes/web.php, you can link the controller to a route:
use App\Http\Controllers\TaskController;
Route::get('/tasks', [TaskController::class, 'index'])->name('tasks.index');
This sets up a basic route to display a task list. The index method returns a view located at resources/views/tasks/index.blade.php.
Resource Controllers
Resource controllers simplify CRUD operations by providing predefined methods (index, create, store, show, edit, update, destroy). Let’s create a resource controller for tasks:
php artisan make:controller TaskController --resource
This generates a controller with the following methods:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index() {}
public function create() {}
public function store(Request $request) {}
public function show($id) {}
public function edit($id) {}
public function update(Request $request, $id) {}
public function destroy($id) {}
}
To register a resource controller in routes/web.php:
Route::resource('tasks', TaskController::class);
This automatically maps the controller methods to RESTful routes:
Verb | URI | Action | Route Name |
---|---|---|---|
GET | /tasks | index | tasks.index |
GET | /tasks/create | create | tasks.create |
POST | /tasks | store | tasks.store |
GET | /tasks/{task} | show | tasks.show |
GET | /tasks/{task}/edit | edit | tasks.edit |
PUT/PATCH | /tasks/{task} | update | tasks.update |
DELETE | /tasks/{task} | destroy | tasks.destroy |
Example: Task Management SystemLet’s implement a resource controller for tasks. Assume we have a Task model and a tasks table with columns id, title, description, and status.
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function index()
{
$tasks = Task::all();
return view('tasks.index', compact('tasks'));
}
public function create()
{
return view('tasks.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'status' => 'required|in:pending,completed',
]);
Task::create($validated);
return redirect()->route('tasks.index')->with('success', 'Task created successfully!');
}
public function show(Task $task)
{
return view('tasks.show', compact('task'));
}
public function edit(Task $task)
{
return view('tasks.edit', compact('task'));
}
public function update(Request $request, Task $task)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'status' => 'required|in:pending,completed',
]);
$task->update($validated);
return redirect()->route('tasks.index')->with('success', 'Task updated successfully!');
}
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index')->with('success', 'Task deleted successfully!');
}
}
Blade Views
resources/views/tasks/index.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>Task List</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<h1>Tasks</h1>
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
<a href="{{ route('tasks.create') }}">Create New Task</a>
<ul>
@foreach ($tasks as $task)
<li>
{{ $task->title }} ({{ $task->status }})
<a href="{{ route('tasks.show', $task) }}">View</a>
<a href="{{ route('tasks.edit', $task) }}">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST" style="display:inline;">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
</li>
@endforeach
</ul>
</body>
</html>
resources/views/tasks/create.blade.php:
<!DOCTYPE html>
<html>
<head>
<title>Create Task</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<h1>Create Task</h1>
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
<div>
<label>Title:</label>
<input type="text" name="title" value="{{ old('title') }}">
@error('title') <span>{{ $message }}</span> @enderror
</div>
<div>
<label>Description:</label>
<textarea name="description">{{ old('description') }}</textarea>
@error('description') <span>{{ $message }}</span> @enderror
</div>
<div>
<label>Status:</label>
<select name="status">
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
@error('status') <span>{{ $message }}</span> @enderror
</div>
<button type="submit">Create</button>
</form>
</body>
</html>
This example demonstrates a fully functional task management system with CRUD operations. Users can view, create, edit, and delete tasks via a clean interface.
Single-Action Controllers
Single-action controllers are used for routes that perform a single task. For example, let’s create a controller to handle sending a welcome email to new users.
php artisan make:controller SendWelcomeEmailController --invokable
This creates a controller with a single __invoke method:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
class SendWelcomeEmailController extends Controller
{
public function __invoke(Request $request)
{
$user = $request->user();
Mail::to($user->email)->send(new WelcomeEmail($user));
return redirect()->back()->with('success', 'Welcome email sent!');
}
}
Register the controller in routes/web.php:
Route::get('/send-welcome-email', SendWelcomeEmailController::class)->name('send.welcome.email');
When to Use Single-Action Controllers:
For simple, one-off actions (e.g., sending emails, generating reports).
When you don’t need the full CRUD functionality of a resource controller.
Best Practices and Standards
Keep Controllers Thin: Move complex logic to services, repositories, or models.
Use Route Model Binding: Automatically inject models (e.g., Task $task) instead of manually fetching them.
Validate Input: Always validate user input using $request->validate().
Follow RESTful Conventions: Use resource controllers for CRUD operations to maintain consistency.
Name Controllers Clearly: Use descriptive names like TaskController or SendWelcomeEmailController.
3. Controller Methods & Dependency Injection
Writing Controller Methods
Controller methods handle specific actions for a route. Each method typically returns a response, such as a view, JSON data, or a redirect. Let’s enhance our TaskController with a method to filter tasks by status.
public function filterByStatus(Request $request)
{
$status = $request->query('status', 'pending');
$tasks = Task::where('status', $status)->get();
return view('tasks.index', compact('tasks', 'status'));
}
Update routes/web.php:
Route::get('/tasks/filter', [TaskController::class, 'filterByStatus'])->name('tasks.filter');
In index.blade.php, add a filter dropdown:
<form action="{{ route('tasks.filter') }}" method="GET">
<select name="status" onchange="this.form.submit()">
<option value="pending" {{ $status == 'pending' ? 'selected' : '' }}>Pending</option>
<option value="completed" {{ $status == 'completed' ? 'selected' : '' }}>Completed</option>
</select>
</form>
This allows users to filter tasks by their status (pending or completed).
Dependency Injection in Controllers
Laravel’s dependency injection allows you to inject dependencies (e.g., services, repositories) into controller methods or constructors. This promotes loose coupling and testability.
Example: E-commerce Product ManagementLet’s create a ProductController that uses a ProductService to handle business logic.
First, create a ProductService:
php artisan make:service ProductService
In app/Services/ProductService.php:
<?php
namespace App\Services;
use App\Models\Product;
class ProductService
{
public function getAllProducts()
{
return Product::all();
}
public function createProduct(array $data)
{
return Product::create($data);
}
public function findProduct($id)
{
return Product::findOrFail($id);
}
}
Now, inject ProductService into ProductController:
<?php
namespace App\Http\Controllers;
use App\Services\ProductService;
use Illuminate\Http\Request;
class ProductController extends Controller
{
protected $productService;
public function __construct(ProductService $productService)
{
$this->productService = $productService;
}
public function index()
{
$products = $this->productService->getAllProducts();
return view('products.index', compact('products'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
]);
$this->productService->createProduct($validated);
return redirect()->route('products.index')->with('success', 'Product created!');
}
public function show($id)
{
$product = $this->productService->findProduct($id);
return view('products.show', compact('product'));
}
}
Register the resource controller:
Route::resource('products', ProductController::class);
Why Use Dependency Injection?
Modularity: You can swap out the ProductService implementation without changing the controller.
Testability: Mock the ProductService in unit tests to isolate controller logic.
Reusability: The same service can be used in multiple controllers or commands.
Pros, Cons, and Alternatives
Pros:
Cleaner controllers by delegating logic to services.
Easier to test and maintain.
Promotes single responsibility principle.
Cons:
Adds complexity for small applications.
Requires understanding of dependency injection concepts.
Alternatives:
Repositories: Use repositories for database interactions instead of services.
Actions: Use single-action classes (e.g., CreateProductAction) for specific tasks.
Model Methods: For simple apps, place logic directly in model methods (not recommended for large apps).
4. Middleware Overview
What Is Middleware?
Middleware in Laravel acts as a filter between HTTP requests and your application’s logic. It can perform tasks like authentication, logging, or modifying requests/responses before they reach the controller or after the response is generated.
For example, Laravel’s auth middleware ensures a user is logged in before accessing a protected route. If not, it redirects them to the login page.
How Middleware Works
Middleware sits between the request and the response:
Request → Middleware → Controller → Middleware → Response
Each middleware can:
Modify the request (e.g., add headers).
Block the request (e.g., redirect unauthenticated users).
Modify the response (e.g., add caching headers).
Built-in Middleware
Laravel provides several built-in middleware, such as:
auth: Ensures the user is authenticated.
guest: Ensures the user is not authenticated.
throttle: Limits the number of requests (e.g., for API rate limiting).
can: Authorizes actions based on Laravel’s Gate or Policy.
Real-Life Use Cases
Authentication: Restrict access to a dashboard for logged-in users only.
Rate Limiting: Prevent abuse in an API by limiting requests per minute.
Logging: Log all requests for analytics or debugging.
Role-Based Access: Allow only admins to access certain routes.
5. Applying Middleware to Routes & Groups
Route-Specific Middleware
You can apply middleware to individual routes using the middleware method.
Example: Restrict the tasks.index route to authenticated users:
Route::get('/tasks', [TaskController::class, 'index'])->middleware('auth')->name('tasks.index');
If a user is not logged in, they’ll be redirected to the login page.
Group Middleware
For multiple routes, use middleware groups to apply middleware collectively.
Example: Role-Based Dashboard AccessLet’s create a dashboard with role-based access (e.g., admin vs. regular user). Assume we have a users table with a role column (admin or user).
First, define a custom middleware for admin access:
php artisan make:middleware EnsureUserIsAdmin
In app/Http/Middleware/EnsureUserIsAdmin.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next)
{
if (Auth::user() && Auth::user()->role === 'admin') {
return $next($request);
}
return redirect('/home')->with('error', 'Unauthorized access.');
}
}
Register the middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
// Other middleware...
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
];
Apply the middleware to a route group in routes/web.php:
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard');
Route::get('/admin/users', [AdminController::class, 'users'])->name('admin.users');
});
Example: AdminController
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
class AdminController extends Controller
{
public function index()
{
return view('admin.dashboard');
}
public function users()
{
$users = User::all();
return view('admin.users', compact('users'));
}
}
Blade View (admin/dashboard.blade.php):
<!DOCTYPE html>
<html>
<head>
<title>Admin Dashboard</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<h1>Admin Dashboard</h1>
<p>Welcome, {{ Auth::user()->name }}!</p>
<a href="{{ route('admin.users') }}">Manage Users</a>
</body>
</html>
This setup ensures only admin users can access the dashboard and user management pages.
Best Practices for Middleware Application
Use Descriptive Names: Name middleware clearly (e.g., EnsureUserIsAdmin).
Apply Middleware Sparingly: Avoid overusing middleware on every route to keep performance optimal.
Group Related Routes: Use route groups to apply middleware to related routes (e.g., all admin routes).
Leverage Laravel’s Built-in Middleware: Use auth, can, or throttle when possible to avoid reinventing the wheel.
6. Creating Custom Middleware
Building Custom Middleware
Let’s create a custom middleware to restrict access based on a user’s subscription status (e.g., for a SaaS application).
php artisan make:middleware RestrictToPremiumUsers
In app/Http/Middleware/RestrictToPremiumUsers.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RestrictToPremiumUsers
{
public function handle(Request $request, Closure $next)
{
if (Auth::user() && Auth::user()->is_premium) {
return $next($request);
}
return redirect('/subscribe')->with('error', 'Please upgrade to a premium plan.');
}
}
Register the middleware in app/Http/Kernel.php:
protected $routeMiddleware = [
// Other middleware...
'premium' => \App\Http\Middleware\RestrictToPremiumUsers::class,
];
Apply it to a route:
Route::get('/premium/feature', [PremiumController::class, 'index'])->middleware(['auth', 'premium'])->name('premium.feature');
Example: Restricting Access Based on Subscription
Assume a SaaS app where premium users can access advanced features (e.g., analytics reports).
PremiumController:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PremiumController extends Controller
{
public function index()
{
return view('premium.index');
}
}
Blade View (premium/index.blade.php):
<!DOCTYPE html>
<html>
<head>
<title>Premium Feature</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<h1>Premium Analytics</h1>
<p>Exclusive content for premium users!</p>
</body>
</html>
Advanced Middleware Scenarios
Dynamic Middleware Parameters: Pass parameters to middleware for flexibility. For example, restrict access to specific roles:
public function handle(Request $request, Closure $next, $role)
{
if (Auth::user() && Auth::user()->role === $role) {
return $next($request);
}
return redirect('/home')->with('error', 'Unauthorized access.');
}
Apply it in routes/web.php:
Route::get('/manager/dashboard', [ManagerController::class, 'index'])->middleware('role:manager');
Terminable Middleware: Use the terminate method to perform actions after the response is sent (e.g., logging).
public function terminate(Request $request, $response)
{
\Log::info('Request processed for URL: ' . $request->url());
}
Pros, Cons, and Alternatives
Pros:
Reusable logic across routes.
Enhances security and access control.
Easy to test and maintain.
Cons:
Overuse can complicate route definitions.
Poorly written middleware can impact performance.
Alternatives:
Gates and Policies: Use Laravel’s authorization features for complex permission logic.
Route Constraints: Use route constraints (e.g., where) for simple checks.
Controller Logic: For one-off checks, handle logic in the controller (not recommended for reusable logic).
7. Conclusion
In this Module 6 of our Laravel Core Complete Course, we’ve explored the power of controllers and middleware in building robust, secure, and scalable web applications. From creating basic and resource controllers to leveraging dependency injection and custom middleware, you now have the tools to structure your application logic effectively and secure your routes.
Key Takeaways
Controllers centralize request-handling logic, making your codebase modular and maintainable.
Resource Controllers streamline CRUD operations, while Single-Action Controllers are perfect for simple tasks.
Dependency Injection promotes loose coupling and testability.
Middleware provides a flexible way to filter requests and enforce security.
Custom Middleware allows you to implement application-specific logic, such as subscription-based access or role checks.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam