PHP & Laravel — Zero to Hero Episode 17: Controllers — Organizing Your Application Logic the Laravel Way

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:


    <?php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;

    class PostController extends Controller
    {
        public function index()
        {
            return "Showing all posts";
        }

        public function create()
        {
            return "Showing create post form";
        }

        public function store(Request $request)
        {
            return "Storing new post";
        }

        public function show($id)
        {
            return "Showing post number: " . $id;
        }

        public function edit($id)
        {
            return "Editing post number: " . $id;
        }

        public function update(Request $request, $id)
        {
            return "Updating post number: " . $id;
        }

        public function destroy($id)
        {
            return "Deleting post number: " . $id;
        }
    }

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:


    <?php

    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\PostController;

    Route::get('/', function () {
        return "Welcome to the Blog";
    })->name('home');

    Route::resource('posts', PostController::class);

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.

php.ini — The Configuration File That Controls How PHP Behaves

When PHP starts up, before it runs a single line of your code, it reads a configuration file. This file tells PHP how to behave — how much memory to allow, how long scripts can run, which extensions to load, how to handle errors. That file is php.ini.

Understanding php.ini is not optional knowledge for a serious PHP developer. Every time a Composer package fails, an extension is missing, or something works on one machine but not another — php.ini is almost always involved.


What Exactly is php.ini

php.ini is a plain text configuration file written in INI format. It is read by PHP once at startup. Every setting inside it controls a specific behavior of the PHP engine.

It is not PHP code. You cannot use variables, functions, or logic inside it. Every line is either a key-value setting, an extension declaration, or a comment.

memory_limit = 256M
max_execution_time = 30
upload_max_filesize = 64M

Three settings. Three behaviors controlled. That's the entire format.


Where is php.ini Located

The location depends on your setup. To find it exactly, run this in your terminal:

php --ini

Output:

Configuration File (php.ini) Path: C:\xampp\php
Loaded Configuration File:         C:\xampp\php\php.ini

The second line is what matters — the actual file being loaded right now. This is especially important because multiple PHP installations can exist on one machine, each with their own php.ini, and PHP will only load one of them.

You can also check it from a PHP file:


    <?php
    echo php_ini_loaded_file();

Or see every loaded configuration file:


    <?php
    phpinfo();

phpinfo() outputs a full page showing which php.ini was loaded, every setting currently active, and every extension currently available.


The Comment Syntax — Semicolons

INI format uses ; as the comment character. Any line starting with ; is completely ignored by PHP:

; This entire line is a comment
; PHP will never read this

This is where the enable/disable pattern for extensions comes from. When an extension line has a ; in front, PHP skips it entirely:

;extension=gd

Remove the semicolon and PHP loads the extension at startup:

extension=gd

Same line. One character difference. Completely different behavior.


What Are Extensions

PHP's core is intentionally lean. It handles the language itself — variables, loops, functions, classes, file I/O. Everything else — image processing, encryption, database drivers, internationalization — lives in extensions.

Extensions are compiled modules (.dll files on Windows, .so files on Linux/macOS) that add new functions and capabilities to PHP. They ship with PHP but are not all active by default. You enable only what your project needs.

In XAMPP on Windows, all extension files sit here:

C:\xampp\php\ext\

You'll find files like php_gd.dll, php_intl.dll, php_sodium.dll — all available but waiting to be enabled in php.ini.


The Most Important Extensions — What They Do

ext-gd — Image Processing

GD is PHP's built-in image manipulation library.

Enable it:

extension=gd

What it unlocks:


    <?php
    ob_clean();

    $image = imagecreatetruecolor(400, 200);

    $background = imagecolorallocate($image, 30, 30, 30);
    $textColor = imagecolorallocate($image, 255, 255, 255);

    imagefill($image, 0, 0, $background);
    imagestring($image, 5, 150, 90, 'Hello GD!', $textColor);

    header('Content-Type: image/png');

    imagepng($image);
    imagedestroy($image);
    exit;

Used for generating CAPTCHA images, creating thumbnails, adding watermarks, resizing uploaded photos, drawing charts — anything involving image creation or manipulation.

Laravel's image packages like intervention/image require this extension.


ext-intl — Internationalization

The intl extension provides formatting and language tools that are locale-aware — meaning they respect regional differences in how dates, numbers, and currencies are displayed.

Enable it:

extension=intl

What it unlocks:


    <?php

    $formatter = new NumberFormatter('en_IN', NumberFormatter::CURRENCY);
    echo $formatter->formatCurrency(125000, 'INR');

    $dateFormatter = new IntlDateFormatter(
        'hi_IN',
        IntlDateFormatter::LONG,
        IntlDateFormatter::NONE
    );
    echo $dateFormatter->format(new DateTime());

₹1,25,000.00
18 जून 2026

Used for multi-language applications, e-commerce platforms showing regional pricing, date formatting per locale, and sorting strings correctly across different languages. Symfony's translation component and several Laravel packages depend on this.


ext-sodium — Modern Cryptography

Sodium is a modern, high-level cryptography library. It handles encryption, decryption, digital signatures, and password hashing using algorithms that are considered secure by current standards.

