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

$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);

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 16: Routing and Migrations — Defining URLs and Building Your Database With Code

What Are We Doing in This Post?

In Episode 15 we installed Laravel and understood the folder structure. We saw a basic route in action — a URL mapped to a response in routes/web.php.

In this episode we go deep into two of the most fundamental Laravel features.

Routing — the system that decides what happens when a user visits a URL. Every URL your application responds to is defined through routing.

Migrations — the system that defines your database table structure in PHP code. Instead of creating tables manually in phpMyAdmin, you write a migration file and Laravel creates the table for you with one command.

These two features are the backbone of every Laravel application. Let us go deep.


Part 1 — Routing

What is a Route?

A route is a mapping between a URL and what should happen when that URL is visited.

Real world analogy: Think of routes like a reception desk at a large office building. Every visitor who walks in says where they want to go. The receptionist checks the directory and says — "Third floor, room 302." The route is that directory entry. The URL is the visitor's destination request. Laravel checks the routes file and figures out what to do with that request.

Without routes, Laravel has no idea what to do when someone visits a URL. Every single URL your application responds to must be defined in routes/web.php.


Basic Route Syntax

Open routes/web.php and look at the structure:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return "Hello from Laravel!";
});

Route::get() registers a route that responds to HTTP GET requests. The first argument is the URL path. The second argument is what to do when that URL is visited — currently a closure (an anonymous function).

Laravel supports all HTTP methods:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/page', function () {
    return "This handles GET requests";
});

Route::post('/page', function () {
    return "This handles POST requests — form submissions";
});

Route::put('/page', function () {
    return "This handles PUT requests — full updates";
});

Route::patch('/page', function () {
    return "This handles PATCH requests — partial updates";
});

Route::delete('/page', function () {
    return "This handles DELETE requests";
});

In a real application, GET is used for displaying pages, POST for creating data, PUT/PATCH for updating data, and DELETE for removing data.


Route Parameters — Dynamic URLs

Real websites have dynamic URLs. A blog post URL like /posts/15 or a user profile like /users/gagan. The number or name changes per request — but the route structure is the same.

Route parameters let you capture that dynamic part of the URL.

<?php

use Illuminate\Support\Facades\Route;

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

Route::get('/posts/{slug}', function ($slug) {
    return "Showing post: " . $slug;
});

Visit http://127.0.0.1:8000/users/42 — you see: Showing user with ID: 42

Visit http://127.0.0.1:8000/users/99 — you see: Showing user with ID: 99

The {id} in the route becomes the $id parameter in the function automatically. Whatever is in that URL segment gets passed directly.

Multiple parameters:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/categories/{category}/posts/{id}', function ($category, $id) {
    return "Category: $category | Post ID: $id";
});

Visit http://127.0.0.1:8000/categories/technology/posts/5 — you see: Category: technology | Post ID: 5

Optional parameters:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/products/{category?}', function ($category = "all") {
    return "Showing products in category: " . $category;
});

The ? makes the parameter optional. If the user visits /products, the default value "all" is used. If they visit /products/laptops, $category becomes "laptops".


Route Constraints — Validating URL Parameters

You can restrict what values a route parameter accepts using where().

<?php

use Illuminate\Support\Facades\Route;

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

Route::get('/posts/{slug}', function ($slug) {
    return "Post: " . $slug;
})->where('slug', '[a-z\-]+');

The first route only matches if {id} is one or more digits. Visiting /users/abc will return a 404 — because abc does not match [0-9]+.

The second route only matches if {slug} contains only lowercase letters and hyphens.

This is important for security and correctness — you do not want someone passing arbitrary strings where you expect a numeric ID.


Named Routes — Giving Routes a Name

Instead of hardcoding URLs throughout your application, you give routes names and reference them by name. This way if you ever change a URL, you only change it in one place — the route definition — and everywhere that uses the route name automatically updates.

<?php

use Illuminate\Support\Facades\Route;

Route::get('/user/profile', function () {
    return "User profile page";
})->name('profile');

Route::get('/dashboard', function () {
    return "Dashboard page";
})->name('dashboard');

Route::get('/posts/{id}', function ($id) {
    return "Post number: " . $id;
})->name('posts.show');

Now you can generate URLs using route names anywhere in your application:

<?php

$url = route('profile');
$url = route('posts.show', ['id' => 15]);

In Blade templates you will use:

<a href="{{ route('profile') }}">My Profile</a>
<a href="{{ route('posts.show', ['id' => 15]) }}">Read Post</a>

