Routing and Controllers

Module 19: PHP Backend - Laravel

Introduction to Laravel Routing

Routing is the process of mapping URLs to specific actions in your application. Think of routes as the switchboard operators of your application - they direct incoming calls (requests) to the appropriate departments (controllers or closures).

In real-world terms, routing is similar to how a post office sorts mail by zip code and address - each piece of mail (HTTP request) gets directed to its proper destination (controller action) based on its address (URL).

graph LR A[HTTP Request] --> B[Router] B --> C1[Controller Action] B --> C2[View] B --> C3[Closure Function] B --> C4[Resource Controller] B --> C5[API Endpoint] C1 --> D[Response] C2 --> D C3 --> D C4 --> D C5 --> D D --> E[Client]

Route Files

Laravel organizes routes into separate files based on their purpose:

This separation is like organizing your documents into different folders by type - it helps maintain clarity and makes it easier to find what you're looking for.


// routes/web.php - Web routes
Route::get('/', function () {
    return view('welcome');
});

// routes/api.php - API routes
Route::get('/users', [UserController::class, 'index']);
            

An important distinction: routes in api.php are automatically prefixed with /api and assigned to the api middleware group.

Basic Route Definitions

Laravel provides expressive methods for defining routes for different HTTP verbs:


// Basic GET route
Route::get('/hello', function () {
    return 'Hello World';
});

// Route with parameters
Route::get('/users/{id}', function ($id) {
    return 'User ' . $id;
});

// Named route
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');

// Route with multiple HTTP verbs
Route::match(['get', 'post'], '/contact', [ContactController::class, 'handleContact']);

// Route for all HTTP verbs
Route::any('/webhook', [WebhookController::class, 'handle']);
            

Each HTTP verb corresponds to a specific type of action:

This aligns with the CRUD (Create, Read, Update, Delete) operations in most applications.

As an analogy, these HTTP verbs are like different types of requests you might make at a library:

Route Parameters

Routes often need to capture parts of the URL as parameters. Laravel makes this straightforward:

Required Parameters


Route::get('/users/{id}', function ($id) {
    return 'User with ID: ' . $id;
});
            

Optional Parameters


Route::get('/users/{id?}', function ($id = null) {
    return $id ? 'User ' . $id : 'All users';
});
            

Parameters with Constraints


Route::get('/users/{id}', function ($id) {
    return 'User ' . $id;
})->where('id', '[0-9]+');

// Multiple constraints
Route::get('/posts/{slug}/{id}', function ($slug, $id) {
    return "Post $slug with ID $id";
})->where(['slug' => '[A-Za-z0-9\-]+', 'id' => '[0-9]+']);

// Global constraints in RouteServiceProvider
// public function boot()
// {
//     Route::pattern('id', '[0-9]+');
//     // ...
// }
            

Route parameters work like variables in a function - they capture dynamic parts of the URL and make them available to your code.

Think of route parameters as form fields in an address - some fields are required (like the street name), others are optional (like an apartment number), and some have specific formats (like a zip code that must be numeric).

Named Routes

Naming routes allows you to reference them without hardcoding URLs, making your application more maintainable:


// Defining a named route
Route::get('/user/profile', [ProfileController::class, 'show'])->name('profile');

// Generating URLs to named routes
$url = route('profile'); // /user/profile

// Generating URLs with parameters
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])->name('profile.show');
$url = route('profile.show', ['id' => 1]); // /user/1/profile

// Redirecting to named routes
return redirect()->route('profile');
            

Named routes are like contacts in your phone - instead of remembering everyone's phone number (URL structure), you can simply select their name (route name) to call them.

This approach has several advantages:

Route Groups

Route groups allow you to share attributes across multiple routes, such as middleware, namespaces, prefixes, and more:


// Route group with prefix
Route::prefix('admin')->group(function () {
    Route::get('/users', [AdminController::class, 'users']);
    Route::get('/settings', [AdminController::class, 'settings']);
    // Both routes are prefixed with /admin
});

// Route group with middleware
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'show']);
    // Both routes use the auth middleware
});

