What Are We Doing in This Post?
In Episode 16 we defined routes using closures — anonymous functions directly inside routes/web.php. That works fine for simple cases but falls apart quickly in real applications.
Imagine having 50 routes, each with complex logic inside a closure. Your web.php file becomes hundreds of lines of mixed routing and business logic. Impossible to maintain.
Controllers solve this. A controller is a dedicated PHP class that handles the logic for a group of related routes. Routes stay clean — they just point to a controller method. All the actual logic lives in the controller.
This is one of the core principles of the MVC pattern that Laravel is built on.
What is MVC?
MVC stands for Model — View — Controller. It is an architectural pattern that separates an application into three distinct layers.
Real world analogy: Think of a restaurant again. The customer places an order — that is the incoming HTTP request. The waiter takes the order and coordinates everything — that is the Controller. The kitchen prepares the food using ingredients — that is the Model working with the database. The plated dish presented to the customer — that is the View, the HTML response.
Each layer has one job and one job only:
Model — handles data and database interaction.
View — handles what the user sees, the HTML.
Controller — handles the logic in between. Receives the request, asks the Model for data, passes it to the View, returns the response.
We covered Models (Episode 13) and we will cover Views in Episode 18. Today is all about Controllers.
Creating a Controller
Laravel's Artisan command creates controllers for you. Run this:
php artisan make:controller PostController
Laravel creates the file app/Http/Controllers/PostController.php. Open it:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller { // }
This is a blank controller. It extends Laravel's base Controller class — through inheritance from Episode 13, your controller automatically gets access to useful Laravel features.
The namespace App\Http\Controllers tells PHP and Laravel exactly where this class lives — remember namespaces from Episode 14.
Adding Methods to a Controller
Each public method in a controller handles one route. Let us add methods for our blog posts:
These seven methods — index, create, store, show, edit, update, destroy — are the standard RESTful methods. This naming convention is not mandatory but it is so universal in Laravel that every developer immediately understands what each method does.
Connecting Routes to Controller Methods
Now update routes/web.php to point to the controller instead of using closures:
<?php
use Illuminate\Support\Facades\Route; use App\Http\Controllers\PostController;
Route::get('/', function () { return "Welcome to the Blog"; })->name('home');
Route::get('/posts', [PostController::class, 'index'])->name('posts.index'); Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create'); Route::post('/posts', [PostController::class, 'store'])->name('posts.store'); Route::get('/posts/{id}', [PostController::class, 'show'])->name('posts.show'); Route::get('/posts/{id}/edit', [PostController::class, 'edit'])->name('posts.edit'); Route::patch('/posts/{id}', [PostController::class, 'update'])->name('posts.update'); Route::delete('/posts/{id}', [PostController::class, 'destroy'])->name('posts.destroy');
The syntax [PostController::class, 'index'] means: use the index method of the PostController class. This is an array with the class reference and method name.
Notice the use App\Http\Controllers\PostController at the top — importing the class by its full namespace so we can reference it by short name.
Run php artisan route:list — you will see all routes now pointing to PostController@methodname instead of closures.
Visit http://127.0.0.1:8000/posts — you see: Showing all posts.
Visit http://127.0.0.1:8000/posts/5 — you see: Showing post number: 5.
Resource Controllers — All Seven Routes in One Line
Because index, create, store, show, edit, update, destroy is such a universal pattern, Laravel provides a shortcut that generates all seven routes with one line.
First delete the seven individual route lines you just wrote and replace them with:
Run php artisan route:list again. You will see all seven routes are still there — identical to what you wrote manually — but now generated from one line.Route::resource() automatically creates these routes:
GET /posts posts.index index()
GET /posts/create posts.create create()
POST /posts posts.store store()
GET /posts/{post} posts.show show()
GET /posts/{post}/edit posts.edit edit()
PATCH /posts/{post} posts.update update()
DELETE /posts/{post} posts.destroy destroy()
This is the standard way to define CRUD routes in Laravel. One line replaces seven.
The Request Object — Reading Incoming Data
When a form is submitted or data is sent to your application, Laravel wraps all of it in a Request object. You have already seen it in method signatures — store(Request $request) and update(Request $request, $id).
The Request object gives you clean access to all incoming data.
Let us update the store method to actually read submitted data:
<?php
public function store(Request $request) { $title = $request->input('title'); $body = $request->input('body');
return "Creating post: " . $title; }
$request->input('fieldname') reads a value from the submitted form data — works for both POST body and query string parameters.
Other useful Request methods:
<?php
$request->all(); $request->only(['title', 'body']); $request->except(['_token']); $request->has('title'); $request->filled('title'); $request->method(); $request->isMethod('post'); $request->url(); $request->path(); $request->ip();
$request->all() returns every piece of submitted data as an array.
$request->only([...]) returns only the specified fields — useful for filtering out unwanted input before saving to the database.
$request->except([...]) returns everything except the specified fields.
$request->has('title') returns true if the field exists in the request.
$request->filled('title') returns true if the field exists and is not empty.
Returning Different Response Types
Controller methods can return different types of responses depending on what the route needs to send back.
Return a string:
<?php
public function index() { return "Hello from the controller"; }
Return a JSON response:
<?php
public function index() { $data = [ 'posts' => ['Post One', 'Post Two', 'Post Three'], 'total' => 3 ];
return response()->json($data); }
Visit /posts and you will see properly formatted JSON. This is how API endpoints work in Laravel.
Return a view:
<?php
public function index() { return view('posts.index'); }
This returns a Blade template file. We cover views fully in Episode 18 — for now just know that view('posts.index') looks for the file resources/views/posts/index.blade.php.
Return a redirect:
<?php
public function store(Request $request) { return redirect()->route('posts.index'); }
After storing data, you redirect the user to another page. redirect()->route('posts.index') redirects to the named route posts.index.
Return with a flash message:
<?php
public function store(Request $request) { return redirect()->route('posts.index')->with('success', 'Post created successfully!'); }
->with() flashes a message to the session — it is available on the next request only. Useful for showing success or error messages after form submissions.
A Complete Working Controller Example
Let us build a controller that actually works with real data — no database yet, just arrays — so you can see the full flow working end to end.
First create a simple view file. Create the folder resources/views/posts/ and inside it create index.blade.php:
<!DOCTYPE html> <html>
<head> <title>All Posts</title> </head>
<body> <h1>All Posts</h1>
@foreach ($posts as $post) <div style="border: 1px solid #ccc; padding: 16px; margin: 12px 0;"> <h2>{{ $post['title'] }}</h2> <p>{{ $post['body'] }}</p> <small>By: {{ $post['author'] }}</small> </div> @endforeach
</body>
</html>
Do not worry about the @foreach and {{ }} syntax yet — that is Blade which we cover in Episode 18. Just type it as shown.
Now update PostController:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller { private $posts = [ [ 'id' => 1, 'title' => 'Getting Started With Laravel', 'body' => 'Laravel is a powerful PHP framework that makes web development enjoyable.', 'author' => 'Gagan', 'status' => 'published' ], [ 'id' => 2, 'title' => 'Understanding MVC Architecture', 'body' => 'MVC separates your application into Models, Views, and Controllers.', 'author' => 'Rahul', 'status' => 'published' ], [ 'id' => 3, 'title' => 'Working With Eloquent ORM', 'body' => 'Eloquent makes database queries feel like writing plain English.', 'author' => 'Priya', 'status' => 'draft' ], ];
public function index() { return view('posts.index', ['posts' => $this->posts]); }
public function show($id) { $post = collect($this->posts)->firstWhere('id', (int)$id);
if (!$post) { abort(404, 'Post not found'); }
return response()->json($post); }
public function store(Request $request) { $title = $request->input('title', 'Untitled'); $body = $request->input('body', ''); $author = $request->input('author', 'Anonymous');
return response()->json([ 'message' => 'Post created successfully', 'post' => ['title' => $title, 'body' => $body, 'author' => $author] ]); }
public function destroy($id) { return response()->json([ 'message' => "Post $id deleted successfully" ]); } }
Visit http://127.0.0.1:8000/posts — you see all three posts rendered in the browser.
Visit http://127.0.0.1:8000/posts/1 — you see the first post as JSON.
Visit http://127.0.0.1:8000/posts/99 — you get a 404 error page.
Two things to highlight from this example.
Passing data to views:
<?php
return view('posts.index', ['posts' => $this->posts]);
The second argument to view() is an associative array of data to pass to the template. The key becomes the variable name inside the view. So 'posts' => $this->posts becomes $posts in the Blade template.
abort(404):
<?php
abort(404, 'Post not found');
abort() immediately stops execution and returns an HTTP error response. Laravel has a beautiful error page for 404, 403, 500, and other status codes automatically.
Single Action Controllers
Sometimes a controller handles just one action — a homepage, a settings page, a dashboard. For these, Laravel has single action controllers.
php artisan make:controller HomeController --invokable
This creates a controller with a single __invoke() method:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HomeController extends Controller { public function __invoke(Request $request) { return "Welcome to the homepage"; } }
In routes/web.php:
<?php
use App\Http\Controllers\HomeController;
Route::get('/', HomeController::class);
No method name needed — Laravel calls __invoke() automatically. Clean for single-purpose controllers.
What Did We Learn in This Post?
MVC separates applications into Models (data), Views (display), and Controllers (logic in between).
Controllers are PHP classes in app/Http/Controllers/ that group related route logic. Created with php artisan make:controller ControllerName.
Route methods now point to controller methods using [ControllerName::class, 'methodName'] syntax. Routes stay clean — all logic moves into the controller.
Route::resource() generates all seven RESTful routes in one line — index, create, store, show, edit, update, destroy.
The Request object gives clean access to all incoming data through methods like input(), all(), only(), except(), has(), and filled().
Controllers can return strings, JSON responses via response()->json(), views via view(), or redirects via redirect()->route().
abort(404) immediately returns an error response and stops execution.
Single action controllers with --invokable are clean for controllers that handle just one action.
What is Coming in Episode 18?
Our controllers are returning views — but we have barely touched views yet. In Episode 18 we dive into Blade, Laravel's templating engine.
Blade gives you clean syntax for displaying data, loops, conditionals, and most powerfully — template inheritance, where you define a master layout once and every page extends it. No more copy-pasting the same HTML header and footer across every file.
See you in the next one.
No comments:
Post a Comment