How to Make a Custom Theme

This page tells you how a theme works and how to make a theme for a website.

A theme is a directory in the project. It contains the layout templates, the CSS files, and the JavaScript files of the pages. A plugin makes the content of a page. The theme puts the content into the layout.

See also Templates and Static files in the framework documentation.

Theme, Theme package, and ThemeAssets

Three different things have "theme" in their name. Do not mix them.

Name What it is What it contains
Theme A directory in the project: themes/<name>/ main.phtml, head.phtml, CSS, JS, images. You make it, or you install one (for example SmartAdmin).
Theme package The Composer package festi-team/festi-framework-theme (this repository) The PHP classes (DefaultTheme, DisplayPlugin), the HUI components, and the default templates for HUI and DGS. It has no page layout and no CSS.
ThemeAssets The repository FestiFramework_ThemeKit Shared JS and CSS files: jimbo.js, jQuery plugins, Bootstrap 3. A theme includes it as its assets directory.
flowchart LR subgraph Project T["Theme<br>themes/default/"] A["ThemeAssets<br>themes/default/assets/"] P["Plugin templates"] end subgraph Vendor TP["Theme package<br>DefaultTheme, DisplayPlugin, HUI"] end P -- content --> TP TP -- "renders main.phtml" --> T T -- "loads jimbo.js" --> A

How the framework selects the theme

The entry script of the application (index.php) gives the theme options to Core.

Core option Constant Default Use
theme_name Core::OPTION_THEME_NAME default The name of the theme directory.
theme_path Core::OPTION_THEME_PATH <project root>/themes/<theme_name>/ The directory of the theme. The framework looks for Theme.php and include.php here.
theme_template_path Core::OPTION_THEME_TEMPLATE_PATH <project root>/themes/<theme_name>/templates/ The directory of the layout templates.
theme_url Core::OPTION_THEME_URL <http_base>themes/<theme_name>/ The URL of the theme directory, for CSS, JS, and images.

The default of theme_template_path comes from theme_name, not from theme_path. Thus a project that sets theme_path to a different directory must also set theme_template_path.

Usually you set only theme_name:

$options = [
    'theme_name' => 'default',
];

$core = Core::getInstance($options);

Each entry script can use a different theme. For example, the public website uses themes/default and the admin panel uses themes/dashboard:

// index.php (website)
$options = ['theme_name' => 'default'];

// dashboard.php (admin panel)
$options = ['theme_name' => 'dashboard'];

The directory of a theme

themes/
`-- default/
    |-- Theme.php            (optional) the theme class
    |-- include.php          (optional) PHP code that runs for each request
    |-- assets/              ThemeAssets (git submodule)
    |-- css/
    |-- js/
    |-- images/
    `-- templates/
        |-- main.phtml       the page layout (necessary)
        |-- head.phtml       the content of <head> (necessary if main.phtml calls fetchHead())
        |-- menu.phtml       the menu (necessary if main.phtml calls fetchMenu())
        |-- logo.phtml       the logo (necessary if main.phtml calls fetchLogo())
        |-- notifications.phtml
        |-- ...              other templates of the theme
        |-- ui/              (optional) replacements for HUI component templates
        `-- plugins/
            `-- <Name>/      (optional) replacements for the templates of the plugin <Name>

The layout templates are in the templates directory, not in the root of the theme.

Procedure: make a theme for a website

1. Make the directories

mkdir -p themes/default/templates themes/default/css themes/default/js themes/default/images

2. Add ThemeAssets

Add ThemeAssets if the pages use Jimbo.js, DGS lists, DGS forms, or HUI dialogs.

git submodule add [email protected]:FestiCore/FestiFramework_ThemeKit.git themes/default/assets

3. Make templates/main.phtml

main.phtml is the layout of each page. The theme object renders it, thus $this is the theme (ITheme).

<?php
/**
 * @var \Festi\Theme\ITheme $this
 * @var string $content
 * @var array $info
 */
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <?php echo $this->fetchHead(); ?>
</head>
<body>
    <header class="b-header">
        <?php echo $this->fetchLogo(); ?>
        <nav class="e-menu"><?php echo $this->fetchMenu(); ?></nav>
    </header>

    <?php echo $this->fetchNotifications(); ?>

    <main class="b-content"><?php echo $content; ?></main>

    <footer class="b-footer"><?php echo $this->fetchFooter(); ?></footer>

    <?php echo $this->fetchGoogleAnalytics(); ?>
</body>
</html>

Variables in main.phtml:

Variable Value
$content The HTML that the plugin made. Print it one time.
$info Page properties. See the table below.
$jsVersion The value of the js_version setting. Add it to the URL of your CSS and JS files.

Keys of $info:

