Having used Fat-Free Framework 2.x for a little while now, I’m still generally quite happy with it… aside from one thing.
The built-in templating engine’s syntax doesn’t offer many advantages over raw PHP, it’s more verbose in certain common cases, and, as Fabien Potencier wrote when he took over maintainership for Twig, template languages have evolved a lot since PHP began its life as one in 1995. …so I decided to use Twig.
Why Twig?
It does help that I also work in Django sometimes but it’s not as big a factor as you might think. The main reason is that Twig is both the fastest, lightest templating engine I know (benchmark) and also one of the most featureful. Both good reasons to use FatFree and Twig together.
Here are a few of the reasons I switched:
- Fully extensible
- Auto-escaping for cleaner, safer code and less stressful coding.
- Let’s me choose my preferred mix of template inheritance and template includes.
- Supports macros for tidying up repeated code snippets like form fields.
- Has clever constructs like for/else to clean up common cases like “List results or display a ‘no results’ message”
- Syntax is easier for me to read and write in data-heavy templates with many nullable fields.
- Generally more amenable to the kinds of factoring-out of duplicated boilerplate that I want to do.
Setup Instructions
Unsurprisingly, I couldn’t get integration with F3::get()
as concisely as in F3 templates, but I did pretty well, all things considered.
The simplest way I’ve found to set this up is to add something along these lines to the top of your index.php:
require_once __DIR__ . '/lib/Twig/Autoloader.php';
Twig_Autoloader::register();
$twig_loader = new Twig_Loader_Filesystem(__DIR__ . '/templates');
$twig = new Twig_Environment($twig_loader, array(
'cache' => __DIR__ . '/twig_cache',
'auto_reload' => true,
));
$twig->addFilter('f3', new Twig_Filter_Function('F3::get'));
$twig->addGlobal('is_ajax', Web::isajax());
This will set up the following:
- Template Location
- Templates will be stored in a folder named
templates
so they don’t conflict with any templates you continue to use with the default F3 templates. (Assuming you’re still using the default ui
folder you probably kept from the sample code)
- Template Caching
- Twig templates will be cached in a folder named
twig_cache
to avoid conflicts and the cache will be automatically regenerated when the templates change
- F3::get()
- The
f3
filter gives you 'foo'|f3
as a slightly inelegant but not overly verbose equivalent to @foo
in templates. Things like 'result'|f3.title
will work as desired.
- Also, if you’re only using F3::get to expose things to the templates, you can use Twig globals, which let you use
{{ foo }}
instead of {{ @foo }}
. The syntax for doing so is as follows:
$twig->addGlobal('foo', $foo);
- is_ajax
- This global allows you to do the switching between full templating and minimal responses for AJAX requests entirely in the templates using this snippet of code (source):
{% extends is_ajax ? "base_ajax.html" : "base.html" %}
- Rendering
-
global $twig;
echo $twig->render('page.html', array('bar' => $bar));
Important: Compatibility Fix
The one caveat is that there’s a little incompatibility between the current versions of Twig and Axon. When using {% if my_hydrated_axon.my_null_valued_field %}
, you’ll get
Undefined method Axon->my_null_valued_field()
The problem is:
- Twig’s dot operator uses
isset()
to determine whether a property exists, then fails over to assuming you wanted a method call with no arguments.
Axon.__isset()
uses isset()
on its internal array of fields.
isset()
on an array returns false if the field exists but has a value of NULL
.
- Even with
strict_variables
set to false
, current Twig has a bug that makes it error out rather than producing an empty value when accessing axon properties.
You have two options. First, you can use my_hydrated_axon.cast.my_null_valued_field
everywhere (Axon.cast()
returns an ordinary associative array, which doesn’t have this problem) or you can make a small patch to FatFree’s lib/db.php
and change this…
function __isset($field) {
return isset($this->fields[$field]) || isset($this->adhoc[$field]);
}
…to this…
function __isset($field) {
return (is_array($this->fields) && array_key_exists($field, $this->fields)) ||
(is_array($this->adhoc) && array_key_exists($field, $this->adhoc));
}
The only caveat with this approach is that, in your PHP code, if (!$axon->property)
won’t work properly.