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. |
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.phpsignup.phtmlforgot_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.
- Find the name of the plugin, for example
Orders. - Make the directory
templates/plugins/Orders/in the theme. - 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
$thisin 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. ExtendDefaultThemeto 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:
- The theme class dispatches an event with
doThemeEvent(). - A plugin adds a listener for the event and returns HTML.
- 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
- 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
$contentand$info. It does not get the variables that the plugin assigned to its templates. $thisin a layout template is the theme object.$this->uigives 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
Rules
- Do not put business logic in a layout template. Put it in a plugin, and use an event to get the HTML.
- Print
$contentone time inmain.phtml. - Print
$info['css']and$info['js']inhead.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-, andm-. 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.