Ember Template Engine

Ember is LavaLust’s built-in template engine. It compiles lightweight template syntax into cached PHP files, giving you clean, readable templates without sacrificing PHP’s full power. Compiled files are reused on subsequent requests so there is no repeated parsing overhead.

Ember is inspired by Blade and Twig syntax but is purpose-built for LavaLust and has no external dependencies.

Setup

1. Enable Ember in app/config/ember.php

<?php
$config['ember_helper_enabled'] = TRUE;
$config['templates_path']       = APPPATH . 'views/';
$config['cache_path']           = APPPATH . '../runtime/cache/ember/';
$config['auto_escape']          = TRUE;
$config['enable_php_blocks']    = FALSE;  // TRUE to allow @php ... @endphp
$config['escape_context']       = 'html'; // 'html', 'attr', 'js', or 'url'

2. Load the library in your controller

<?php
$this->load->library('ember');

Once loaded it is available as $this->ember.

Supported template file extensions

Ember resolves templates by trying the following extensions in order:

Extension

Example

(no extension)

views/home

.ember.php

views/home.ember.php

.php

views/home.php

.html

views/home.html

.tpl

views/home.tpl

Rendering Templates

Use render() to compile and execute a template, passing variables as an associative array. The method returns the rendered HTML as a string.

<?php
// Render and output immediately
echo $this->ember->render('home', [
    'title' => 'Welcome',
    'user'  => $user,
]);

// Or pass it to a LavaLust view
$data['content'] = $this->ember->render('partials/hero', ['title' => 'Hello']);
$this->load->view('layout', $data);

Template files live under the configured templates_path (default: app/views/).

app/views/home.ember.php
app/views/partials/hero.ember.php
app/views/layouts/main.ember.php

Output — Echoing Variables

Escaped output (auto-escapes HTML entities — use this by default)

<h1>{{ title }}</h1>
<p>Hello, {{ user.name }}</p>

Raw / unescaped output (use only for trusted HTML)

{!! html_content !!}

Applying the raw filter inline

{{ body|raw }}

Warning

Always use {{ }} for user-supplied data. Only use {!! !!} or the raw filter for HTML you have generated or sanitised yourself.

Filters

Filters transform a variable’s value before it is output. Chain multiple filters with |.

Built-in filters

Filter

Syntax

Description

upper

{{ name|upper }}

Convert to uppercase

lower

{{ name|lower }}

Convert to lowercase

raw

{{ html|raw }}

Output without escaping

escape / e

{{ value|escape }}

Explicitly escape the value

nl2br

{{ bio|nl2br }}

Convert newlines to <br> tags

striptags

{{ input|striptags }}

Strip HTML tags

trim

{{ value|trim }}

Strip leading and trailing whitespace

Chaining filters

{{ comment|striptags|trim|upper }}

Custom filters

Register your own filter with add_filter() and use it in templates:

<?php
// In your controller or bootstrap
$this->ember->add_filter('currency', function($value, $symbol = '$') {
    return $symbol . number_format((float) $value, 2);
});
<!-- Template -->
{{ price|currency }}
{{ price|currency('€') }}

Functions

Call registered functions directly inside {{ }}:

<?php
$this->ember->add_function('asset', function($path) {
    return base_url('assets/' . $path);
});

$this->ember->add_function('route', function($name) {
    return site_url($name);
});
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<a href="{{ route('users/index') }}">Users</a>

Global Variables

Global variables are available in every template without being passed explicitly to render():

<?php
$this->ember->add_global('app_name', config_item('app_name'));
$this->ember->add_global('current_user', $this->session->userdata('user'));
<title>{{ app_name }}</title>
<p>Logged in as: {{ current_user.name }}</p>

Control Structures

if / elseif / else / endif

@if(user.role == 'admin')
    <a href="/admin">Admin Panel</a>
@elseif(user.role == 'moderator')
    <a href="/moderate">Moderate</a>
@else
    <a href="/profile">My Profile</a>
@endif

foreach / endforeach

<ul>
@foreach(posts as post)
    <li>{{ post.title }}</li>
@endforeach
</ul>

for / endfor

@for($i = 1; $i <= 5; $i++)
    <span>{{ i }}</span>
@endfor

while / endwhile

@while(!queue.isEmpty())
    <p>{{ queue.next() }}</p>
@endwhile

Template Inheritance

Ember supports a full extends / section / yield layout system so you can define a base layout once and override named sections per page.

Base layout (layouts/main.ember.php)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>@yield('title') — My App</title>
    @yield('head')
</head>
<body>
    <nav><!-- navigation --></nav>

    <main>
        @yield('content')
    </main>

    <footer><!-- footer --></footer>
    @yield('scripts')
</body>
</html>

Child template (pages/dashboard.ember.php)

@extends('layouts/main')

@section('title')
Dashboard
@endsection

@section('head')
<link rel="stylesheet" href="{{ asset('css/dashboard.css') }}">
@endsection

@section('content')
<h1>Welcome, {{ user.name }}</h1>
<p>Here is your dashboard.</p>
@endsection

@section('scripts')
<script src="{{ asset('js/dashboard.js') }}"></script>
@endsection

Rendering the child template

<?php
echo $this->ember->render('pages/dashboard', [
    'user' => $user,
]);

Note

@extends() must reference the layout relative to templates_path. The layout file is rendered automatically — you do not call render() on it directly.

Including Partials

Use @include() to embed another template inside the current one. All variables from the current context are available in the included partial.

@include('partials/navbar')
@include('partials/alert')

Partial file (partials/navbar.ember.php):

