Templates

A template is a PHP file that makes HTML. The framework has no special template language. Template files have the extension .phtml. Some old templates have the extension .php.

This page tells you how to:

  • give data to a template
  • render a template from a plugin
  • find which file the framework uses when two templates have the same name
  • change a template of a plugin or of the system without a change to its package

Related pages:

The Template class

The Template class is the base of all template objects. It does three things:

  1. It keeps the variables that you assign.
  2. It finds the template file in its lookup paths.
  3. It includes the file and returns the output as a string.

These classes extend Template or implement the same interface (core\ITemplate):

Class Package Where it is used
Festi\Theme\Plugin\DisplayPlugin Theme package Each plugin that renders HTML.
Festi\Theme\DefaultTheme Theme package The theme layout (main.phtml, head.phtml).
AbstractField Core Each DGS field.
StoreView Core The DGS list and forms.

DisplayPlugin and DefaultTheme are not in the Core package. They are available only if the project has the Theme package (festi-team/festi-framework-theme) in its composer.json:

"require": {
    "festi-team/festi-framework-core": "dev-develop",
    "festi-team/festi-framework-theme": "dev-develop"
}

Assign data

Use assign() to give a variable to the template. The method returns the object, thus you can chain the calls.

$template = new Template('/path/to/templates/');

$template->assign('title', 'My Page Title');

$template->assign([
    'content' => 'This is the content of the page.',
    'user'    => 'John Doe',
]);

A property assignment does the same as assign():

$template->title = 'My Page Title';

Render a template

Use fetch() to render a template. The method returns the output. It does not print it.

$content = $template->fetch('example.phtml');
echo $content;

In example.phtml each assigned variable is a PHP variable:

<h1><?php echo $title; ?></h1>
<p><?php echo $content; ?></p>

fetch() throws a SystemException when it does not find the file.

Local variables

The second argument of fetch() is an array of local variables. A local variable exists only for that one fetch() call. If a local variable and an assigned variable have the same name, the template gets the local variable.

$localVars = [
    'description' => 'This description is local.',
];

$content = $template->fetch('example.phtml', $localVars);

Read an assigned variable

Use retrieve() to read a variable that you assigned. The method returns null when the variable does not exist.

$template->assign('user', 'John Doe');

$user = $template->retrieve('user'); // John Doe

Check that a template exists

if ($template->isTemplateExists('example.phtml')) {
    echo $template->fetch('example.phtml');
}

isTemplateExists() looks only in the lookup paths of the object.

Lookup paths

A Template object can have one lookup path or a list of lookup paths. fetch() uses the first file that it finds.

$template = new Template([
    '/path/to/custom/templates/',
    '/path/to/default/templates/',
]);

The order of the search is:

  1. The file name as it is, if a file with this path exists.
  2. Each lookup path, in the order of the list.

For the Template class, the third argument of fetch() is a directory. If you give it, fetch() uses only directory + file name.

Templates in a plugin

A plugin that extends Festi\Theme\Plugin\DisplayPlugin is a template object. This class comes from the Theme package (see above). Put the templates of the plugin in its templates directory:

plugins/
`-- My/
    |-- MyPlugin.php
    |-- templates/
    |   |-- example.phtml
    |   `-- parts/
    |       `-- row.phtml
    `-- static/

Render a plugin template

<?php

use Festi\Theme\Plugin\DisplayPlugin;

class MyPlugin extends DisplayPlugin
{
    public function onDisplayMain(Response &$response): bool
    {
        // One variable
        $this->assign('userToken', '12345678');

        // A property assignment does the same as assign()
        $this->userName = 'test user';

        // Many variables
        $this->assign([
            'idUser' => 5,
        ]);

        $response->content = $this->fetch('example.phtml');

        return true;
    }
}

templates/example.phtml:

<b>User Name:</b> <?php echo htmlspecialchars($userName); ?><br>
<b>User Token:</b> <?php echo htmlspecialchars($userToken); ?><br>
<b>User ID:</b> <?php echo (int) $idUser; ?><br>

The template only makes the content of the page. The theme puts this content into the page layout. See Frontend overview.

Note: A template prints its variables without a change. Use htmlspecialchars() for each value that comes from a user or from the database.

What $this is in a template

In a template, $this is the object that called fetch().

The template is rendered by $this is
a plugin ($this->fetch() in a plugin method) the plugin instance
the theme (main.phtml, head.phtml, menu.phtml) the theme instance (ITheme)
a DGS field the field instance
a HUI component a copy of the plugin or of the theme that made the component

In a plugin template you can call each public and protected method of the plugin:

<a href="<?php echo $this->getUrl('/users/'); ?>"><?php echo __('Users'); ?></a>

<?php echo $this->ui->button()->setCaption(__('Save'))->fetch(); ?>

Scope and lifetime of variables

