Template Engines in PHP — Bringing Structure to Your Frontend

If you've worked with frontend frameworks or other backend languages, you've probably encountered template engines — tools that let you write cleaner, more expressive view files without mixing raw language syntax into your markup. PHP developers have access to the same concept, and the ecosystem around it is mature and widely adopted.


First — Does PHP Even Need a Template Engine?

This is a fair question, because PHP itself is technically already a template language. You can embed PHP directly inside HTML:

<!DOCTYPE html>
<html>
<body>
    <h1>Hello, <?php echo htmlspecialchars($name); ?></h1>

    <?php if ($isLoggedIn): ?>
        <p>Welcome back!</p>
    <?php endif; ?>

    <?php foreach ($posts as $post): ?>
        <div><?php echo htmlspecialchars($post['title']); ?></div>
    <?php endforeach; ?>
</body>
</html>

This works. But look at it — it's noisy. The <?php ?> tags interrupt the HTML flow constantly, htmlspecialchars() has to be called manually every time you output a variable, and as files grow larger this becomes genuinely hard to read and maintain.

Template engines solve exactly this problem.


Yes — PHP Has Template Engines, and They Are Excellent

PHP's template engine ecosystem is one of the strongest in any backend language. Two engines dominate the space completely.


Twig — The Standalone Standard

Twig is developed and maintained by Symfony. It's the most widely used PHP template engine outside of Laravel and has a clean, expressive syntax inspired by Django's template language.

Installation

composer require twig/twig

Basic Setup

<?php

require_once 'vendor/autoload.php';

$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader, [
    'cache' => 'cache',
    'auto_reload' => true,
]);

echo $twig->render('home.html.twig', [
    'name' => 'Gagan',
    'posts' => $posts,
    'isLoggedIn' => true,
]);

Template File — templates/home.html.twig

<!DOCTYPE html>
<html>
<body>
    <h1>Hello, {{ name }}</h1>

    {% if isLoggedIn %}
        <p>Welcome back!</p>
    {% endif %}

    {% for post in posts %}
        <div>{{ post.title }}</div>
    {% endfor %}
</body>
</html>

Compare this to the raw PHP version above. Same output, dramatically cleaner syntax.

Template Inheritance in Twig

This is where Twig really shines — defining a base layout once and extending it across all pages:

