PHP is A-OK for Templating
February 4, 2020PHP templating often gets a bad rap for facilitating subpar code — but that doesn't have to be the case. Let’s look at how PHP projects can enforce a basic Model, View, Controller (MVC) structure without depending on a purpose-built templating engine.
But first, a very brief PHP history lesson
The history of PHP as a tool for HTML templating is full of twists and turns.
One of the first programming languages used for HTML templating was C, but it was quickly found to be tedious to use and generally ill-suited for the task.
Rasmus Lerdorf created PHP with this in mind. He wasn’t opposed to using C to handle back-end business logic but wanted a better way to generate dynamic HTML for the front end. PHP was originally designed as a templating language, but adopted more features over time and eventually became a full programming language in its own right.
PHP’s unique ability to switch between programming mode and HTML mode was found to be quite convenient, but also made it tempting for programmers to write unmaintainable code — code that mixed business logic and templating logic. A PHP file could begin with some HTML templating and then suddenly dive into an advanced SQL query without warning. This structure is confusing to read and makes reusing HTML templates difficult.
As time passed, the web development community found more and more value enforcing a strict MVC structure for PHP projects. Templating engines were created as a way to effectively separate views from their controllers.
To accomplish this task, templating engines typically have the following characteristics:
- The engine is purposely underpowered for business logic. If a developer wants to perform a database query, for example, they need to make that query in the controller and then pass the result to the template. Making a query in the middle of the template’s HTML is not an option.
- The engine takes care of common security risks behind the scenes. Even if a developer fails to validate user input and passes it directly into the template, the template will often escape any dangerous HTML automatically.
Templating engines are now a mainstay feature in many web technology stacks. They make for more maintainable, more secure codebases, so this comes as no surprise.
It is possible, however, to handle HTML templating with plain PHP as well. You may want to do this for a number of reasons. Maybe you are working on a legacy project and don’t want to bring in any additional dependencies, or maybe you are working on a very small project and prefer to keep things as lightweight as possible. Or maybe the decision isn’t up to you at all.
Use cases for PHP templating
One place that I often implement plain PHP templating is WordPress, which doesn’t enforce a rigid MVC structure by default. I feel that bringing in a full templating engine is a bit heavy-handed, but I still want to separate my business logic from my templates and want my views to be reusable.
Whatever your reason, using plain PHP to define your HTML templates is sometimes the preferred choice. This post explores how this can be done in a reasonably professional way. The approach represents a practical middle ground between the spaghetti-coded style that PHP templating has become notorious for and the no-logic-allowed approach available with formal templating engines.
Let’s dive into an example of how a basic templating system can be put into practice. Again, we’re using WordPress as an example, but this could be swapped to a plain PHP environment or many other environments. And you don’t need to be familiar with WordPress to follow along.
The goal is to break our views into components and create a distinct separation between the business logic and HTML templates. Specifically, we are going to create a view that displays a grid of cards. Each card is going to display the title, excerpt, and author of a recent post.
Step 1: Fetching data to render
The first step to take is fetching the data that we want to display in our view. This could involve executing a SQL query or using the ORM or helper functions of your framework/CMS to access your database indirectly. It could also involve making an HTTP request to an external API or collecting user input from a form or query string.
In this example, we’re going to use the WordPress get_posts
helper function to fetch some posts to display on our homepage.
<?php // index.php
$wp_posts = get_posts([
'numberposts' => 3
]);
We now have access to the data we want to display in the cards grid, but we need to do some additional work before we can pass it to our view.
Step 2: Preparing data for templating
The get_posts
function returns an array of WP_Post
objects. Each object contains the post title, excerpt, and author information that we need, but we don’t want to couple our view to the WP_Post
object type because we might want to show other kinds of data on our cards somewhere else in the project.
Instead, it makes sense to proactively convert each post object to a neutral data type, like an associative array:
<?php // index.php
$wp_posts = get_posts([
'numberposts' => 3
]);
$cards = array_map(function ($wp_post) {
return [
'heading' => $wp_post->post_title,
'body' => $wp_post->post_excerpt,
'footing' => get_author_name($wp_post->post_author)
];
}, $wp_posts);
In this case, each WP_Post
object is converted into an associative array by using the array_map
function. Notice that the keys for each value are not title
, excerpt
, and author
but are given more general names instead: heading
, body
, and footing
. We do this because the cards grid component is meant to support any kind of data, not just posts. It could just as easily be used to show a grid of testimonials that have a quote and a customer’s name, for example.
With the data properly prepared, it can now be passed into our render_view
function:
<?php // index.php
// Data fetching and formatting same as before
render_view('cards_grid', [
'cards' => $cards
]);
Of course, the render_view
function does not exist yet. Let’s define it.
Step 3: Creating a render function
// Defined in functions.php, or somewhere else that will make it globally available.
// If you are worried about possible collisions within the global namespace,
// you can define this function as a static method of a namespaced class
function render_view($view, $data)
{
extract($data);
require('views/' . $view . '.php');
}
This function accepts the name of the rendered view and an associative array representing any data to be displayed. The extract function takes each item in the associative array and creates a variable for it. In this example, we now have a variable named $cards
that contains the items we prepared in index.php
.
Since the view is executed in its own function, it gets its own scope. This is nice because it allows us to use simple variable names without fear of collisions.
The second line of our function prints the view matching the name passed. In this case, it looks for the view in views/cards_grid.php
. Let’s go ahead and create that file.
Step 4: Creating templates
<?php /* views/cards_grid.php */ ?>
<section>
<ul>
<?php foreach ($cards as $card) : ?>
<li>
<?php render_view('card', $card) ?>
</li>
<?php endforeach; ?>
</ul>
</section>
This template uses the $cards
variable that was just extracted and renders it as an unordered list. For each card in the array, the template renders a subview: the singular card view.
Having a template for a single card is useful because it gives us the flexibility to render a single card directly or use it in another view somewhere else in the project.
Let’s define the basic card view:
<?php /* views/card.php */ ?>
<div class="card">
<?php if (!empty($heading)) : ?>
<h4><?= htmlspecialchars($heading) ?></h4>
<?php endif;
if (!empty($body)) : ?>
<p><?= htmlspecialchars($body) ?></p>
<?php endif;
if (!empty($footing)) : ?>
<span><?= htmlspecialchars($footing) ?></span>
<?php endif; ?>
</div>
Since the $card
that was passed into the render function contained keys for a heading
, body
, and footing
, variables of those same names are now available in the template.
In this example, we can be reasonably sure that our data is free of XSS hazards, but it’s possible that this view could be used with user input at some later time, so passing each value through htmlspecialchars
is prudent. If a script tag exists in our data it will be safely escaped.
It’s also often helpful to check that each variable contains a non-empty value before rendering it. This allows for variables to be omitted without leaving empty HTML tags in our markup.
PHP templating engines are great but it is sometimes appropriate to use PHP for what it was originally designed for: generating dynamic HTML.
Templating in PHP doesn’t have to result in unmaintainable spaghetti code. With a little bit of foresight we can achieve a basic MVC system that keeps views and controllers separate from each other, and this can be done with surprisingly little code.