// Combined attributes
Route::prefix('admin')->middleware(['auth', 'admin'])->group(function () {
    // Routes with prefix /admin and middlewares auth & admin
    Route::get('/users', [AdminController::class, 'users']);
    // More routes...
});
            

Route groups act like organizational departments - they group related functionalities and apply common policies (middleware) to them.

Nested Route Groups


Route::prefix('api')->group(function () {
    Route::prefix('v1')->group(function () {
        Route::get('/users', [ApiController::class, 'usersV1']);
    });
    
    Route::prefix('v2')->group(function () {
        Route::get('/users', [ApiController::class, 'usersV2']);
    });
});
// Creates /api/v1/users and /api/v2/users
            

This hierarchical organization is like nested folders in a file system - it helps organize routes logically based on their relationships.

Route Model Binding

Laravel's route model binding automatically resolves Eloquent models from route parameters, saving you from manually querying the database:

Implicit Binding


// Route definition
Route::get('/users/{user}', [UserController::class, 'show']);

// Controller method
public function show(User $user)
{
    return view('users.show', ['user' => $user]);
}
            

Laravel automatically resolves {user} to a User model instance by matching the route parameter value against the model's primary key. If no matching model exists, a 404 response is generated.

Explicit Binding


// In RouteServiceProvider::boot
public function boot()
{
    Route::bind('user', function ($value) {
        return User::where('username', $value)->firstOrFail();
    });
}

// Route definition
Route::get('/users/{user}', [UserController::class, 'show']);
            

Route model binding is like having an assistant who automatically retrieves the file you need whenever you mention its name - it eliminates the repetitive task of looking up records by ID.

Custom Keys for Model Binding


// Resolving by username instead of ID
public function resolveRouteBinding($value, $field = null)
{
    return $this->where('username', $value)->firstOrFail();
}
            

This customization allows you to use more user-friendly identifiers in your URLs, such as usernames or slugs, instead of database IDs.

Introduction to Controllers

Controllers are the orchestra conductors of your application - they coordinate the interaction between the HTTP request, models, and views. They provide a central place to group related request handling logic.

sequenceDiagram participant Client participant Routes participant Controller participant Model participant View Client->>Routes: HTTP Request Routes->>Controller: Dispatch to Action Controller->>Model: Request Data Model-->>Controller: Return Data Controller->>View: Pass Data View-->>Controller: Return Rendered View Controller-->>Routes: Return Response Routes-->>Client: HTTP Response

Creating Controllers


// Create a basic controller
php artisan make:controller UserController

// Create a controller with resource methods
php artisan make:controller ProductController --resource

// Create a controller with a model binding
php artisan make:controller OrderController --model=Order
            

Basic Controller Structure


namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users.index', ['users' => $users]);
    }
    
    public function show($id)
    {
        $user = User::findOrFail($id);
        return view('users.show', ['user' => $user]);
    }
}
            

Controllers typically contain methods that correspond to different actions your application can perform, such as listing records, showing details, creating new records, etc.

Controller Route Binding

There are multiple ways to connect controllers to routes:

Method 1: Individual Route Definitions


Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id}', [UserController::class, 'show']);
Route::post('/users', [UserController::class, 'store']);
            

Method 2: Resource Controllers


// Register all resource routes
Route::resource('photos', PhotoController::class);

// Register only specific resource routes
Route::resource('photos', PhotoController::class)->only([
    'index', 'show'
]);

// Register all resource routes except specific ones
Route::resource('photos', PhotoController::class)->except([
    'create', 'store', 'update', 'destroy'
]);
            

Resource controllers automatically map conventional routes to controller methods:

HTTP Verb URI Action Route Name
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

Resource controllers implement the CRUD pattern, providing a standardized approach to resource management. It's like having standardized forms for common business procedures - everyone knows what to expect and how to use them.

Single Action Controllers

For controllers that handle only a single action, Laravel offers single action controllers with the __invoke method:


// SingleActionController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ShowDashboard extends Controller
{
    public function __invoke(Request $request)
    {
        return view('dashboard');
    }
}

// Route definition
Route::get('/dashboard', ShowDashboard::class);
            

