Extending Latte

Latte is very flexible and can be extended in many ways: you can add custom filters, functions, tags, loaders, etc. We will show you how to do it.

This chapter describes the different ways to extend Latte. If you want to reuse your changes in different projects or if you want to share them with others, you should then create so-called extension.

How Many Roads Lead to Rome?

Since some of the ways of extending Latte can be blended, let's first try to explain the differences between them. As an example, let's try to implement a Lorem ipsum generator, which is passed the number of words to generate.

The main Latte language construct is the tag. We can implement a generator by extending Latte with a new tag:

{lipsum 40}

The tag will work great. However, the generator in the form of a tag may not be flexible enough because it cannot be used in an expression. By the way, in practice, you rarely need to generate tags; and that's good news, because tags are a more complicated way to extend.

Okay, let's try creating a filter instead of a tag:

{=40|lipsum}

Again, a valid option. But the filter should transform the passed value into something else. Here we use the value 40, which indicates the number of words generated, as the filter argument, not as the value we want to transform.

So let's try using function:

{lipsum(40)}

That's it! For this particular example, creating a function is the ideal extension point to use. You can call it anywhere where an expression is accepted, for example:

{var $text = lipsum(40)}

Filters

Create a filter by registering its name and any PHP callable, such as a function:

$latte = new Latte\Engine;
$latte->addFilter('shortify', fn(string $s) => mb_substr($s, 0, 10)); // shortens the text to 10 characters

In this case it would be better for the filter to get an additional parameter:

$latte->addFilter('shortify', fn(string $s, int $len = 10) => mb_substr($s, 0, $len));

We use it in a template like this:

<p>{$text|shortify}</p>
<p>{$text|shortify:100}</p>

As you can see, the function receives the left side of the filter before the pipe | as the first argument and the arguments passed to the filter after : as the next arguments.

Of course, the function representing the filter can accept any number of parameters, and variadic parameters are also supported.

If the filter returns a string in HTML, you can mark it so that Latte does not automatically (and therefore double) escaping it. This avoids the need to specify |noescape in the template. The easiest way is to wrap the string in a Latte\Runtime\Html object, the other way is Contextual Filters.

$latte->addFilter('money', fn(float $amount) => new Latte\Runtime\Html("<i>$amount EUR</i>"));

In this case, the filter must ensure correct escaping of the data.

Filters Using the Class

The second way to define a filter is to use class. We create a method with the TemplateFilter attribute:

class TemplateParameters
{
	public function __construct(
		// parameters
	) {}

	#[Latte\Attributes\TemplateFilter]
	public function shortify(string $s, int $len = 10): string
	{
		return mb_substr($s, 0, $len);
	}
}

$params = new TemplateParameters(/* ... */);
$latte->render('template.latte', $params);

Filter Loader

Instead of registering individual filters, you can create a so-called loader, which is a function that is called with the filter name as an argument and returns its PHP callable, or null.

$latte->addFilterLoader([new Filters, 'load']);


class Filters
{
	public function load(string $filter): ?callable
	{
		if (in_array($filter, get_class_methods($this))) {
			return [$this, $filter];
		}
		return null;
	}

	public function shortify($s, $len = 10)
	{
		return mb_substr($s, 0, $len);
	}

	// ...
}

Contextual Filters

A contextual filter is one that accepts an object Latte\Runtime\FilterInfo in the first parameter, followed by other parameters as in the case of classical filters. It is registered in the same way, Latte itself recognizes that the filter is contextual:

use Latte\Runtime\FilterInfo;

$latte->addFilter('foo', function (FilterInfo $info, string $str): string {
	// ...
});

Context filters can detect and change the content-type they receive in the $info->contentType variable. If the filter is called classically over a variable (e.g. {$var|foo}), the $info->contentType will contain null.

The filter should first check if the content-type of the input string is supported. It can also change it. Example of a filter that accepts text (or null) and returns HTML:

use Latte\Runtime\FilterInfo;

$latte->addFilter('money', function (FilterInfo $info, float $amount): string {
	// first we check if the input's content-type is text
	if (!in_array($info->contentType, [null, ContentType::Text])) {
		throw new Exception("Filter |money used in incompatible content type $info->contentType.");
	}

	// change content-type to HTML
	$info->contentType = ContentType::Html;
	return "<i>$amount EUR</i>";
});

In this case, the filter must ensure correct escaping of the data.

All filters that are used over blocks (e.g. as {block|foo}...{/block}) must be contextual.

Functions

By default, all native PHP functions can be used in Latte, unless the sandbox disables it. But you can also define your own functions. They can override the native functions.

Create a function by registering its name and any PHP callable:

$latte = new Latte\Engine;
$latte->addFunction('random', function (...$args) {
	return $args[array_rand($args)];
});

The usage is then the same as when calling the PHP function:

{random(apple, orange, lemon)} // prints for example: apple

Functions Using the Class

The second way to define a function is to use class. We create a method with the TemplateFunction attribute:

class TemplateParameters
{
	public function __construct(
		// parameters
	) {}

	#[Latte\Attributes\TemplateFunction]
	public function random(...$args)
	{
		return $args[array_rand($args)];
	}
}

$params = new TemplateParameters(/* ... */);
$latte->render('template.latte', $params);

Loaders

Loaders are responsible for loading templates from a source, such as a file system. They are set using the setLoader() method:

$latte->setLoader(new MyLoader);

The built-in loaders are:

FileLoader

Default loader. Loads templates from the filesystem.

Access to files can be restricted by setting the base directory:

$latte->setLoader(new Latte\Loaders\FileLoader($templateDir));
$latte->render('test.latte');

StringLoader

Loads templates from strings. This loader is very useful for unit testing. It can also be used for small projects where it may make sense to store all templates in a single PHP file.

$latte->setLoader(new Latte\Loaders\StringLoader([
	'main.file' => '{include other.file}',
	'other.file' => '{if true} {$var} {/if}',
]));

$latte->render('main.file');

Simplified use:

$template = '{if true} {$var} {/if}';
$latte->setLoader(new Latte\Loaders\StringLoader);
$latte->render($template);

Creating a Custom Loader

Loader is a class that implements the Latte\Loader interface.

Tags

One of the most interesting features of the templating engine is the ability to define new language constructs using tags. It's also a more complex functionality and you need to understand how Latte internally works.

In most cases, however, the tag is not needed:

  • if it should generate some output, use function instead
  • if it was to modify some input and return it, use filter instead
  • if it was to edit a area of text, wrap it with a {block} tag and use a filter
  • if it was not supposed to output anything but just call a function, call it with {do}

If you still want to create a tag, great! All the essentials can be found in Creating an Extension.

Compiler Passes

Compiler passes are functions that modify ASTs or collect information in them. In Latte, for example, a sandbox is implemented in this way: it traverses all the nodes of an AST, finds function and method calls, and replaces them with controlled calls.

As with tags, this is more complex functionality and you need to understand how Latte works under the hood. All the essentials can be found in the Creating an Extension chapter.

version: 3.0 2.x