younishd / endobox
minimal template engine.
Requires
- php: ^7.0
- erusev/parsedown: ^1.6
Requires (Dev)
- phpunit/phpunit: ^6.5
README
endobox
minimal template engine.
🌱 Native PHP syntax | 📝 Markdown on-board | 🚀 Minimal API |
---|---|---|
Write templates in vanilla PHP. No need to learn a new syntax. | A full-blown Markdown parser is built right in. Yes, it can be combined with PHP! | Do powerful things with just a handful of elementary methods. |
Documentation
Installation
Install endobox is via Composer:
composer require younishd/endobox
You will need PHP 7.0+.
Get started
The typical way to configure endobox to load templates for an application looks like this:
require_once '/path/to/vendor/autoload.php'; use endobox\Endobox; $endobox = Endobox::create('path/to/templates');
You can add additional template locations:
$endobox->addFolder('another/path/to/templates');
Render templates
Instantiate a Box
for your template:
$welcome = $endobox('welcome');
Render the template with some variables by calling render()
:
echo $welcome->render([ 'name' => "Alice" ]);
The template file itself could look like this:
welcome.php
<h1>Hello, <?= $name ?>!</h1>
File extensions
endobox decides how to render a template based on the file extension.
When you instantiate the template Box
however, the extension is omitted.
$members = $endobox('members'); // no file extension
PHP: .php
PHP templates are processed by evaluating the code between PHP tags (i.e., <? … ?>
) and returning the result.
members.php
<h1>Members</h1>
<ul>
<?php foreach ($users as $u): ?>
<li><?= $u->name ?></li>
<?php endforeach ?>
</ul>
ℹ️ Protip: The
<?=
is syntactic sugar for<?php echo
.
Markdown: .md
Markdown templates are processed by a Markdown parser (Parsedown) which produces the corresponding HTML code. This can be used for static content.
members.md
# Members - Alice - Bob - Carol
PHP+Markdown: .md.php
As the name suggests, this template type combines both PHP and Markdown: The template gets evaluated as PHP first, then parsed as Markdown. Pretty neat.
members.md.php
# Members
<?php foreach ($users as $u): ?>
- <?= $u->name ?>
<?php endforeach ?>
HTML: .html
HTML templates are always printed as is. No further processing takes place.
members.html
<h1>Members</h1> <ul> <li>Alice</li> <li>Bob</li> <li>Carol</li> </ul>
Data
Data is accessible inside a template as simple variables (e.g., $foo
) where the variable name corresponds to the assigned array key or property.
<h1>Hello, <?= $username ?>!</h1>
Assign data
There are several ways to assign data to a template box:
// via assign(…) $welcome->assign([ "username" => "eve" ]); // via object property $welcome->username = "eve"; // via render(…) $welcome->render([ "username" => "eve" ]); // implicitly $welcome([ "username" => "eve" ]);
Shared data
Usually, template boxes are isolated from each other. Data that's been assigned to one box, will not be visible from another.
$welcome->username = "eve"; // not accessible to 'profile' $profile->email = "eve@example.com"; // not accessible to 'welcome'
If they should share their data however, you can link them together:
$welcome->link($profile);
Now, these template boxes are linked and they share the same data.
welcome.php
<h1>Hello, <?= $username ?>!</h1>
<p>Your email address is: <code><?= $email ?></code></p>
profile.php
<h1>Profile</h1>
<ul>
<li>Username: <strong><?= $username ?></strong></li>
<li>Email: <strong><?= $email ?></strong></li>
</ul>
Notice how welcome.php
prints out $email
which was initially assigned to $profile
and profile.php
echoes $username
even though it was assigned to $welcome
.
ℹ️ Protip: You can create template boxes using an existing
Box
object (instead of using theBoxFactory
object) with$box->create('template')
which has the advantage of linking the two boxes together by default.
Default values
Sometimes it can be useful to supply a default value to be printed in case a variable has not been assigned. You can easily achieve that using PHP 7's null coalescing operator: ??
<title><?= $title ?? "Default" ?></title>
Escaping
Escaping is a form of data filtering which sanitizes unsafe, user supplied input prior to outputting it as HTML.
endobox provides two shortcuts to the htmlspecialchars()
function: $escape()
and its shorthand version $e()
<h1>Hello, <?= $escape($username) ?>!</h1>
<h1>Hello, <?= $e($username) ?>!</h1>
Escaping HTML attributes
⚠️ Warning: It's VERY important to always double quote HTML attributes that contain escaped variables, otherwise your template will still be open to injection attacks (e.g., XSS).
<!-- Good -->
<img src="portrait.jpg" alt="<?= $e($name) ?>">
<!-- BAD -->
<img src="portrait.jpg" alt='<?= $e($name) ?>'>
<!-- BAD -->
<img src="portrait.jpg" alt=<?= $e($name) ?>>
Chaining & Nesting
Since you're rarely dealing with just a single template you might be looking for a method that combines multiple templates in a meaningful way.
Chaining
By chaining we mean concatenating templates without rendering them.
Chaining two templates is as simple as:
$header($article);
Now, calling ->render()
on either $header
or $article
will render both templates and return the concatenated result.
ℹ️ Protip: The benefit of not having to render the templates to strings right away is flexibility: You can define the layout made out of your templates before knowing the concrete values of their variables.
The general syntax for chaining a bunch of templates is simply:
$first($second)($third)($fourth); // and so on
Neat.
The more explicit (and strictly equivalent) form would be using append()
or prepend()
as follows:
$first->append($second)->append($third)->append($fourth);
Or…
$fourth->prepend($third)->prepend($second)->prepend($first);
ℹ️ Protip: Note that the previously seen
Box::__invoke()
is simply an alias ofBox::append()
.
You have now seen how you can append (or prepend) Box
es together.
Notice however that the variables $first
, $second
, $third
, and $fourth
were objects of type Box
which means they needed to be brought to life at some point —
probably using the BoxFactory
object created in the very beginning (which we kept calling $endobox
in this document).
In other words the complete code would probably look something like this:
$first = $endobox('first'); $second = $endobox('second'); $third = $endobox('third'); echo $first($second)($third);
It turns out there is a way to avoid this kind of boilerplate code: You can directly pass the name of the template (a string
) to the append()
method instead of the Box
object!
So, instead you could just write:
echo $endobox('first')('second')('third');
It looks trivial, but there is a lot going on here. The more verbose form would look as follows:
echo $endobox->create('first')->append('second')->append('third');
This is – in turn – equivalent to the following lines:
echo ($_ = $endobox->create('first')) ->append($endobox->create('second')->link($_)) ->append($endobox->create('third')->link($_));
Notice that unlike before these (implicitly created) boxes are now all linked together automatically, meaning they share the same data.
The rule of thumb is: Boxes created from other boxes are linked by default.
Nesting
A fairly different approach (probably the template designer rather than the developer way) would be to define some sort of layout template instead:
layout.php
<html>
<head></head>
<body>
<header><?= $header ?></header>
<article><?= $article ?></article>
<footer><?= $footer ?></footer>
Then somewhere in controller land:
$layout = $endobox('layout'); $header = $endobox('header'); // header.html $article = $endobox('article'); // article.php $footer = $endobox('footer'); // footer.html echo $layout->render([ 'header' => $header, 'article' => $article->assign([ 'title' => "How to make Lasagna" ]), 'footer' => $footer ]);
This should be fine, but we can get rid of some boilerplate code here: $header
and $footer
really don't need to be variables.
That's where nesting comes into play!
Use the $box()
function to instantiate a template Box
from inside another template:
layout.php
<html>
<head></head>
<body>
<header><?= $box('header') ?></header>
<article><?= $article ?></article>
<footer><?= $box('footer') ?></footer>
Then simply…
echo $endobox('layout')->render([ 'article' => $endobox('article')->assign([ 'title' => "How to make Lasagna" ]) ]);
This is already much cleaner, but it gets even better: By using $box()
to nest a template Box
inside another these two boxes will be linked by default!
That allows us to condense this even further. Check it out:
layout.php
<html>
<head></head>
<body>
<header><?= $box('header') ?></header>
<article><?= $box('article') ?></article>
<footer><?= $box('footer') ?></footer>
All three templates are now nested using $box()
and therefore linked to their parent (i.e., $layout
).
This reduces our controller code to one line:
echo $endobox('layout')->render([ 'title' => "How to make Lasagna" ]);
Notice how we are assigning a title to the layout
template even though the actual $title
variable occurs in the nested article
template.
ℹ️ Protip: The
$box()
function is also available as a method ofBox
objects (i.e., outside templates): You can instantiate new boxes with$box->create('template')
where$box
is someBox
object that has already been created.
Functions
Functions are a cool and handy way of adding reusable functionality to your templates (e.g., filters, URL builders…).
Registering functions
You can register your own custom function (i.e., closure) by simply assigning it to a template Box
just like data!
Here is a simple function $day()
which returns the day of the week:
$calendar->day = function () { return date('l'); };
Inside your template file you can then use it in the same fashion as any variable:
<p>Today is <?= $day ?>.</p>
This would look like this (at least sometimes):
<p>Today is Tuesday.</p>
You can go even further and actually invoke the variable just like any function and actually pass some arguments along the way.
Below is a simple closure $a()
that wraps and escapes some text in a hyperlink tag:
$profile->a = function ($text, $href) { return sprintf('<a href="%s">%s</a>', htmlspecialchars($href), htmlspecialchars($text)); };
Calling this function inside your template would look like this:
<p>Follow me on <?= $a("GitHub", "https://github.com/younishd") ?></p>
This would produce something like this:
<p>Follow me on <a href="https://github.com/younishd">GitHub</a></p>
Default functions
There are a couple of default helper functions that you can use out of the box (some of which you may have already seen):
Function | Description | Example |
---|---|---|
$box() or $b() |
Instantiate a Box from within another template. (See Nesting.) |
<article><?= $box('article') ?></article> |
$markdown() or $m() |
Render some text as Markdown. Useful when the text is user input/stored in a database. | <?= $markdown('This is some _crazy comment_!') ?> |
$escape() or $e() |
Sanitize unsafe user input using htmlspecialchars() . (See Escaping.) |
<img src="portrait.jpg" alt="<?= $e($name) ?>"> |
Cloning
You can easily clone a template Box
using the built-in clone
keyword.
$sheep = $endobox('sheep'); $cloned = clone $sheep;
The cloned box will have the same content and data as the original one. However, chained or linked boxes are discarded.
License
endobox is open-sourced software licensed under the MIT license.