{# templates/layout.html.twig #}

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My App{% endblock %}</title>
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; {{ "now"|date("Y") }}</p>
    </footer>
</body>
</html>
{# templates/home.html.twig #}

{% extends 'layout.html.twig' %}

{% block title %}Home — My App{% endblock %}

{% block content %}
    <h1>Hello, {{ name }}</h1>
    <p>Latest posts below.</p>
{% endblock %}

One layout file. Every page just fills in the blocks it needs. No copy-pasting HTML structure across files.

Key Twig Features

  • {{ variable }} — outputs a value, auto-escaped by default
  • {% if %}, {% for %}, {% block %} — logic and structure tags
  • {{ value|filter }} — built-in filters like upper, date, length, default
  • Auto-escaping — XSS protection out of the box, no manual htmlspecialchars() needed
  • Template inheritance and includes
  • Custom filters and functions you can register from PHP

Blade — Laravel's Built-In Engine

If you're using Laravel, you already have Blade — it's baked in with zero configuration. Blade has its own syntax and a few powerful features that go slightly beyond what Twig offers.

Basic Blade Template

<!DOCTYPE html>
<html>
<body>
    <h1>Hello, {{ $name }}</h1>

    @if($isLoggedIn)
        <p>Welcome back!</p>
    @endif

    @foreach($posts as $post)
        <div>{{ $post->title }}</div>
    @endforeach
</body>
</html>

Blade Layout System

{{-- resources/views/layouts/app.blade.php --}}

<!DOCTYPE html>
<html>
<head>
    <title>@yield('title', 'My App')</title>
</head>
<body>
    @include('partials.navbar')

    <main>
        @yield('content')
    </main>
</body>
</html>
{{-- resources/views/home.blade.php --}}

@extends('layouts.app')

@section('title', 'Home')

@section('content')
    <h1>Hello, {{ $name }}</h1>
    @include('partials.post-list', ['posts' => $posts])
@endsection

Blade-Specific Features Worth Knowing

{{-- Raw output, no escaping --}}
{!! $rawHtml !!}

{{-- Components --}}
<x-alert type="error" :message="$errorMessage" />

{{-- Stacks — push scripts/styles from child templates --}}
@push('scripts')
    <script src="/js/chart.js"></script>
@endpush

{{-- CSRF token for forms --}}
<form method="POST" action="/submit">
    @csrf
    <input type="text" name="title" />
    <button type="submit">Submit</button>
</form>

{{-- Authorization directives --}}
@can('edit-post', $post)
    <a href="/posts/{{ $post->id }}/edit">Edit</a>
@endcan

Blade also compiles down to plain PHP and gets cached, so there's no runtime performance penalty for using it.


Twig vs Blade — Quick Comparison

Feature Twig Blade
Framework Standalone / Symfony Laravel only
Syntax style {{ }} and {% %} {{ }} and @directives
Auto-escaping Yes, by default Yes, by default
Template inheritance Yes Yes
Components Via macros Native x-component system
Compilation/caching Yes Yes
Custom extensions Yes Yes
Use without framework Yes Possible but complex

Other Template Engines Worth Knowing

Beyond Twig and Blade, a few others exist in the PHP ecosystem:

  • Smarty — one of the oldest PHP template engines, still maintained, used in legacy codebases
  • Plates — a native PHP template engine with no new syntax, just plain PHP with structure added on top
  • Latte — developed by Nette framework, known for strong security features

For any new project today, Twig or Blade covers everything you'll need.


Auto-Escaping — The Most Underrated Benefit

Both Twig and Blade automatically escape output by default. This means:

{# User submitted: <script>alert('xss')</script> #}
{{ userInput }}

Twig renders this as safe text — the script tag is neutralized automatically. In raw PHP you have to remember htmlspecialchars() on every single echo. Template engines make XSS protection the default behavior, not something you have to manually remember.


The Bottom Line

PHP absolutely supports template engines, and the two main options — Twig and Blade — are production-grade, actively maintained, and used in some of the largest PHP applications in the world. They bring clean syntax, layout inheritance, component systems, and automatic security escaping to your frontend views.

If you're using Laravel, Blade is already there. If you're building something custom or framework-agnostic, Twig is the go-to choice. Either way, there's no reason to write raw PHP mixed into HTML once you've worked with a proper template engine.

PHP & Laravel — Zero to Hero Episode 06: Loops — Teaching PHP to Repeat Tasks Automatically

What Are We Doing in This Post?

In Episode 05, PHP learned how to make decisions. Now we teach it to repeat tasks.

Imagine you have 100 products to display on a page. You are not going to write echo 100 times. You write the logic once, tell PHP how many times to repeat it, and PHP handles the rest. That is what loops do.

Loops are one of the most powerful and most used concepts in all of programming. Master this episode and a huge chunk of real-world PHP will start making sense immediately.


The while Loop — Repeat As Long As Something is True

Real world analogy: You are filling a bucket with water. You keep pouring as long as the bucket is not full. The moment it is full, you stop. That is a while loop.


    <?php

    $count = 1;

    while ($count <= 5) {
        echo "Count is: $count";
        echo "<br>";
        $count++;
    }

Output:

Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5

PHP checks the condition before every iteration. Is $count less than or equal to 5? If yes, run the block. After each run, $count increases by 1. When $count reaches 6, the condition becomes false and the loop stops.

The $count++ line is critical. Without it, $count never changes, the condition is always true, and the loop runs forever. This is called an infinite loop and it will crash your server. Always make sure your loop has a way to eventually become false.


The do while Loop — Run First, Check Later

The do while loop is similar to while, but with one key difference: it runs the code block first and checks the condition after. This guarantees the block runs at least once, even if the condition is false from the start.


    <?php

    $number = 10;

    do {
        echo "Number is: $number";
        echo "<br>";
        $number++;
    } while ($number < 5);

Output:

Number is: 10

Even though $number is already 10 and the condition $number < 5 is immediately false, the block still runs once because the check happens after the first execution.

Real world use: showing a user a form at least once, and then re-showing it only if validation fails.


The for Loop — When You Know Exactly How Many Times

The for loop is the most precise loop. You use it when you know in advance exactly how many times you want to repeat something.


    <?php

    for ($i = 1; $i <= 5; $i++) {
        echo "Iteration: $i";
        echo "<br>";
    }

Output:

Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5

The for loop has three parts inside the parentheses, separated by semicolons.

The first part is the initializer: $i = 1. This runs once at the very beginning and sets up the counter variable.

The second part is the condition: $i <= 5. PHP checks this before every iteration. If true, the block runs. If false, the loop stops.

The third part is the increment: $i++. This runs after every iteration, updating the counter.

All three parts in one line makes for loops very readable. When you see a for loop, you immediately know the start, the end, and the step.


Counting Backwards and Custom Steps

The for loop is flexible. You can count in any direction and any step size.


    <?php

    for ($i = 10; $i >= 1; $i--) {
        echo "$i ";
    }

Output:

10 9 8 7 6 5 4 3 2 1


    <?php

    for ($i = 0; $i <= 20; $i += 5) {
        echo "$i ";
    }

Output:

0 5 10 15 20

The second example increments by 5 each time using $i += 5. You can step by any amount you need.


The foreach Loop — Made for Arrays

The foreach loop is designed specifically to loop through arrays. Arrays are lists of values — we will cover them in full detail in Episode 07. But for now, think of an array as a list.


    <?php

    $fruits = ["Apple", "Banana", "Mango", "Orange", "Grapes"];

    foreach ($fruits as $fruit) {
        echo $fruit;
        echo "<br>";
    }

Output:

Apple
Banana
Mango
Orange
Grapes

foreach goes through the array one item at a time. On each iteration, the current item is stored in $fruit and you can use it inside the block. PHP automatically knows when the list ends and stops.

You do not manage a counter yourself. You do not worry about going out of bounds. foreach handles all of that. This is the loop you will use most often in real Laravel applications.


foreach With Key and Value

Arrays in PHP have both keys and values. foreach can give you both at the same time.


    <?php

    $person = ["name" => "Gagan", "age" => 22, "city" => "Delhi", "job" => "Developer"];

    foreach ($person as $key => $value) {
        echo "$key: $value";
        echo "<br>";
    }

Output:

name: Gagan
age: 22
city: Delhi
job: Developer

The => symbol maps a key to a value. This is called an associative array and we will go deep into arrays in Episode 07. For now just notice how foreach with $key => $value gives you both pieces of information on every iteration.


break and continue — Controlling Loop Flow

Sometimes you need to exit a loop early or skip a specific iteration. PHP gives you two keywords for this.

break — exit the loop immediately:


    <?php

    for ($i = 1; $i <= 10; $i++) {
        if ($i == 6) {
            break;
        }
        echo "$i ";
    }

Output: 1 2 3 4 5

When $i reaches 6, break exits the loop completely. Numbers 6 through 10 are never printed.

Real world use: searching through a list for a specific item. Once you find it, you break out of the loop — no point checking the rest.

continue — skip this iteration and move to the next:


    <?php

    for ($i = 1; $i <= 10; $i++) {
        if ($i % 2 == 0) {
            continue;
        }
        echo "$i ";
    }

Output: 1 3 5 7 9

When $i is even (divisible by 2 with no remainder), continue skips the echo and jumps straight to the next iteration. Only odd numbers get printed.

Real world use: processing a list but skipping items that do not meet certain criteria — for example, skipping inactive users when sending a newsletter.


Nested Loops — Loops Inside Loops

You can put a loop inside another loop. The inner loop completes fully for every single iteration of the outer loop.


    <?php

    for ($row = 1; $row <= 3; $row++) {
        for ($col = 1; $col <= 3; $col++) {
            echo "[$row,$col] ";
        }
        echo "<br>";
    }

Output:

[1,1] [1,2] [1,3]
[2,1] [2,2] [2,3]
[3,1] [3,2] [3,3]

For each value of $row, the inner loop runs completely through all three values of $col. Real world use: generating a calendar grid, building a multiplication table, or rendering rows and columns of data in a table.


A Real World Example — Product Listing Page

Let us build something practical. A simple product listing page that a real e-commerce site might generate.

Create a new file called products.php in your phplearning folder:


    <!DOCTYPE html>
    <html>

    <head>
        <title>Product Listing</title>
    </head>

    <body>

        <h2>Our Products</h2>

        <?php
        $products = [
            ["name" => "Wireless Mouse",      "price" => 599,  "stock" => 15],
            ["name" => "Mechanical Keyboard", "price" => 2499, "stock" => 0],
            ["name" => "USB Hub",             "price" => 899,  "stock" => 8],
            ["name" => "Webcam HD",           "price" => 1799, "stock" => 0],
            ["name" => "Monitor Stand",       "price" => 1199, "stock" => 22],
        ];

        foreach ($products as $product) {
            echo "<div style='border: 1px solid #ccc; padding: 10px; margin: 10px 0;'>";
            echo "<h3>" . htmlspecialchars($product["name"]) . "</h3>";
            echo "<p>Price: Rs. " . $product["price"] . "</p>";

            if ($product["stock"] > 0) {
                echo "<p style='color: green;'>In Stock (" . $product["stock"] . " units)</p>";
                echo "<button>Add to Cart</button>";
            } else {
                echo "<p style='color: red;'>Out of Stock</p>";
            }

            echo "</div>";
        }
        ?>

    </body>

    </html>

Visit http://localhost:8080/phplearning/products.php

You will see five product cards. Two of them show Out of Stock in red with no Add to Cart button. Three show In Stock in green with the button. This is exactly how real product listing pages work — one loop, one set of logic, handling every product dynamically.

Notice how loops and conditionals work together here. The foreach handles repetition. The if/else inside handles the per-product logic. These two concepts combined are the backbone of almost every dynamic web page you will ever build.


Choosing the Right Loop

Here is a simple guide for deciding which loop to use.

Use while when you do not know in advance how many times you need to loop — you just know the stopping condition.

Use do while when you need the block to run at least once before checking the condition.

Use for when you know the exact number of iterations upfront.

Use foreach when you are working with an array or a list of items. This will be your most used loop in Laravel.


What Did We Learn in This Post?

while loops repeat as long as a condition is true. Always ensure the condition can eventually become false.

do while loops run the block first and check the condition after — guaranteed at least one execution.

for loops are precise — initializer, condition, and increment all defined in one line. Best when you know the exact count.

foreach loops are built for arrays — they handle the iteration automatically without manual counter management.

break exits a loop immediately. continue skips the current iteration and moves to the next.

Nested loops let you work with grid-like data — a complete inner loop runs for every single outer iteration.


What is Coming in Episode 07?

We used arrays briefly in this episode. In Episode 07 we go deep into them.

Arrays are how PHP stores and manages lists and collections of data. Indexed arrays, associative arrays, multidimensional arrays, and all the built-in array functions PHP provides. This is one of the most important episodes in the entire Core PHP section.

See you in the next one.


Next Episode: Arrays — Storing and Managing Collections of Data in PHP

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

Running PHP in the Terminal — No Browser Required

Most developers discover PHP through XAMPP, a local server, or a tutorial that immediately sets up Apache and a browser preview. So it's natural to assume PHP only lives inside a web server. But that's just one way to use it.

PHP has a fully functional Command Line Interface (CLI). You can write a .php file, run it from your terminal, and see output instantly — no browser, no server, no HTTP involved at all.


Do You Need to Install PHP Separately?

If you already have XAMPP installed, the answer is no.

XAMPP bundles PHP as part of its package. The PHP binary is already sitting on your machine — it's just not registered in your system's PATH by default, which is why your terminal doesn't recognize the php command out of the box.

You have two options:

  • Add XAMPP's PHP to your system PATH — recommended, one-time setup
  • Install PHP separately — only needed if you don't have XAMPP or want an independent installation

Option 1 — Use PHP From XAMPP (Windows)

Step 1 — Find Your XAMPP PHP Path

XAMPP installs PHP at:

C:\xampp\php

Confirm it by checking if php.exe exists there:

C:\xampp\php\php.exe

Step 2 — Add It to System PATH

  1. Press Win + S, search "Environment Variables"
  2. Click "Edit the system environment variables"
  3. Click "Environment Variables" button
  4. Under System variables, find and select Path, click Edit
  5. Click New and add:
C:\xampp\php
  1. Click OK on all dialogs

Step 3 — Restart Your Terminal

Close and reopen VS Code terminal or Command Prompt, then verify:

php --version

Expected output:

PHP 8.2.12 (cli) (built: Oct 24 2023)

That (cli) confirms you're running the CLI SAPI — not the web module. You're good to go.


Option 2 — Install PHP Separately (Without XAMPP)

If you want a clean, standalone PHP installation independent of XAMPP:

Windows

  1. Go to https://windows.php.net/download
  2. Download the latest VS16 x64 Non Thread Safe ZIP
  3. Extract it to a folder, for example:
C:\php
  1. Add C:\php to your system PATH using the same steps above
  2. Verify:
php --version

macOS

PHP comes pre-installed on older macOS versions, but for a current version use Homebrew:

brew install php

Then verify:

php --version

Linux (Ubuntu / Debian)

sudo apt update
sudo apt install php-cli

Verify:

php --version

Note — php-cli is a minimal package. It installs only what's needed to run PHP in the terminal, without Apache or any web server component.


Running Your First PHP Script in the Terminal

Create a file anywhere on your machine — call it index.php:


    <?php

    $name = "Gagan";
    $age = 25;

    echo "Name: " . $name . PHP_EOL;
    echo "Age: " . $age . PHP_EOL;

Open your terminal in that folder and run:
php index.php

Output:

Name: Gagan
Age: 25

No browser. No server. Just PHP and your terminal.


The Interactive Shell

PHP also ships with an interactive shell — write and execute PHP expressions in real time without creating a file:

php -a
Interactive shell

php > $x = 10;
php > $y = 20;
php > echo $x + $y;
30
php > echo strtoupper("hello world");
HELLO WORLD
php >

Type exit to quit. Useful for quickly testing a function, checking a value, or experimenting with syntax.


Run a One-Liner Without Any File

For something quick, you don't even need a file:

php -r "echo date('Y-m-d') . PHP_EOL;"
2026-06-05

One Important Difference — HTML Tags Don't Work Here

Since there's no browser to render markup, HTML tags will print as plain text:


    <?php
    echo "<h1>Hello</h1>";

Terminal output:

<h1>Hello</h1>

For CLI scripts, use PHP_EOL for line breaks instead of <br>, and avoid HTML markup entirely. The terminal only cares about plain text.


What You Can Build With PHP CLI

Once you're comfortable running PHP in the terminal, a completely different category of work opens up:

  • Automation scripts — file processing, batch renaming, data cleanup
  • Cron jobs — scheduled tasks that run in the background on a server
  • Data migration — moving records between databases
  • Custom CLI tools — interactive prompts, argument parsing, system utilities
  • Running tests — PHPUnit, Pest, and all major PHP testing frameworks run entirely in the terminal

And if you use Laravel, you've already been using PHP CLI every time you typed php artisan — that entire command system is built on it.


The Bottom Line

PHP is not limited to the browser. The CLI has been part of PHP since version 4.3 and it works exactly the way you'd expect — write a file, run it, see output. If you have XAMPP, you already have everything you need, just add the path and you're set.

The browser-only workflow is a habit formed by how PHP is introduced to most developers. It was never a technical limitation.

else if vs elseif in PHP — What's the Difference and Which Should You Use?

If you've been writing PHP for a while, you've probably used both else if (two words) and elseif (one word) without thinking much about it. They look almost identical, they behave almost identically, and PHP accepts both without complaint. So what's actually going on under the hood?

Let's break it down.


The Basics First

Here's the classic conditional structure most developers learn on day one:


    $score = 75;

    if ($score >= 90) {
        echo "Grade: A";
    } elseif ($score >= 75) {
        echo "Grade: B";
    } elseif ($score >= 60) {
        echo "Grade: C";
    } else {
        echo "Grade: F";
    }

Clean, readable, and works perfectly. Now swap every elseif with else if — it still works. So why does PHP have two ways to write the same thing?


They Are NOT Always the Same

Here's the critical part most tutorials skip.

With Curly Braces {} — Both Work Identically


    if ($x > 10) {
        echo "big";
    } else if ($x > 5) {
        echo "medium";
    } else {
        echo "small";
    }

    if ($x > 10) {
        echo "big";
    } elseif ($x > 5) {
        echo "medium";
    } else {
        echo "small";
    }

These two blocks compile to the same bytecode. Zero difference.

With Colon Syntax : — Only elseif is Valid

PHP supports an alternative syntax for control structures, commonly used inside HTML templates:


    <?php if ($isLoggedIn): ?>
        <p>Welcome back!</p>
    <?php elseif ($isGuest): ?>
        <p>Hello, guest!</p>
    <?php else: ?>
        <p>Please log in.</p>
    <?php endif; ?>

Now try using else if here:


    <?php if ($isLoggedIn): ?>
        <p>Welcome back!</p>
    <?php else if ($isGuest): ?> <!-- Parse error! -->
        <p>Hello, guest!</p>
    <?php endif; ?>

PHP throws a parse error. This is because else if in the colon syntax creates a nested if inside else, breaking the endif pairing. The parser no longer knows which if the endif closes.

This is the one real, concrete difference between the two.


What's Happening Internally

When you write else if (two words) with curly braces, PHP actually parses it as a nested structure:


    // What you write
    } else if ($condition) {

    // What PHP actually sees
    } else {
        if ($condition) {

        }
    }

It's else followed by a new if block nested inside it. PHP is smart enough to flatten this in the compiled output when you're using curly braces, so performance is identical. But with colon syntax, that nesting breaks the template structure — which is why else if is forbidden there.

elseif (one word), on the other hand, is a dedicated keyword. PHP treats it as a single, flat continuation of the conditional chain — no hidden nesting involved.


Performance — Is There Any Difference?

Short answer: No.

With curly braces, both forms produce identical opcodes. PHP's compiler optimizes else if into the same flat structure as elseif. You will never notice a performance difference in any real application.


So Which One Should You Use?

Follow this simple rule:

Situation

Use

Standard curly brace blocks

Either works — pick one and be consistent

Alternative colon/template syntax

Always use elseif

Team or framework codebase

Follow the existing style guide

PSR-12 and Community Convention

The PHP-FIG's PSR-12 coding standard — the most widely adopted PHP style guide — explicitly recommends elseif over else if:

"The keyword elseif SHOULD be used instead of else if so that all control keywords look like single words."

Laravel, Symfony, and most major PHP frameworks follow PSR-12. If you're writing professional PHP, elseif is the right default.


Quick Reference


    // Recommended — works everywhere
    if ($a) {
        // ...
    } elseif ($b) {
        // ...
    } else {
        // ...
    }

    // Also valid — but only with curly braces
    if ($a) {
        // ...
    } else if ($b) {
        // ...
    } else {
        // ...
    }

    // Template syntax — elseif ONLY, else if will throw a parse error
    <?php if ($a): ?>
        ...
    <?php elseif ($b): ?>
        ...
    <?php else: ?>
        ...
    <?php endif; ?>


The Bottom Line

elseif and else if are functionally identical in curly brace contexts, but elseif is the only valid option in PHP's alternative colon syntax. Beyond correctness, elseif is what PSR-12 recommends, what major frameworks enforce, and what makes your code look intentional and professional.

Pick elseif. Use it everywhere. Move on to harder problems.

Why PHP Doesn't Need a Closing ?> Tag — And Why That's Actually a Good Thing

If you've written PHP inside a .php file and noticed that omitting the closing ?> tag doesn't break anything, you're not imagining it. The file runs perfectly, output is correct, and PHP doesn't complain. VS Code might not even flag it as an issue.

So what's going on?


First, Understand What ?> Actually Does

The closing tag ?> is an instruction to the PHP parser — it says:

"Stop processing PHP here. Everything after this point is raw HTML/text — send it directly to the output buffer."

So in a mixed HTML + PHP file, it makes perfect sense:


    <?php
    $name = "Gagan";
    ?>
    <h1>Hello, <?php echo $name; ?></h1>
    <p>Welcome to my site.</p>

Here ?> is necessary because you're switching between PHP mode and HTML mode multiple times. The parser needs to know when to stop treating content as PHP code.


So Why Does a Pure PHP File Work Without It?

When your file contains only PHP — no HTML after the last line — the parser reaches the end of the file and simply stops. There is no HTML waiting to be sent, nothing to switch to. The closing tag's job was already done by the file ending.

PHP's parser behavior is:

"If I reach EOF while still in PHP mode, that's fine. I'll just stop here."


    <?php

    class UserService
    {
        public function getUser(int $id): array
        {
            return ['id' => $id, 'name' => 'Gagan'];
        }
    }

No ?> at the bottom. PHP reads the opening <?php, processes everything, hits EOF, and exits cleanly. Full success.


The Real Reason You Should OMIT ?>

This is where it gets interesting — and practical.

The Whitespace / Newline Problem

When you add ?> at the end of a file, PHP does something you might not expect. Any whitespace, newline, or blank line that exists after the closing tag gets sent to the output buffer as raw content.

   
    <?php

    function add(int $a, int $b): int
    {
        return $a + $b;
    }

    ?>

See that blank line after ?>? PHP sends that as output. A literal \n character is now part of your response — before any of your actual intended output.

Now imagine this file is included in a larger application:


    <?php
    // some-functions.php is included here
    require_once 'some-functions.php';

    header('Content-Type: application/json');
    echo json_encode(['status' => 'ok']);

You'll get this error:

Warning: Cannot modify header information - headers already sent by
(output started at some-functions.php:9)

A single invisible newline broke your headers. This kind of bug is notoriously difficult to track down, especially in large codebases where files are required/included across many layers.

The fix? Never add ?> in the first place.


What PSR-12 Says

PHP-FIG's PSR-12 standard is explicit about this:

"A file SHOULD either declare symbols (classes, functions, constants, etc.) or cause side-effects (e.g. generate output, change .ini settings, etc.) but SHOULD NOT do both. The closing ?> tag MUST be omitted from files containing only PHP."

Laravel, Symfony, WordPress (modern code), and virtually every professional PHP codebase follow this rule. If you open any controller, model, or service class in Laravel, you will never see a closing ?>.


When IS ?> Required?

Only in one scenario — files that intentionally mix PHP and HTML, like template files:


    <!DOCTYPE html>
    <html>

    <head>
        <title><?php echo $pageTitle; ?></title>
    </head>

    <body>
        <?php if ($isLoggedIn): ?>
            <p>Welcome, <?php echo $userName; ?></p>
        <?php else: ?>
            <p>Please log in.</p>
        <?php endif; ?>
    </body>

    </html>

Here, ?> is doing real work — it's toggling the parser between PHP mode and HTML output mode. You need it here. But notice — even this file doesn't need a closing ?> at the very end, because there's no PHP block left to close after the final HTML line.


Summary Table

File Type

Closing ?>

Pure PHP class / service / controller

Omit — always

Pure PHP config / constants file

Omit — always

PHP + HTML mixed template

Use it between blocks, omit at end

PHP file included via require / include

Omit — headers risk



The Bottom Line

PHP was designed to be embedded inside HTML, which is why ?> exists at all. But when a file is pure PHP, the closing tag is not just unnecessary — it's a silent bug waiting to happen. An accidental newline after it can corrupt headers, break JSON responses, and ruin session handling.

VS Code removing it automatically (or not flagging its absence) isn't a quirk — it's the editor agreeing with PSR-12. Omit the closing tag in every pure PHP file, every time, without exception.

Interesting Facts About PHP You Must Know

PHP powers a massive chunk of the internet and has been doing so for over three decades. Most developers use it daily without knowing its ba...