Single action controllers are like specialized tools that do one job extremely well - they're perfect for focused, single-purpose actions.

Creating Single Action Controllers


php artisan make:controller ShowDashboard --invokable
            

Controller Middleware

Middleware can be applied to controllers to filter HTTP requests before they reach your controller actions:

Method 1: In Route Definitions


Route::get('/profile', [ProfileController::class, 'show'])->middleware('auth');
            

Method 2: In Controller Constructor


class UserController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('log')->only('index');
        $this->middleware('subscribed')->except('store');
    }
}
            

Controller middleware is like security checks or preparatory procedures that happen before the main event. For example, the auth middleware ensures only authenticated users can access certain controller actions, similar to how a security guard checks badges before allowing entry to restricted areas.

Dependency Injection in Controllers

Laravel's service container automatically resolves any dependencies declared in your controller method signatures:


public function store(Request $request, PostRepository $posts)
{
    $posts->create($request->all());
    return redirect()->route('posts.index');
}
            

In this example, Laravel automatically injects the current Request instance and resolves the PostRepository from the service container.

Dependency injection is like having assistants who automatically bring you exactly what you need for a task without you having to fetch everything yourself.

Method Injection vs. Constructor Injection


// Constructor injection
class UserController extends Controller
{
    protected $users;
    
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }
    
    public function show($id)
    {
        $user = $this->users->find($id);
        return view('users.show', ['user' => $user]);
    }
}

// Method injection
class ProductController extends Controller
{
    public function show($id, ProductRepository $products)
    {
        $product = $products->find($id);
        return view('products.show', ['product' => $product]);
    }
}
            

Choose constructor injection for dependencies used across multiple controller methods, and method injection for dependencies needed only by specific methods.

Request Handling in Controllers

Controllers process incoming HTTP requests and generate appropriate responses:

Accessing Request Data


public function store(Request $request)
{
    // Access all input data
    $all = $request->all();
    
    // Access specific input field
    $name = $request->input('name');
    
    // Access with default value if not present
    $quantity = $request->input('quantity', 1);
    
    // Check if input field exists
    if ($request->has('product_id')) {
        // ...
    }
    
    // Retrieve only specific fields
    $credentials = $request->only(['email', 'password']);
    
    // Retrieve all except specific fields
    $data = $request->except(['credit_card']);
}
            

Form Validation


public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'body' => 'required',
        'published_at' => 'nullable|date',
    ]);
    
    // The validation passed, continue processing...
    Article::create($validated);
    
    return redirect()->route('articles.index');
}
            

Validation is like a quality control checkpoint that ensures incoming data meets your specifications before processing it. It's similar to how a receptionist might verify that a form has all required fields filled out before passing it to the appropriate department.

Returning Responses

Controllers can return various types of responses:

View Responses


// Return a view
return view('user.profile', ['user' => $user]);

// With flash data
return view('dashboard')->with('status', 'Profile updated!');
            

JSON Responses


// Return JSON data (automatically sets Content-Type header)
return response()->json([
    'name' => 'John',
    'email' => 'john@example.com'
]);

// With status code
return response()->json(['error' => 'Unauthorized'], 401);
            

File Downloads


// File download
return response()->download($pathToFile);

// File download with custom name
return response()->download($pathToFile, 'report.pdf');

// Display file in browser
return response()->file($pathToFile);
            

Redirects


// Redirect to named route
return redirect()->route('login');

// With parameters
return redirect()->route('users.show', ['id' => 1]);

// Redirect to controller action
return redirect()->action([UserController::class, 'index']);

// Redirect back with input
return back()->withInput();

// Redirect with flash data
return redirect('dashboard')->with('status', 'Profile updated!');
            

The variety of response types is like having different communication channels for different purposes - sometimes you need to send a document (view), sometimes structured data (JSON), sometimes a file, and sometimes you need to direct someone elsewhere (redirect).

API Resource Controllers

For API development, Laravel provides specialized resource controllers that focus on JSON responses:


// Create an API resource controller
php artisan make:controller API/ProductController --api

// Register API resource routes
Route::apiResource('products', ProductController::class);
            