Type of variable How you set it How long it exists Which templates get it
Assigned $this->assign('name', $value) or $this->name = $value Until the request ends All templates that this plugin renders after the assignment
Local Second argument of fetch() One fetch() call Only that template

Important rules:

  • A plugin has one instance for each request. Thus an assigned variable stays in the plugin after fetch().
  • Each plugin has its own variables. A plugin does not get the variables of a different plugin.
  • The theme does not get the variables of a plugin. See Give data to the theme.
  • A template that you render from a template does not get the PHP variables of the first template. It gets the assigned variables of the plugin and the local variables that you give it.

Use local variables for data that only one template uses. This prevents a conflict between two methods that assign the same name.

Names that you must not use for variables

fetch() makes PHP variables from the assigned variables and the local variables.

The Template class of the framework renders the templates of a DGS (store views, fields, filters) and every template that you render with your own new Template(...). Its fetch() includes the file in a method that has no variable of its own that a template variable can replace:

  • A variable named templatePath, localVars, vars, file, path or result is an ordinary variable. It does not change the file that fetch() includes.
  • A template does not get variables that it was not given. $templatePath, $localVars, $vars and $file do not exist in a template unless you assign them. Use __FILE__ and __DIR__ to get the path of the template, and get_defined_vars() to get its variables.
  • A variable named this is ignored. $this is always the template object.
  • Names that start with __festiTemplate are the names of fetch(). Do not use them: the template does not get a variable named __festiTemplatePath or __festiTemplateVars.

DisplayPlugin of the theme package renders the templates of a plugin with its own fetchFrom(). Until that package has the same change, do not assign templatePath or localVars in a plugin: the assigned value replaces an internal variable, and fetch() then fails or includes the wrong file. Also do not use file, path, vars and result there.

The framework also sets these names. Do not use them for other data:

Name Who sets it Value
content DisplayPlugin::fetchMain() and fetch() with a wrapper template The HTML of the page content
info DisplayPlugin::fetchMain() Page properties for the theme (URLs, CSS files, JS files, SEO data)
seo DisplayPlugin::fetchMain() SEO data of the page

Render a template in a template

Call $this->fetch() in the template. Give the data as local variables.

templates/list.phtml:

<ul class="b-users">
    <?php foreach ($users as $user) : ?>
        <?php echo $this->fetch('parts/row.phtml', ['user' => $user]); ?>
    <?php endforeach; ?>
</ul>

templates/parts/row.phtml:

<li class="e-user"><?php echo htmlspecialchars($user['name']); ?></li>

The second template gets:

  • all assigned variables of the plugin
  • the local variables from the call ($user)

The second template does not get $users, if $users was a local variable of the first template.

Render a template of a different plugin

Call fetch() on the instance of the other plugin. The template then uses the variables and the templates directory of the other plugin.

$html = $this->plugin->users->fetch('profile_card.phtml', ['idUser' => $idUser]);

Prefer a public method of the other plugin that returns the HTML. Then the other plugin controls its template and its data.

Wrapper template

For a plugin, the third argument of fetch() is the name of a wrapper template. It is not a directory.

$response->content = $this->fetch('report.phtml', null, 'layout.phtml');

The steps are:

  1. The plugin renders report.phtml.
  2. The plugin assigns the result to the variable content.
  3. The plugin renders layout.phtml, which prints $content.

templates/layout.phtml:

<div class="b-report">
    <h2><?php echo __('Report'); ?></h2>
    <?php echo $content; ?>
</div>

Render a template from a specified directory

Use fetchFrom() when the template is not in the lookup paths. The third argument is the directory.

$path = __DIR__.'/templates/mail/';

$html = $this->fetchFrom('welcome.phtml', ['user' => $user], $path);

fetchFrom() does no lookup. It uses only directory + file name.

Which file the plugin uses

When a plugin calls fetch('name.phtml'), the framework looks for the file in this order. It uses the first file that exists.

flowchart TD A["fetch('name.phtml')"] --> T{"File in the theme directory<br>templates/plugins/My/?"} T -- yes --> O["Use themes/default/templates/plugins/My/name.phtml"] T -- no --> B{"File in the plugin<br>templates directory?"} B -- yes --> P["Use plugins/My/templates/name.phtml"] B -- no --> C{"Is the name a path<br>to a file that exists?"} C -- yes --> Q["Use that path"] C -- no --> D{"File in the project<br>templates directory?"} D -- yes --> R["Use templates/name.phtml"] D -- no --> E["Use the theme templates directory.<br>Error if the file is not there."]
Step Directory Core option Default
1 Theme templates for this plugin Core::OPTION_THEME_TEMPLATE_PATH + plugins/<Name>/ <project root>/themes/<theme name>/templates/plugins/<Name>/
2 Plugin templates plugin_path + templates/ plugins/<Name>/templates/
3 The name as a path - -
4 Project templates Core::OPTION_TEMPLATE_PATH <project root>/templates/
5 Theme templates Core::OPTION_THEME_TEMPLATE_PATH <project root>/themes/<theme name>/templates/

