PHP has built-in templating, but mixing PHP and HTML gets messy quickly. Separate template engines like Twig or Smarty add dependencies and complexity. I wanted a small implementation with Jinja-like syntax that’s easy to understand and modify. So I wrote MintyPHP Template.
It allows you to write things like:
<h1>{{ title }}</h1>
{% if user.is_admin %}
<span class="badge">Admin</span>
{% endif %}
<ul>
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
</ul>
All output is HTML-escaped by default. Variables use {{ }}, control structures
use {% %}, and comments use {# #}.
How it works
The implementation uses a recursive descent parser. It tokenizes the template into literals, variables, control structures, and comments, then processes them in order.
Variables: The parser extracts the expression between {{ }}, evaluates it
against the data context, applies any filters with the pipe operator, and
escapes the result for HTML output.
Control Structures: It parses {% if %}, {% for %}, {% extends %},
{% block %}, and {% include %} tags, recursively processes their content,
and evaluates conditions or iterates over data.
Expressions: A full expression parser handles operators (arithmetic, comparison, logical), dot notation for nested data access, and parentheses for grouping. Operator precedence follows standard rules.
Variables and expressions
Variables support dot notation for nested data:
{{ user.profile.name }}
{{ price * quantity }}
{{ first_name + " " + last_name }}
The expression evaluator handles all standard operators: +, -, *, /,
%, ==, !=, <, >, <=, >=, and, or, not.
Filters
Filters transform values using pipe syntax:
{{ name|upper }}
{{ content|truncate(100) }}
{{ items|join(", ") }}
{{ price|sprintf("$%.2f") }}
The engine includes built-in filters: upper, lower, capitalize, title,
trim, truncate, replace, split, join, reverse, abs, round,
sprintf, filesizeformat, length, first, last, sum, default,
attr, and debug.
You can also pass custom filters as functions:
$filters = [
'dateFormat' => fn($date, $format) => date($format, strtotime($date))
];
$html = $template->render($content, $data, $filters);
The raw filter outputs unescaped HTML.
Control structures
If statements support elseif and else:
{% if score >= 90 %}
Grade: A
{% elseif score >= 80 %}
Grade: B
{% else %}
Grade: F
{% endif %}
For loops work with arrays and key-value pairs:
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
{% for key, value in items %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
Tests
Tests check value properties using the is keyword:
{% if value is defined %} Value exists {% endif %}
{% if count is even %} Even number {% endif %}
{% if total is divisibleby(3) %} Divisible by 3 {% endif %}
Built-in tests include: defined, undefined, null, even, odd,
divisibleby, number, string, and iterable. Use is not to negate.
Template inheritance
Templates can extend base templates using blocks:
base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
page.html:
{% extends "base.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}<h1>Hello World</h1>{% endblock %}
The {% extends %} directive must be first. Child blocks completely replace
parent blocks. This requires a template loader function to load files.
Template inclusion
Include other templates with {% include %}:
{% include "header.html" %}
<main>{{ content }}</main>
{% include "footer.html" %}
Included templates share the same data context.
Important notes
- HTML escaping is default: All output is escaped unless you use
|raw - No external dependencies: Beyond PHPUnit for testing
- Expressions are fully evaluated: Not just variable lookup
- Whitespace is preserved: Except lines with only
{% %}or{# #}tags - Filters chain: You can do
{{ value|lower|trim|truncate(50) }} - Template loader required: For
{% extends %}and{% include %}to work
The implementation is intentionally simple with no dependencies. I wrote it because I wanted something I could understand completely and modify easily without diving into complex template engine internals.
See: https://github.com/mintyphp/template
Enjoy!