Enable it:

extension=sodium

What it unlocks:


    <?php

    $key = sodium_crypto_secretbox_keygen();
    $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    $message = "sensitive user data";

    $encrypted = sodium_crypto_secretbox($message, $nonce, $key);
    $decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key);

    echo $decrypted;

sensitive user data

Laravel itself uses Sodium for its encryption layer. JWT libraries, OAuth packages, and any package dealing with tokens or signed payloads typically require this. It became a core PHP extension in PHP 7.2.


ext-zip — ZIP File Handling

Enable it:

extension=zip

Allows PHP to create, read, and extract ZIP archives. Composer itself requires this extension to download and extract packages. If ext-zip is disabled, Composer cannot function.


ext-mbstring — Multibyte String Functions

Enable it:

extension=mbstring

PHP's default string functions like strlen() and strtolower() count bytes, not characters. For ASCII text this works fine. For UTF-8 text — Hindi, Arabic, Chinese, emoji — one character can be 2-4 bytes. mbstring provides multibyte-aware versions:


    <?php

    $text = "नमस्ते";

    echo strlen($text);
    echo mb_strlen($text);

18
6

strlen counted bytes — 18. mb_strlen counted actual characters — 6. Almost every Laravel application needs mbstring enabled.


ext-pdo_mysql — MySQL Database Driver

Enable it:

extension=pdo_mysql

PDO (PHP Data Objects) is PHP's database abstraction layer. pdo_mysql is the MySQL-specific driver. Without it, Laravel cannot connect to a MySQL database at all. This is one of the first extensions you enable when setting up a Laravel project with XAMPP.


ext-curl — HTTP Requests

Enable it:

extension=curl

Allows PHP to make HTTP requests to external APIs and services. Laravel's HTTP client (Http::get(), Http::post()) is built on top of Guzzle, which requires cURL. Any package that talks to an external API will need this.


Key php.ini Settings Beyond Extensions

Extensions are not the only thing php.ini controls. These settings directly affect how your Laravel application runs:

Memory Limit

memory_limit = 256M

Maximum RAM a single PHP script can use. Laravel applications with large datasets, complex queries, or image processing can hit the default 128M limit. Increase it for development:

memory_limit = 512M

Max Execution Time

max_execution_time = 30

Seconds before PHP kills a running script. For CLI scripts and long-running jobs, set it to 0 (unlimited):

max_execution_time = 0

File Upload Settings

upload_max_filesize = 64M
post_max_size = 64M

Controls the maximum file size users can upload. Both settings must be updated together — post_max_size must be equal to or larger than upload_max_filesize.

Error Display

display_errors = On
error_reporting = E_ALL

For development, show all errors. For production, always turn this off:

display_errors = Off
log_errors = On
error_log = C:\xampp\php\logs\php_error.log

Multiple php.ini Files — The Common Confusion

This catches many developers off guard. XAMPP runs two different PHP processes:

Context

PHP Binary

php.ini Used

Web (Apache)

mod_php inside Apache

C:\xampp\php\php.ini

CLI (Terminal)

php.exe

C:\xampp\php\php.ini

Normally they use the same file in XAMPP. But if you have a separate PHP installation on your system — or if your PATH points to a different PHP — CLI and web could be using completely different php.ini files with different extensions enabled.

This is why the classic situation happens — something works in the browser but fails when running php artisan. Always verify which PHP and which php.ini your CLI is using:

php --ini
php -v

Both outputs should match what XAMPP shows in phpMyAdmin's PHP version display.


How Composer Uses This

When you run composer install or composer require, Composer checks which PHP extensions are active in your current php.ini. If a package declares "require": {"ext-gd": "*"} in its composer.json, Composer verifies that ext-gd is enabled before installing.

If it's not enabled, you get:

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - intervention/image requires ext-gd * -> it is missing from your system.

The fix is always the same — open php.ini, find the extension line, remove the semicolon, restart Apache from the XAMPP control panel, and run Composer again.


Workflow — Enabling an Extension Step by Step

  1. Open php.ini at C:\xampp\php\php.ini
  2. Press Ctrl + F, search for the extension name — for example gd
  3. Find the line:
;extension=gd
  1. Remove the semicolon:
extension=gd
  1. Save the file
  2. Restart Apache from XAMPP Control Panel
  3. Verify it's active:
php -m | findstr gd

If gd appears in the output, the extension is loaded and ready.


The Bottom Line

php.ini is PHP's master configuration file — it decides which features PHP has, how much memory it uses, how errors are reported, and how files are handled. Extensions are modular additions to PHP that stay disabled until you enable them by removing a single semicolon. Every time Composer complains about a missing extension, every time a package fails to install, the solution is almost always in php.ini. Knowing this file deeply means you spend less time debugging environment issues and more time writing actual code.

PHP & Laravel — Zero to Hero Episode 17: Controllers — Organizing Your Application Logic the Laravel Way

What Are We Doing in This Post? In Episode 16 we defined routes using closures — anonymous functions directly inside routes/web.php . That w...