Key Value
http_base The base URL of the application.
theme_url The URL of the theme directory.
engine_url The URL of the framework directory.
charset The character set of the page.
title The value that a plugin set with Core::setTitle(). The key exists only if a plugin set it. For the page title, prefer $info['seo']['title'].
css The list of CSS URLs that plugins registered. The key exists only if a file is registered.
js The list of JS URLs that plugins registered. The key exists only if a file is registered.
seo SEO data: title, description, keywords, site_caption, favicon_url, logo_url, canonical_url.

4. Make templates/head.phtml

fetchHead() renders head.phtml. This template must print the CSS and JS files that the plugins registered. If it does not, includeStatic() in a plugin has no effect.

<?php
/**
 * @var \Festi\Theme\ITheme $this
 * @var array $info
 * @var string $jsVersion
 * @var string|null $siteCaption
 * @var array $headTags
 */
$title = $info['seo']['title'] ?? '';
?>
<meta charset="<?php echo $info['charset']; ?>">
<meta name="viewport" content="<?php echo htmlspecialchars($this->fetchViewport()); ?>">
<title><?php echo htmlspecialchars($title.' '.$siteCaption); ?></title>
<meta name="description" content="<?php echo htmlspecialchars($info['seo']['description'] ?? ''); ?>">

<!-- CSS of the theme -->
<link rel="stylesheet" href="<?php echo $info['theme_url']; ?>css/main.css?v<?php echo $jsVersion; ?>">

<!-- Libraries and Jimbo.js -->
<script src="<?php echo $info['theme_url']; ?>assets/js/jquery.min.js"></script>
<script src="<?php echo $info['theme_url']; ?>assets/js/jimbo.js?v<?php echo $jsVersion; ?>"></script>

<!-- CSS files that plugins registered -->
<?php foreach ($info['css'] ?? [] as $path) : ?>
    <link rel="stylesheet" href="<?php echo $path; ?>?v<?php echo $jsVersion; ?>">
<?php endforeach; ?>

<!-- JS files that plugins registered -->
<?php foreach ($info['js'] ?? [] as $path) : ?>
    <script src="<?php echo $path; ?>?v<?php echo $jsVersion; ?>"></script>
<?php endforeach; ?>

<!-- Tags from ISEODataProvider::EVENT_HEAD_TAGS -->
<?php foreach ($headTags ?? [] as $headTag) : ?>
    <?php
    $tagName = $headTag['tag'] ?? 'meta';
    unset($headTag['tag']);
    $attributes = '';
    foreach ($headTag as $key => $value) {
        $attributes .= ' '.htmlspecialchars($key).'="'.htmlspecialchars($value).'"';
    }
    ?>
    <<?php echo htmlspecialchars($tagName).$attributes; ?> />
<?php endforeach; ?>

<?php echo $this->fetchHeadBottom(); ?>

Variables in head.phtml:

Variable Value
$jsVersion The js_version setting. Default: 1.
$siteCaption The site_caption setting.
$host The host setting.
$themeUrl The URL of the theme directory.
$headTags Tags that listeners of ISEODataProvider::EVENT_HEAD_TAGS added.
$info The same array as in main.phtml.

5. Make the templates that your layout calls

Each fetch* method of the theme renders a template or dispatches an event. Make the template only if main.phtml calls the method.

Method Template Variables in the template
fetchHead() head.phtml See step 4.
fetchLogo() logo.phtml $link (base URL), $logo (<http_base>static/images/logo.png)
fetchMenu($area, $template) menu.phtml, or the name in $template $items, $area, $currentItem, $activeItem
fetchNotifications() notifications.phtml $notifications (list of messages for the user)

templates/logo.phtml:

<a href="<?php echo $link; ?>">
    <img src="<?php echo $logo; ?>" alt="logo">
</a>

templates/notifications.phtml (this example uses Jimbo.js):

<?php if ($notifications) : ?>
    <script>
        jQuery(document).ready(function () {
            <?php foreach ($notifications as $message) : ?>
                Jimbo.growlCreate(<?php echo json_encode(__('Notification')); ?>, <?php echo json_encode($message); ?>, false);
            <?php endforeach; ?>
        });
    </script>
<?php endif; ?>

These methods do not use a template. They print the results of event listeners:

Method Event
fetchHeadBottom() ITheme::EVENT_THEME_HEAD_BOTTOM
fetchFooter() ITheme::EVENT_THEME_FETCH_FOOTER
fetchUserActivities() ITheme::EVENT_THEME_HEADER_USER_ACTIVITY
fetchProjectsList() ITheme::EVENT_THEME_PROJECTS_LIST
fetchGoogleAnalytics() ITheme::EVENT_THEME_FETCH_GOOGLE_ANALYTICS, or the google_analytics setting
fetchViewport() ITheme::EVENT_THEME_VIEWPORT

See Theme events.

6. Add the sign-in templates

