Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 32 of 54

Mixing PHP and HTML

Build HTML pages with PHP using the short echo tag, the alternative syntax for loops and conditions, and htmlspecialchars to print data safely.

13 minPHP on the webUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonWrite PHP templates that loop over data and print it safely into HTML.

PHP is a template language

A .php file can contain HTML with small islands of PHP. Everything outside the PHP tags is sent to the browser unchanged; everything inside runs on the server. You can switch in and out as often as you like, which makes PHP a natural language for building pages.

This lesson builds a small product listing. The data would normally come from a database; here it is an array at the top of the file, so you can concentrate on the template.

The short echo tag

Printing values is the most common thing a template does, so PHP has a short form for it. <?= $value ?> means exactly the same as <?php echo $value; ?>, and it is always enabled:

php
<?php $name = "Asha"; ?>
<p>Hello, <?= $name ?>!</p>
<p>Today is <?= date('l', mktime(0, 0, 0, 6, 3, 2024)) ?>.</p>
http
GET /hello.php
result
<p>Hello, Asha!</p>
<p>Today is Monday.</p>

Note that the first line, the PHP block that sets $name, produced no output, and PHP also swallowed the single line break directly after its closing ?>. That is why the output starts with the paragraph.

Loops and conditions in templates

Curly braces get lost between HTML tags. The alternative syntax replaces the opening brace with a colon and the closing brace with endif;, endforeach;, endfor; or endwhile;, so each block is labelled:

php
<?php
$products = [
    ['name' => 'Mechanical Keyboard', 'price' => 89.5, 'stock' => 60],
    ['name' => 'Puzzle 1000 Pieces', 'price' => 19.99, 'stock' => 0],
    ['name' => 'Ceramic Mug Set', 'price' => 18.75, 'stock' => 4],
];
?>
<h1>Products</h1>
<?php if (count($products) === 0): ?>
  <p>No products yet.</p>
<?php else: ?>
  <ul>
  <?php foreach ($products as $product): ?>
    <li>
      <?= $product['name'] ?>: ₹<?= number_format($product['price'], 2) ?>
      <?php if ($product['stock'] === 0): ?>
        <strong>Sold out</strong>
      <?php elseif ($product['stock'] < 10): ?>
        <em>Only <?= $product['stock'] ?> left</em>
      <?php endif; ?>
    </li>
  <?php endforeach; ?>
  </ul>
<?php endif; ?>
http
GET /products.php
result
<h1>Products</h1>
  <ul>
      <li>
      Mechanical Keyboard: ₹89.50          </li>
      <li>
      Puzzle 1000 Pieces: ₹19.99              <strong>Sold out</strong>
          </li>
      <li>
      Ceramic Mug Set: ₹18.75              <em>Only 4 left</em>
          </li>
    </ul>

The indentation of the output looks untidy, because the whitespace around the PHP tags is kept. Browsers ignore it, so it does not matter for the page; it is only a little surprising in View Source. What matters is that the structure of the template is easy to read.

Escaping output with htmlspecialchars

Now suppose a product name comes from a form or a database, where anyone could have typed anything. If the text contains HTML, printing it with <?= inserts that HTML into your page:

php
<?php
$review = 'Great mug! <script>alert("hacked")</script>';
?>
<p>Unsafe: <?= $review ?></p>
<p>Safe: <?= htmlspecialchars($review) ?></p>
http
GET /review.php
result
<p>Unsafe: Great mug! <script>alert("hacked")</script></p>
<p>Safe: Great mug! &lt;script&gt;alert(&quot;hacked&quot;)&lt;/script&gt;</p>

In the unsafe line, the browser would run that script. A real attacker would not show an alert; they would steal the session cookies of everyone who views the page. This attack is called cross-site scripting (XSS) and it is one of the most common web vulnerabilities. In the safe line, htmlspecialchars() converted <, >, &, " and ' into HTML entities, so the browser shows the text exactly as it was typed and runs nothing.

The rule is simple: escape every value when you print it into HTML, whether it came from a user, a database or an API. Values you wrote yourself in the code, such as a fixed heading, do not need it, but escaping them does no harm. Escaping also keeps ordinary text correct: a product called "Salt & Pepper" should be printed as Salt &amp; Pepper.

Typing htmlspecialchars() everywhere is tedious, so most projects define a tiny helper:

php
<?php
function e(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
php
<?php
require __DIR__ . '/helpers.php';
$product = ['name' => 'Salt & Pepper "Classic" Set', 'url' => '/p/salt-pepper'];
?>
<a href="<?= e($product['url']) ?>" title="<?= e($product['name']) ?>"><?= e($product['name']) ?></a>
http
GET /card.php
result
<a href="/p/salt-pepper" title="Salt &amp; Pepper &quot;Classic&quot; Set">Salt &amp; Pepper &quot;Classic&quot; Set</a>

ENT_QUOTES (the default in PHP 8) escapes both kinds of quote, which matters inside attributes. Template engines such as Laravel's Blade and Twig escape automatically, which is one of the main reasons people use them. The web security lesson covers the cases that escaping alone does not, such as URLs in href.

Keep logic out of the template

Templates become hard to maintain when they also fetch data, validate forms and calculate prices. A common structure is:

  1. PHP at the top of the file (or in a separate file) prepares all the data.
  2. The HTML below only loops, checks simple conditions and prints escaped values.

Frameworks take this further with separate controller and view files, but the habit starts here.

Common mistakes

  • Printing data without htmlspecialchars(), which opens the door to XSS.
  • Mismatched alternative syntax, such as an endif; missing inside a loop. PHP reports "syntax error, unexpected end of file".
  • Mixing braces and the alternative syntax in the same block.
  • Doing database work in the middle of the HTML, which makes the template hard to read and test.

Exercise

Create menu.php with an array of three dishes, each with a name and a price, where one name contains an ampersand ("Rajma & Rice"). Print them as an HTML table with a header row, escape every name, and format prices with two decimals.

Show one possible answer
php
<?php
$dishes = [
    ['name' => 'Masala Dosa', 'price' => 120],
    ['name' => 'Rajma & Rice', 'price' => 150],
    ['name' => 'Filter Coffee', 'price' => 40],
];
?>
<table>
  <tr><th>Dish</th><th>Price</th></tr>
<?php foreach ($dishes as $dish): ?>
  <tr><td><?= htmlspecialchars($dish['name']) ?></td><td><?= number_format($dish['price'], 2) ?></td></tr>
<?php endforeach; ?>
</table>
http
GET /menu.php
result
<table>
  <tr><th>Dish</th><th>Price</th></tr>
  <tr><td>Masala Dosa</td><td>120.00</td></tr>
  <tr><td>Rajma &amp; Rice</td><td>150.00</td></tr>
  <tr><td>Filter Coffee</td><td>40.00</td></tr>
</table>

Key points

  • Outside <?php ... ?> tags, everything in a .php file is sent as it is. Switch in and out of PHP as needed.
  • <?= $value ?> is short for <?php echo $value; ?> and is always available.
  • Use the alternative syntax (if: ... endif; foreach: ... endforeach;) in templates for readability.
  • Escape every value you print into HTML with htmlspecialchars(), or a visitor's text can become code on your page.

Check yourself

1. What is <?= $title ?> short for?
2. What does htmlspecialchars('<b>Hi</b>') return?
3. Why escape a product name before printing it into HTML?
4. Which closing keyword matches <?php foreach ($items as $item): ?>?