Important results of this order:

  • A theme replaces one template of one plugin with a file in its templates/plugins/<Name>/ directory (step 1). <Name> is the name of the plugin. See Templates that a plugin has.
  • Without such a file, the template of the plugin has priority. A file with the same name in the project templates directory or in the root of the theme templates directory does not replace it.
  • If the plugin does not have the file, the project templates directory has priority over the theme.
  • A plugin template does not use the templates of the Theme package (ENGINE_TEMPLATE_PATH). Only HUI components and the DGS use them.

Note: The DGS and the HUI components use a different lookup order. See DGS templates and the Theme package.

Two plugins with the same template name

Each plugin looks in the theme directory with its own name and then in its own templates directory. Thus two plugins can have a template with the same name. There is no conflict.

Change a template without a change to its package

Do not edit a template in a plugin that comes from a package or from a submodule. The next update removes your change. Use one of these procedures.

Templates that a plugin does not have (system templates)

Some plugins render templates that are not in their own templates directory. The Jimbo system plugin does this for the sign-in pages:

Template Page
signin.php Sign-in form
signup.phtml Registration form
forgot_password.phtml Password recovery form

The theme supplies these files (step 5 of the lookup). To replace one of them, put a file with the same name in the project templates directory (step 4):

<project root>/
|-- templates/
|   `-- signin.php        <-- your file, the framework uses it
`-- themes/
    `-- default/
        `-- templates/
            `-- signin.php   <-- the file of the theme, not used

You do not change the theme.

Templates that a plugin has

A file with the same name in the project templates directory or in the root of the theme templates directory does not replace a template that the plugin has. Use one of these methods:

  1. Theme directory for the plugin. Put a file with the same name in templates/plugins/<Name>/ of the theme, where <Name> is the name of the plugin. The plugin uses this file and takes its other templates from its own directory. This needs a version of the Theme package that has this lookup step.

    <project root>/
    |-- plugins/
    |   `-- Orders/
    |       `-- templates/
    |           |-- list.phtml       <-- the file of the plugin, not used
    |           `-- form.phtml       <-- used, the theme has no replacement
    `-- themes/
        `-- default/
            `-- templates/
                `-- plugins/
                    `-- Orders/
                        `-- list.phtml   <-- your file, the framework uses it
  2. Template directory with a postfix. Set the Core option plugin_template_postfix (Core::OPTION_PLUGIN_TEMPLATE_POSTFIX), for example to _site. If a plugin has the directory templates_site, the plugin uses it as its template directory. The plugin then does not use its templates directory. Thus templates_site must contain all the templates of that plugin.

  3. Event listener. Add a listener for ITheme::EVENT_THEME_BEFORE_FETCH_MAIN. The listener gets the page content by reference and can change it. See the Theme package.
  4. Subclass. Make a plugin that extends the original plugin, and render your own template in the method that you override.
  5. Extension point in the plugin. If you are the owner of the plugin, add an extension point. For example, render the template with fetchFrom() and a directory that a project can set, or dispatch an event that lets a listener replace the HTML.

Give data to the theme

The theme layout (main.phtml) does not get the variables of a plugin. A plugin gives data to the theme only in these ways:

Data How the plugin gives it How the theme reads it
Page content $response->content $content in main.phtml
Page title $this->core->seo = ['title' => 'Users'] $info['seo']['title'] in head.phtml
JS and CSS files $this->includeStatic('js/users.js') $info['js'], $info['css'] in head.phtml
Other SEO data $this->core->seo = [...] or onMeta() $info['seo']
HTML for a specified location of the layout A listener for a theme event The theme prints the result of the event

See Static files for JS and CSS files.

IDE type hints in plugin templates

A plugin template runs in the scope of the plugin instance. An IDE cannot find this automatically. Add a PHPDoc block at the top of each template. Declare $this and all the variables of the template:

<?php
/**
 * @var ContentsPlugin $this
 * @var array          $contents
 * @var int            $currentPage
 * @var string         $resourceTitle
 */
?>

Rules:

  • Use the class name of the plugin for $this (for example ContentsPlugin). Do not use AbstractPlugin.
  • Declare each variable that the plugin method assigns.
  • Change the block when you add or remove a variable in the plugin.

Rules for templates

  • Use the full tag <?php echo ...; ?>. Do not use the short tag <?=.
  • Use __() for each text that a user sees.
  • Use $this->getUrl() for each URL.
  • Use HUI components for forms, buttons, and dialogs. Do not write this HTML by hand.
  • Do not query the database in a template. Get the data in the plugin method and assign it.
  • Do not call includeStatic() in a template. Call it in the plugin method. See Static files.