This is standard practice in all Laravel applications. Never hardcode URLs — always use named routes.


Route Groups — Organizing Related Routes

When multiple routes share a common prefix or middleware, you group them instead of repeating the same configuration on every route.

Prefix grouping:

<?php

use Illuminate\Support\Facades\Route;

Route::prefix('admin')->group(function () {
    Route::get('/dashboard', function () {
        return "Admin Dashboard";
    });

    Route::get('/users', function () {
        return "Admin Users List";
    });

    Route::get('/settings', function () {
        return "Admin Settings";
    });
});

These three routes respond to /admin/dashboard, /admin/users, and /admin/settings. The prefix admin is defined once on the group — not repeated on every route.

Named prefix grouping:

<?php

use Illuminate\Support\Facades\Route;

Route::prefix('admin')->name('admin.')->group(function () {
    Route::get('/dashboard', function () {
        return "Admin Dashboard";
    })->name('dashboard');

    Route::get('/users', function () {
        return "Admin Users";
    })->name('users');
});

Now the routes are named admin.dashboard and admin.users. Clean, organized, and consistent.


Viewing All Registered Routes

Laravel gives you an Artisan command to see every route your application has registered:

php artisan route:list

Run this now. You will see a table with the HTTP method, URI, name, and action for every route. This is one of the most useful debugging tools in Laravel — when a route is not working as expected, this command tells you exactly what Laravel knows about.


Part 2 — Migrations

What is a Migration?

A migration is a PHP file that defines a database table's structure in code.

Real world analogy: Think of a migration like an instruction manual for building a specific piece of furniture. The manual describes exactly what pieces to use, what dimensions to cut, how to assemble everything. Your colleague can take that same instruction manual, follow it step by step, and build an identical piece of furniture. Migrations do the same thing — any developer on your team runs php artisan migrate and gets an identical database structure, no matter what machine they are on.

Before migrations, developers would manually create tables in phpMyAdmin and then email each other SQL files to run. This was error-prone, unversioned, and chaos in team environments.

With migrations, your database structure lives in code, inside version control, alongside your application. It is the only professional way to manage databases in modern PHP development.


The Migration Files Laravel Created

Open database/migrations/ in VS Code. You will see three files already there:

0001_01_01_000000_create_users_table.php
0001_01_01_000001_create_cache_table.php
0001_01_01_000002_create_jobs_table.php

Open 0001_01_01_000000_create_users_table.php and look at the structure:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Every migration has two methods.

up() runs when you execute php artisan migrate — it creates or modifies the table.

down() runs when you execute php artisan migrate:rollback — it undoes whatever up() did. This is your undo button.

Inside up(), Schema::create() takes the table name and a closure. Inside the closure, $table is a Blueprint object — it has methods for every column type you could need.

$table->id() creates an auto-incrementing unsigned big integer primary key column named id.

$table->string('name') creates a VARCHAR(255) column.

$table->string('email')->unique() creates a VARCHAR(255) column with a unique constraint — no two rows can have the same email.

$table->timestamp('email_verified_at')->nullable() creates a timestamp column that can be null.

$table->rememberToken() creates a remember_token VARCHAR(100) column — used for "remember me" login functionality.

$table->timestamps() creates two columns automatically — created_at and updated_at. Laravel updates these automatically when records are created or modified.


Creating Your First Custom Migration

Let us create a migration for a posts table — the kind you would use for a blog application.

Run this Artisan command:

php artisan make:migration create_posts_table