<nav>
    <a href="/">Home</a>
    @if(current_user)
        <a href="/logout">Logout</a>
    @else
        <a href="/login">Login</a>
    @endif
</nav>

Auto-Escaping and Escape Contexts

When auto_escape = TRUE (the default), all {{ }} output is escaped according to the configured escape_context. You can also escape explicitly per expression by passing a context to the escape() method or using the context-specific filter.

Available escape contexts

Context

Config value

Description

html

'escape_context' => 'html'

htmlspecialchars() with ENT_QUOTES — safe for HTML content

attr

'escape_context' => 'attr'

htmlspecialchars() with ENT_QUOTES | ENT_HTML5 — safe for HTML attributes

js

'escape_context' => 'js'

json_encode() with hex escapes — safe for JavaScript strings

url

'escape_context' => 'url'

rawurlencode() — safe for URL parameters

Escaping in PHP code

<?php
// Default context (from config)
echo $this->ember->escape($user_input);

// Explicit context
echo $this->ember->escape($user_input, 'attr');
echo $this->ember->escape($user_input, 'js');
echo $this->ember->escape($user_input, 'url');

PHP Blocks

Raw PHP blocks inside templates are disabled by default for security. Enable them in config only when absolutely necessary:

<?php
$config['enable_php_blocks'] = TRUE;

When enabled:

@php
$total = array_sum(array_column($items, 'price'));
@endphp

<p>Total: {{ total }}</p>

When disabled, @php ... @endphp blocks are replaced with an HTML comment and their content is never executed.

Warning

Enabling PHP blocks in templates that accept user-supplied input is a significant security risk. Only enable this option in trusted, developer-controlled template files.

Template Caching

Ember compiles each template to a PHP file in the configured cache_path the first time it is requested. On subsequent requests the compiled file is served directly — no re-parsing — unless the source template is modified, in which case the cache is automatically invalidated and recompiled.

Clearing the cache manually

<?php
$this->ember->clear_cache();

This deletes all *.php files from the cache directory. The next request will recompile every template from source.

Tip

Add $this->ember->clear_cache() to your deployment script so stale compiled templates are never served after a template update.

Security

Ember includes several security measures:

Protection

Details

Auto-escaping

All {{ }} output is HTML-escaped by default. Disable only with {!! !!} or raw filter for intentional raw HTML.

Path traversal prevention

resolve_template_path() strips .., ./, and \ from template names and verifies the resolved path is inside templates_path using realpath().

PHP blocks disabled

@php ... @endphp blocks are stripped and replaced with a comment unless explicitly enabled in config.

Sandboxed rendering

Templates execute inside a closure with only extracted context variables, registered functions, and $this->escape() available.

Cache file permissions

Compiled cache files are written with 0644 permissions.

Method Reference

Method

Signature

Returns

render()

render($template, $context = [])

string

compile()

compile($template)

string (path to compiled file)

compile_string()

compile_string($source, $tpl_path = '')

string (compiled PHP source)

resolve_template_path()

resolve_template_path($template)

string (absolute file path)

escape()

escape($value, $context = null)

string

add_global()

add_global($name, $value)

void

add_function()

add_function($name, callable $fn)

void

add_filter()

add_filter($name, callable $fn)

void

apply_filter()

apply_filter($name, $value, ...$args)

mixed

clear_cache()

clear_cache()

void

Complete Example

Controller

<?php
// app/controllers/Blog.php
class Blog extends Controller
{
    public function __construct()
    {
        parent::__construct();
        $this->load->library('ember');
        $this->load->model('Post_model', 'post_model');

        // Global available in all templates
        $this->ember->add_global('site_name', 'My Blog');

        // Custom filter
        $this->ember->add_filter('excerpt', function($text, $length = 100) {
            return strlen($text) > $length
                ? substr($text, 0, $length) . '...'
                : $text;
        });

        // Custom function
        $this->ember->add_function('asset', fn($p) => base_url('assets/' . $p));
    }

    public function index()
    {
        $posts = $this->post_model->get_published(10);

        echo $this->ember->render('blog/index', [
            'posts'   => $posts,
            'page'    => 'Blog',
        ]);
    }
}

Layout (app/views/layouts/main.ember.php)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>@yield('title') — {{ site_name }}</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
    @include('partials/nav')
    <main class="container">
        @yield('content')
    </main>
</body>
</html>

Page template (app/views/blog/index.ember.php)

@extends('layouts/main')

@section('title')
Blog
@endsection

@section('content')
<h1>Latest Posts</h1>

@foreach(posts as post)
<article>
    <h2>{{ post.title }}</h2>
    <p>{{ post.body|excerpt(150) }}</p>
    <a href="/blog/{{ post.slug }}">Read more →</a>
</article>
@endforeach

@if(count(posts) === 0)
<p>No posts yet.</p>
@endif
@endsection

Tips and Best Practices

  • Always use {{ }} for user input — never {!! !!} unless you have sanitised the value yourself.

  • Set auto_escape = TRUE in config (it is the default) and only reach for |raw when you deliberately want unescaped HTML.

  • Use @extends and @yield to define a single layout file instead of repeating the <html> shell in every view.

  • Use @include for repeated UI fragments (navigation, alerts, footers) rather than copy-pasting markup across templates.

  • Register shared data (logged-in user, site name, CSRF token) as globals in a base controller constructor so it is available in every template.

  • Clear the cache as part of your deployment pipeline — $this->ember->clear_cache() — to ensure stale compiled files are never served after a template update.

  • Keep enable_php_blocks = FALSE in production. If you need PHP logic in a template, move it to the controller or a custom function registered with add_function().

  • Use the excerpt, striptags, and trim filters to sanitise and shorten text output rather than doing it in the controller.