API resource controllers include only the methods needed for API interactions (index, store, show, update, destroy) and omit the view-related methods (create, edit).

This is like having specialized communication protocols for machine-to-machine interaction (APIs) versus human-facing interfaces.

Resource Collections


// Register multiple API resources at once
Route::apiResources([
    'products' => ProductController::class,
    'categories' => CategoryController::class,
    'tags' => TagController::class,
]);
            

Route Caching

In production, you can cache your routes for better performance:


php artisan route:cache
            

This compiles all routes into a single file that the framework loads faster than it would parsing multiple route files.

Route caching is like preparing a roadmap in advance rather than consulting individual maps for each journey - it's more efficient but less flexible once created.

Remember to clear the cache when you modify routes:


php artisan route:clear
            

Important note: Route caching does not work with Closure-based routes, only controller routes.

Advanced Routing Techniques

Rate Limiting


// Limit a route to 60 requests per minute per user
Route::middleware(['throttle:60,1'])->group(function () {
    Route::get('/api/search', [SearchController::class, 'index']);
});
            

Subdomain Routing


// Match specific subdomain
Route::domain('{account}.example.com')->group(function () {
    Route::get('/', function ($account) {
        return "Dashboard for {$account}";
    });
});
            

Route Caching


// Cache response for 60 seconds
Route::get('/users', [UserController::class, 'index'])->middleware('cache.headers:60');
            

These advanced techniques are like specialized traffic management systems for high-volume roads - they help keep your application running smoothly under various conditions.

Real-World Routing and Controller Examples

E-commerce Product Catalog


// routes/web.php
Route::prefix('products')->name('products.')->group(function () {
    Route::get('/', [ProductController::class, 'index'])->name('index');
    Route::get('/category/{category}', [ProductController::class, 'category'])->name('category');
    Route::get('/{product}', [ProductController::class, 'show'])->name('show');
});

// ProductController.php
class ProductController extends Controller
{
    public function index(Request $request)
    {
        $products = Product::query();
        
        if ($search = $request->input('search')) {
            $products->where('name', 'like', "%{$search}%");
        }
        
        $products = $products->paginate(12);
        
        return view('products.index', compact('products'));
    }
    
    public function category(Category $category)
    {
        $products = $category->products()->paginate(12);
        
        return view('products.category', compact('category', 'products'));
    }
    
    public function show(Product $product)
    {
        $relatedProducts = $product->category->products()
            ->where('id', '!=', $product->id)
            ->limit(4)
            ->get();
            
        return view('products.show', compact('product', 'relatedProducts'));
    }
}
            

User Authentication Flow


// routes/web.php
Route::middleware('guest')->group(function () {
    Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
    Route::post('/login', [AuthController::class, 'login']);
    Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
    Route::post('/register', [AuthController::class, 'register']);
});

Route::middleware('auth')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
    Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
    Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update');
});

// AuthController.php
class AuthController extends Controller
{
    public function showLogin()
    {
        return view('auth.login');
    }
    
    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);
        
        if (Auth::attempt($credentials, $request->filled('remember'))) {
            $request->session()->regenerate();
            
            return redirect()->intended('/dashboard');
        }
        
        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ])->withInput($request->only('email', 'remember'));
    }
    
    // Other methods...
}
            

Best Practices

Routing Best Practices

Controller Best Practices

Practice Activity

Blog Routing System

Create a complete routing system for a blog application with the following features:

  1. Public routes for listing and viewing posts
  2. Admin routes for managing posts, protected by authentication
  3. API routes for accessing post data programmatically
  4. Implement route model binding for posts

Advanced Controller Implementation

Create controllers for your blog with:

  1. Resource controllers for posts and categories
  2. Proper validation for creating and updating posts
  3. Authentication middleware for admin actions
  4. Dependency injection for repositories or services

Real-world Feature Implementation

Implement a search feature for your blog that:

  1. Accepts search terms from a form
  2. Filters posts by title and content
  3. Paginates results
  4. Highlights search terms in results
  5. Provides proper feedback when no results are found

Summary

In the next lecture, we'll explore Blade templating and views, building on our understanding of the Laravel request lifecycle.