Laravel creates a new migration file in database/migrations/ with a timestamp in the filename. Open it:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {

        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Now fill in the up() method with the columns your posts table needs:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->string('status')->default('draft');
            $table->unsignedInteger('views')->default(0);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Let us understand each column.

$table->id() — primary key, auto-increment.

$table->foreignId('user_id')->constrained()->onDelete('cascade') — creates a user_id column and a foreign key constraint pointing to the users table's id column. onDelete('cascade') means if a user is deleted, all their posts are automatically deleted too.

$table->string('title') — VARCHAR(255) for the post title.

$table->string('slug')->unique() — URL-friendly version of the title, must be unique. Like my-first-post.

$table->text('body') — TEXT column for the full post content. Unlike string, text has no length limit.

$table->string('status')->default('draft') — post status, defaults to draft. Can be changed to published.

$table->unsignedInteger('views')->default(0) — view counter, starts at 0, cannot be negative.

$table->timestamp('published_at')->nullable() — when the post was published. Null means not published yet.

$table->timestamps()created_at and updated_at managed automatically by Laravel.


Common Column Types

Here are the most used Blueprint column methods you will use in real projects:

<?php

$table->id();
$table->string('name');
$table->string('email', 100);
$table->text('description');
$table->longText('content');
$table->integer('age');
$table->unsignedInteger('views');
$table->bigInteger('file_size');
$table->float('rating');
$table->decimal('price', 8, 2);
$table->boolean('is_active');
$table->date('birth_date');
$table->timestamp('published_at');
$table->timestamps();
$table->softDeletes();
$table->foreignId('user_id')->constrained();
$table->json('metadata');
$table->enum('status', ['draft', 'published', 'archived']);

decimal('price', 8, 2) — 8 total digits, 2 after decimal point. Perfect for prices.

boolean('is_active') — stores true/false as 1/0 in MySQL.

softDeletes() — adds a deleted_at column. Instead of actually deleting rows, Laravel marks them with a timestamp. The rows stay in the database but are hidden from normal queries. Essential for applications where you need to recover deleted data.

json('metadata') — stores JSON data directly in a column. MySQL parses it natively.

enum('status', [...]) — restricts the column to only the listed values.


Running, Rolling Back, and Refreshing Migrations

Run all pending migrations:

php artisan migrate

Rollback the last batch of migrations:

php artisan migrate:rollback

This calls the down() method of the last batch of migrations — undoing the last migrate operation.

Rollback a specific number of steps:

php artisan migrate:rollback --step=2

Drop everything and start fresh:

php artisan migrate:fresh

This drops all tables and reruns every migration from scratch. Use this during development when you want a clean slate. Never run it on a production database — all data will be lost.

Check migration status:

php artisan migrate:status

Shows every migration file and whether it has been run or not.


A Complete Example — Blog Routes and Migration Together

Let us put everything together. Update routes/web.php with a set of blog routes:

<?php

use Illuminate\Support\Facades\Route;

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

Route::prefix('posts')->name('posts.')->group(function () {

    Route::get('/', function () {
        return "All Posts";
    })->name('index');

    Route::get('/create', function () {
        return "Create New Post Form";
    })->name('create');

    Route::post('/', function () {
        return "Store new post in database";
    })->name('store');

    Route::get('/{id}', function ($id) {
        return "Showing post number: " . $id;
    })->where('id', '[0-9]+')->name('show');

    Route::get('/{id}/edit', function ($id) {
        return "Edit post number: " . $id;
    })->where('id', '[0-9]+')->name('edit');

    Route::patch('/{id}', function ($id) {
        return "Update post number: " . $id;
    })->where('id', '[0-9]+')->name('update');

    Route::delete('/{id}', function ($id) {
        return "Delete post number: " . $id;
    })->where('id', '[0-9]+')->name('destroy');
});

Now run:

php artisan route:list

You will see all your blog routes listed cleanly — their HTTP methods, URIs, names, and actions. This set of seven routes — index, create, store, show, edit, update, destroy — is the standard RESTful resource pattern. It is so common that Laravel can generate all seven with a single line. We will cover that in the next episode when we introduce controllers.

Now run your posts migration:

php artisan migrate

Check phpMyAdmin at http://localhost:8080/phpmyadmin — open laravel_myapp database. You will see the posts table with every column exactly as you defined it in the migration.


What Did We Learn in This Post?

Routes map URLs to actions. Route::get(), Route::post(), Route::put(), Route::patch(), and Route::delete() handle different HTTP methods.

Route parameters capture dynamic URL segments using {parameter} syntax. Optional parameters use {parameter?}. Constraints with where() restrict what values are accepted.

Named routes with ->name() let you reference URLs by name instead of hardcoding them — essential for maintainable applications.

Route groups with prefix() and name() organize related routes cleanly without repeating configuration.

php artisan route:list shows every registered route — your best debugging tool for routing issues.

Migrations define database table structure in PHP code. up() creates or modifies. down() rolls back. Every column type has a dedicated Blueprint method.

php artisan migrate runs migrations. php artisan migrate:rollback undoes the last batch. php artisan migrate:fresh drops everything and starts clean.


What is Coming in Episode 17?

Right now our routes use closures — anonymous functions directly in the route definition. That is fine for simple cases but becomes unmanageable in real applications.

Episode 17 covers Controllers — dedicated PHP classes that handle request logic. We will move our route logic into controllers, create a full PostController with all seven RESTful methods, and introduce Laravel's resource routing shortcut that generates all seven routes in one line.

See you in the next one.


Next Episode: Controllers — Organizing Your Application Logic the Laravel Way

This is Episode 16 of the PHP and Laravel — Zero to Hero series.


PHP & Laravel — Zero to Hero Episode 15: Installing Laravel on Windows 11 — Complete Guide

What Are We Doing in This Post?

Core PHP is done. Fourteen episodes of solid foundation. Now we install Laravel.

But here is the honest truth — Laravel installation on Windows 11 with XAMPP is not always one command and done. There are real errors that hit real beginners. In this episode we cover the complete installation process AND every error you might face, with exact fixes.

By the end of this episode, Laravel will be running on your machine and you will understand exactly why each step exists.


What You Need Before Starting

Make sure these are ready:

XAMPP installed at C:\xampp with Apache and MySQL working — covered in Episode 02.

PHP 8.2 — comes with XAMPP. Verify by opening Command Prompt and typing:

php -v

You should see PHP 8.2.x. If not, reinstall XAMPP from apachefriends.org.

Composer installed — covered in Episode 14. Verify:

composer --version

You should see Composer version 2.x.x.


Step 1 — Enable the ZIP Extension in PHP

This is the most commonly missed step on Windows. Composer needs PHP's zip extension to download and extract packages. Without it, nothing installs.

Open XAMPP Control Panel. In the Apache row, click Config, then click PHP (php.ini). The file opens in your editor.

Press Ctrl+F and search for:

;extension=zip

Remove the semicolon from the beginning of that line so it becomes:

extension=zip

Save the file with Ctrl+S.

In XAMPP Control Panel, click Stop next to Apache, then click Start again.

Verify the extension is now active — open Command Prompt and run:

php -m | findstr zip

If zip appears in the output, the extension is enabled. Move to Step 2.

If nothing appears, double check that you removed the semicolon from the correct line and that you restarted Apache after saving.


Step 2 — Fix Composer Security Advisory Block

By default, newer versions of Composer block packages that have known security advisories. Laravel 11's packages have some logged advisories — not dangerous for local development, but Composer refuses to install them unless you explicitly allow it.

Run this command once to disable the block globally:

composer config -g policy.advisories.block false

This setting applies to all future Composer operations on your machine. You only need to run it once.


Step 3 — Download Laravel Manually

The composer create-project command and the laravel new command both fail on PHP 8.2 with Windows 11 because the Laravel installer itself requires PHP 8.3. We bypass this by downloading Laravel directly as a ZIP file.

Open your browser and go to this URL:

https://github.com/laravel/laravel/archive/refs/tags/v11.0.0.zip

A ZIP file will download — around 35 KB.

Open File Explorer. Go to C:\xampp\htdocs. Extract the ZIP file here. You will get a folder called laravel-11.0.0. Rename it to myapp.

Your Laravel project folder is now at:

C:\xampp\htdocs\myapp

Step 4 — Update composer.json

The default composer.json that comes with the Laravel ZIP has strict version requirements that will conflict with Composer's package resolver. Replace the entire contents of C:\xampp\htdocs\myapp\composer.json with this:


    {
        "name": "laravel/laravel",
        "type": "project",
        "require": {
            "php": "^8.2",
            "laravel/framework": "^11.0"
        },
        "config": {
            "optimize-autoloader": true,
            "preferred-install": "dist",
            "sort-packages": true,
            "allow-plugins": {
                "pestphp/pest-plugin": true,
                "php-http/discovery": true
            }
        },
        "extra": {
            "laravel": {
                "dont-discover": []
            }
        },
        "autoload": {
            "psr-4": {
                "App\\": "app/",
                "Database\\Factories\\": "database/factories/",
                "Database\\Seeders\\": "database/seeders/"
            }
        },
        "autoload-dev": {
            "psr-4": {
                "Tests\\": "tests/"
            }
        },
        "minimum-stability": "stable",
        "prefer-stable": true
    }

Save the file.


Step 5 — Install Laravel Dependencies

Open PowerShell or Command Prompt. Navigate to your project folder:

cd C:\xampp\htdocs\myapp

Run:

composer install --ignore-platform-reqs

Composer will now download all Laravel dependencies — around 70 packages. This takes two to five minutes depending on your internet speed.

When it finishes you will see:

Generating optimized autoload files

That means all packages installed successfully.


Step 6 — Create the .env File and Generate App Key

Laravel needs a .env file for environment configuration. Run these commands one by one — in PowerShell use separate commands, not &&:

copy .env.example .env
php artisan key:generate

You will see:

Application key set successfully.

Step 7 — Configure the Database

Open C:\xampp\htdocs\myapp\.env in VS Code.

You will see a default SQLite configuration. We are using MySQL instead. Find the database section and replace it with this:


    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=laravel_myapp
    DB_USERNAME=root
    DB_PASSWORD=
    DB_COLLATION=utf8mb4_unicode_ci

The DB_COLLATION line is critical on XAMPP. Without it you will get this error:

SQLSTATE[HY000]: General error: 1273 Unknown collation: 'utf8mb4_0900_ai_ci'

This happens because XAMPP's MySQL is an older version that does not support the newer utf8mb4_0900_ai_ci collation that Laravel 11 uses by default. Setting it to utf8mb4_unicode_ci fixes it permanently.

Save the .env file.


Step 8 — Create the Database in phpMyAdmin

Make sure MySQL is running in XAMPP Control Panel. Since you changed Apache's port to 8080 in Episode 02, open phpMyAdmin at:

http://localhost:8080/phpmyadmin

In the left sidebar click New. In the database name field type:

laravel_myapp

Leave the collation as default and click Create.

We create a separate database for Laravel — keeping it separate from the phplearning database we used for Core PHP practice. Mixing them causes migration conflicts.


Step 9 — Run Migrations

Laravel comes with default migrations that create the users, cache, and jobs tables. Run them:

php artisan migrate

You should see:

INFO  Preparing database.
  Creating migration table .... DONE

INFO  Running migrations.
  0001_01_01_000000_create_users_table .... DONE
  0001_01_01_000001_create_cache_table .... DONE
  0001_01_01_000002_create_jobs_table .... DONE

All three migrations done means your database is connected and working perfectly.


Step 10 — Start the Laravel Server

php artisan serve

You will see:

INFO  Server running on [http://127.0.0.1:8000].

Open your browser and go to:

http://127.0.0.1:8000

You will see the Laravel welcome page — dark background, Laravel logo, links to documentation and ecosystem tools.

Laravel is running.


Every Error You Might Face — And The Fix

Let us document every error that can appear during this process and exactly how to fix each one.


Error 1 — zip extension missing

The zip extension and unzip/7z commands are both missing, skipping.

Cause: PHP's zip extension is disabled in php.ini.

Fix: Open C:\xampp\php\php.ini, find ;extension=zip, remove the semicolon, save, restart Apache.


Error 2 — PHP version requirement

Cannot use laravel/laravel's latest version as it requires php ^8.3

Cause: Laravel 13 requires PHP 8.3. XAMPP currently ships with PHP 8.2.

Fix: Use the manual ZIP download method from Step 3 with Laravel 11, which fully supports PHP 8.2. Use --ignore-platform-reqs flag with Composer.


Error 3 — Security advisory block

found laravel/framework but these were not loaded, because they are affected by security advisories

Cause: Composer's advisory block feature prevents installation of packages with logged security notices.

Fix: Run composer config -g policy.advisories.block false once globally.


Error 4 — Laravel installer parse error

PHP Parse error: syntax error, unexpected token "->" in Command.php on line 681

Cause: The global laravel new installer requires PHP 8.3. Running it on PHP 8.2 causes this parse error.

Fix: Do not use laravel new. Use the manual ZIP download method instead.


Error 5 — Unknown collation

SQLSTATE[HY000]: General error: 1273 Unknown collation: 'utf8mb4_0900_ai_ci'

Cause: XAMPP's MySQL version does not support the utf8mb4_0900_ai_ci collation that Laravel 11 sets by default.

Fix: Add DB_COLLATION=utf8mb4_unicode_ci to your .env file.


Error 6 — SQLite database file does not exist

Database file at path [C:\xampp\htdocs\myapp\database\database.sqlite] does not exist.

Cause: Laravel defaults to SQLite and tries to find a sqlite file that was never created.

Fix: Switch to MySQL in .env by setting DB_CONNECTION=mysql and filling in the other DB values.


Error 7 — Table already exists

SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists

Cause: You pointed Laravel at a database that already has a users table — like the phplearning database from Core PHP practice.

Fix: Create a separate fresh database called laravel_myapp in phpMyAdmin and update DB_DATABASE in .env.


Error 8 — && not valid in PowerShell

The token '&&' is not a valid statement separator in this version.

Cause: PowerShell does not support && to chain commands like Command Prompt does.

Fix: Run each command on a separate line in PowerShell. Never chain with && in PowerShell.


Understanding the Folder Structure

Now that Laravel is running, let us understand what was created. Open C:\xampp\htdocs\myapp in VS Code.

myapp/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   └── Middleware/
│   ├── Models/
│   └── Providers/
├── bootstrap/
├── config/
├── database/
│   ├── factories/
│   ├── migrations/
│   └── seeders/
├── public/
├── resources/
│   └── views/
├── routes/
│   ├── web.php
│   └── api.php
├── storage/
├── vendor/
├── .env
├── artisan
└── composer.json

app/Http/Controllers/ — Every controller you write goes here. Controllers handle incoming requests and return responses.

app/Models/ — Every Eloquent model goes here. Each model represents one database table.

app/Http/Middleware/ — Middleware are gatekeepers that run before requests reach your controller. Authentication checks, rate limiting — all middleware.

config/ — All configuration files. They read values from .env using the env() helper.

database/migrations/ — Migration files that define your database table structure in PHP code. Run with php artisan migrate.

database/seeders/ — Files that insert test data into your database with one command.

public/ — The only folder exposed to the internet. public/index.php is the single entry point for every request. All your CSS, JS, and image files that the browser downloads go here.

resources/views/ — All Blade template files live here. These are your HTML files with .blade.php extension.

routes/web.php — Every URL your application responds to is defined here. Open it right now and you will see:


    <?php

    use Illuminate\Support\Facades\Route;

    Route::get('/', function () {
        return view('welcome');
    });

This one line is why you saw the Laravel welcome page. A GET request to / returns the welcome view.

.env — Environment configuration. Database credentials, API keys, debug mode. Never commit this file to version control.

vendor/ — All Composer packages. Never edit anything here manually.

artisan — Laravel's command line tool. You will use it constantly.


Your First Change — Proof That Everything Works

Open routes/web.php and replace everything with:


    <?php

    use Illuminate\Support\Facades\Route;

    Route::get('/', function () {
        return "Laravel is working. The foundation is complete.";
    });

    Route::get('/about', function () {
        return "This is the about page.";
    });

Save the file. Go to http://127.0.0.1:8000 — you see your message.

Go to http://127.0.0.1:8000/about — you see the about page.

Two URLs, two responses, zero file naming confusion. You defined the route — Laravel handled everything else.


Quick Reference — Commands You Will Use Every Day

php artisan serve

Starts the development server at http://127.0.0.1:8000

php artisan make:controller UserController

Creates a new controller file

php artisan make:model Product

Creates a new Eloquent model

php artisan make:migration create_products_table

Creates a new migration file

php artisan migrate

Runs all pending migrations

php artisan migrate:fresh

Drops all tables and reruns all migrations from scratch

php artisan route:list

Shows all registered routes in your application

php artisan tinker

Opens an interactive PHP shell with full Laravel access

php artisan cache:clear

Clears the application cache


What Did We Learn in This Post?

Laravel installation on Windows 11 with XAMPP requires enabling the zip extension, disabling Composer's advisory block, and using PHP version compatible installation methods.

The manual ZIP download of Laravel 11 with --ignore-platform-reqs is the most reliable method on PHP 8.2.

The DB_COLLATION=utf8mb4_unicode_ci setting in .env is essential to avoid collation errors with XAMPP's MySQL version.

Always create a separate database for Laravel — never point it at an existing database that has conflicting tables.

In PowerShell, always run commands one at a time — && chaining does not work.

The folder structure has clear separation of concerns — controllers in app/Http/Controllers, models in app/Models, views in resources/views, routes in routes/web.php.

php artisan serve starts the development server. The artisan command line tool is used throughout the entire Laravel development workflow.


What is Coming in Episode 16?

Now that Laravel is installed and running, we start using it properly.

Episode 16 covers routing in depth — route parameters, named routes, route groups, and middleware on routes. Then we build our first custom migration and create a real database table using Laravel's migration system. The actual Laravel development begins here.

See you in the next one.


Next Episode: Routing and Migrations — Defining URLs and Building Your Database With Code

This is Episode 15 of the PHP and Laravel — Zero to Hero series.


How PHP Embeds Into HTML — And Can It Work Inside JavaScript?

One of PHP's most unique characteristics is that it doesn't live in its own isolated file waiting to be called. It can sit directly inside HTML, wrap around markup, inject into attributes, and even interact with JavaScript in specific ways. Understanding exactly how this works — and where the limits are — is fundamental to writing PHP the right way.


Why This Is Even Possible

The key is understanding what PHP actually is at the server level.

PHP is a server-side preprocessor. Before your browser receives a single byte of HTML, the web server hands the .php file to the PHP engine. The PHP engine scans the file looking for <?php ?> tags. Everything inside those tags gets executed as PHP code. Everything outside those tags gets passed through as-is — plain text, HTML, CSS, JavaScript, whatever it is.

The browser never sees PHP. It receives the final rendered output — pure HTML.

Your .php file  →  PHP Engine processes it  →  Pure HTML sent to browser

This is fundamentally different from JavaScript which runs in the browser after the HTML arrives. PHP is completely done before the browser even enters the picture.


Pattern 1 — PHP Inside HTML

This is the most common pattern. Your file is mostly HTML and PHP drops in to output dynamic values:


    <!DOCTYPE html>
    <html>

    <head>
        <title>Profile Page</title>
    </head>

    <body>

        <h1>Hello, <?php echo $name; ?></h1>
        <p>You have <?php echo $messageCount; ?> new messages.</p>

        <ul>
            <?php foreach ($notifications as $note): ?>
                <li><?php echo $note; ?></li>
            <?php endforeach; ?>
        </ul>

    </body>

    </html>

The PHP engine sees <?php echo $name; ?>, executes it, replaces that entire tag with the output value, and the browser receives clean HTML with no PHP visible anywhere.

Short Echo Tag

PHP also supports a shorthand for echoing values:


    <h1>Hello, <?= $name ?></h1>
    <p>Messages: <?= $messageCount ?></p>

<?= is shorthand for <?php echo. It's enabled by default since PHP 5.4 and is perfectly acceptable in modern PHP — especially inside template files where you're echoing frequently.


Pattern 2 — HTML Inside PHP

Here your file starts in PHP mode and you drop out into HTML when needed:


    <?php
    $isLoggedIn = true;
    $username = "Gagan";
    $role = "admin";

    if ($isLoggedIn) {
    ?>
        <div class="dashboard">
            <h2>Welcome back, <?= $username ?></h2>
            <span class="badge"><?= $role ?></span>
        </div>
    <?php
    } else {
    ?>
        <div class="login-prompt">
            <p>Please log in to continue.</p>
            <a href="/login">Login</a>
        </div>
    <?php
    }
    ?>

Output:

Welcome back, Gagan

admin

The moment PHP sees ?> it exits PHP mode and treats everything after as raw output until it hits another <?php. You're literally switching the parser back and forth.


Pattern 3 — PHP Inside HTML Attributes

This is where it gets interesting. PHP can output values directly inside HTML attribute strings:


    <a href="/profile/<?= $userId ?>">View Profile</a>

    <img src="<?= $avatarUrl ?>" alt="<?= $username ?>">

    <input type="text" name="email" value="<?= $formData['email'] ?>">

    <div class="card <?= $isActive ? 'active' : 'inactive' ?>">
        Content here
    </div>

Since PHP just outputs a string, it doesn't care whether it's inside a tag, an attribute, a class name, or a URL. It replaces itself with whatever value it produces.

A safer way to output inside attributes is using htmlspecialchars() to prevent XSS:


    <input value="<?= htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8') ?>">

Or in Laravel Blade, {{ }} handles this automatically:


    <input value="{{ $userInput }}">


Pattern 4 — PHP Inside Inline CSS

Yes, this works too. PHP can inject values directly into style attributes:


    <?php
    $primaryColor = "#6C63FF";
    $fontSize = 18;
    $bannerHeight = 300;
    ?>

    <div style="background-color: <?= $primaryColor ?>; font-size: <?= $fontSize ?>px;">
        Dynamic styles applied
    </div>

    <section style="height: <?= $bannerHeight ?>px; background-image: url('<?= $imageUrl ?>');">
        Hero section
    </section>

You can also inject into a <style> block:


    <style>
        :root {
            --primary-color: <?= $primaryColor ?>;
            --font-size-base: <?= $fontSize ?>px;
            --banner-height: <?= $bannerHeight ?>px;
        }

        .hero {
            background-color: var(--primary-color);
            height: var(--banner-height);
        }
    </style>

This is especially powerful with CSS custom properties — define your theme values in PHP (perhaps pulled from a database) and inject them as CSS variables. The entire frontend theme becomes dynamic.


Pattern 5 — PHP Inside JavaScript

This is the most misunderstood one. Let's be precise about what is and isn't possible.

What Actually Happens

PHP runs on the server. JavaScript runs in the browser. They never run at the same time. But PHP executes first — and since it can output anything, it can write JavaScript code that already has the values baked in.


    <?php
    $user = [
        'id' => 42,
        'name' => 'Gagan',
        'role' => 'admin',
    ];

    $config = [
        'apiUrl' => 'https://api.myapp.com',
        'debug' => false,
        'version' => '2.1.0',
    ];
    ?>

    <script>
        const user = <?= json_encode($user) ?>;
        const config = <?= json_encode($config) ?>;

        console.log(user.name);
        console.log(config.apiUrl);
    </script>

What the browser actually receives — no PHP anywhere:


    <script>
        const user = {
            "id": 42,
            "name": "Gagan",
            "role": "admin"
        };
        const config = {
            "apiUrl": "https:\/\/api.myapp.com",
            "debug": false,
            "version": "2.1.0"
        };

        console.log(user.name);
        console.log(config.apiUrl);
    </script>

PHP wrote the JavaScript. JavaScript runs with real data already in it.

json_encode() Is the Bridge

json_encode() converts any PHP array or object into a valid JSON string, which is also valid JavaScript. This is the standard, safe way to pass structured data from PHP to JavaScript:


    <?php
    $products = [
        ['id' => 1, 'name' => 'Laptop', 'price' => 999],
        ['id' => 2, 'name' => 'Mouse', 'price' => 29],
        ['id' => 3, 'name' => 'Keyboard', 'price' => 79],
    ];
    ?>

    <script>
        const products = <?= json_encode($products) ?>;

        products.forEach(product => {
            console.log(`${product.name} costs $${product.price}`);
        });
    </script>

Conditional JavaScript Based on PHP Logic


    <?php $isAdmin = true; ?>

    <script>
        const isAdmin = <?= $isAdmin ? 'true' : 'false' ?>;

        if (isAdmin) {
            document.querySelector('.admin-panel').style.display = 'block';
        }
    </script>

Dynamic Event Listeners and IDs


    <?php $buttonId = "submit-btn-" . $formId; ?>

    <button id="<?= $buttonId ?>">Submit</button>

    <script>
        document.getElementById('<?= $buttonId ?>').addEventListener('click', function() {
            console.log('Form <?= $formId ?> submitted');
        });
    </script>


What PHP Cannot Do With JavaScript

This is the hard limit people misunderstand.

PHP runs at request time → Page is sent → PHP is completely done
JavaScript runs after → PHP no longer exists

PHP cannot:

  • React to a button click the user makes
  • Read a value the user typed after the page loaded
  • Update itself when JavaScript changes something
  • Respond to browser events in real time

If you need JavaScript to send data back to PHP after the page has loaded, you use AJAX — JavaScript makes a new HTTP request to a PHP endpoint:


    fetch('/api/save-data.php', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                name: 'Gagan',
                value: 42
            })
        })
        .then(res => res.json())
        .then(data => console.log(data));

That triggers a completely fresh PHP execution on the server. It's not PHP and JavaScript talking to each other — it's JavaScript making a new request that PHP handles independently.


The Full Picture

Pattern

Works?

How

PHP inside HTML tags

Yes

PHP outputs value, HTML wraps it

PHP inside HTML attributes

Yes

PHP outputs string into attribute value

HTML inside PHP

Yes

Close PHP tag, write HTML, reopen

PHP inside <style> block

Yes

PHP outputs CSS values as strings

PHP inside inline style attribute

Yes

PHP outputs style values directly

PHP inside <script> block

Yes

PHP writes JS values before page loads

PHP reacting to JS events live

No

PHP is done before JS even starts

JS modifying PHP variables live

No

Use AJAX for a new server request



The Bottom Line

PHP can embed anywhere in an HTML document because it's a preprocessor — it runs first, replaces itself with output, and hands clean HTML to the browser. CSS attributes, style blocks, JavaScript variables, HTML attributes — PHP doesn't distinguish between them. It just outputs a string wherever you put it.

The only boundary is time. PHP lives at request time. JavaScript lives at runtime in the browser. They don't overlap — but with json_encode() and AJAX, they communicate cleanly across that boundary.

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 m...