Do this step only if the Jimbo system plugin shows its sign-in pages with this theme. Jimbo renders these templates, and it does not contain them. Put them in the templates directory of the theme or in the templates directory of the project:

  • signin.php
  • signup.phtml
  • forgot_password.phtml

Copy them from the SmartAdmin theme and change the HTML. In these templates $this is the Jimbo plugin.

7. Set the theme in the entry script

$options = [
    'theme_name' => 'default',
];

$core = Core::getInstance($options);

8. Make sure that the web server serves the theme

The browser must get the files at <http_base>themes/<theme_name>/. Open the URL of one CSS file in the browser. The server must return the file.

Use a different layout for one page

A plugin method can select a different layout template. Set mainTemplate in the Response object.

public function onDisplayPrintVersion(Response &$response): bool
{
    $response->content = $this->fetch('invoice.phtml');
    $response->mainTemplate = 'print.phtml';

    return true;
}

The theme then renders templates/print.phtml and not main.phtml.

Replace a template of a plugin

A theme can replace one template of one plugin. No change to the plugin is necessary.

  1. Find the name of the plugin, for example Orders.
  2. Make the directory templates/plugins/Orders/ in the theme.
  3. Put a file with the same name and the same subdirectory as the plugin template into it.
themes/default/templates/plugins/Orders/list.phtml       replaces plugins/Orders/templates/list.phtml
themes/default/templates/plugins/Orders/ui/button.phtml  replaces the HUI button template for the Orders plugin
  • $this in the template is the plugin, the same as in the plugin template. The template gets the same variables.
  • The plugin looks in this directory first, for each template that it renders with fetch().
  • A template of a DGS (store/list.phtml) is not a plugin template. See DGS templates.

The full lookup is in Templates.

The theme class

The default class of the theme object is Festi\Theme\DefaultTheme. Make a file Theme.php in the root of the theme when the layout needs more PHP logic.

<?php

use Festi\Theme\DefaultTheme;

class Theme extends DefaultTheme
{
    public const EVENT_THEME_SIDEBAR = 'ThemeSidebar';

    public function fetchSidebar(): string
    {
        $list = $this->doThemeEvent(static::EVENT_THEME_SIDEBAR);

        return join('', $list);
    }
}

Then call the new method in main.phtml:

<aside class="b-sidebar"><?php echo $this->fetchSidebar(); ?></aside>

Rules for Theme.php:

  • The class must implement Festi\Theme\ITheme. Extend DefaultTheme to get the default methods.
  • If the class name is Theme, the file does not return a value.
  • If the class has a different name, the file must return the class name: return MyTheme::class;

Add a location that plugins can fill

The example above is the usual method to let plugins add HTML to the layout:

  1. The theme class dispatches an event with doThemeEvent().
  2. A plugin adds a listener for the event and returns HTML.
  3. The theme prints the results of all listeners.

Listener in the init.php of a plugin:

$this->addEventListener(
    'ThemeSidebar',
    function (FestiEvent &$event) {
        return '<div class="b-banner">...</div>';
    }
);

Use the event name as text in init.php. The framework loads Theme.php only when it renders the first page. Thus the class Theme and its constants are not available when init.php runs.

The lifecycle of the theme object

sequenceDiagram participant P as Plugin (DisplayPlugin) participant C as Core participant T as Theme object P->>C: Event EVENT_THEME_BEFORE_FETCH_MAIN (content by reference) P->>P: getInfo(): options and registered CSS and JS P->>P: onDisplay(), onMeta() alt first page render of the request P->>T: new Theme($info, $core) T->>T: onInit() else theme object exists P->>T: setOptions($info) end P->>T: fetchMain($content, 'main.phtml') T->>T: fetch('main.phtml') T-->>P: HTML of the page
  • The framework makes the theme object when a plugin renders the first full page of the request.
  • There is one theme object for each request. All plugins use the same object.
  • An AJAX request or a dialog request does not render the layout. For such a request the framework does not make the theme object.
  • The theme object gets from the plugin only $content and $info. It does not get the variables that the plugin assigned to its templates.
  • $this in a layout template is the theme object. $this->ui gives the HUI components.

include.php

If the file <theme_path>include.php exists, the Jimbo system plugin includes it for each request, before the event Core::EVENT_ON_REQUEST. Use it for PHP code that the theme needs for each request, for example event listeners of the theme.

SEO data

See How to Work with SEO.

Test the theme

See Unit Test for a Theme.

Rules

  • Do not put business logic in a layout template. Put it in a plugin, and use an event to get the HTML.
  • Print $content one time in main.phtml.
  • Print $info['css'] and $info['js'] in head.phtml.
  • Add ?v<jsVersion> to the URLs of the CSS and JS files of the theme.
  • Use the CSS names with the prefixes b-, e-, and m-. See CSS Naming Convention.
  • Do not edit a theme that comes from a package or from a submodule. Make your own theme, or use the override procedures of that theme.