Festi Theme
The Festi Theme package (festi-team/festi-framework-theme) is the theme system of the Festi Framework. Use it in each project that renders HTML.
The package contains:
DisplayPlugin: the base class of each plugin that renders HTML.DefaultThemeandITheme: the theme object that renders the page layout.- HUI: PHP components for forms, buttons, dialogs, and other elements.
- The default templates for the HUI components and for the Data Grid Store (DGS).
- Theme events, which let a plugin add content to the page layout.
The package does not contain a page layout, CSS, or JavaScript. A theme supplies them.
Documentation
- How to Make a Custom Theme: how a theme works, its directory, its templates, its lifecycle
- Theme Structure
- Theme-Triggered Events
- HUI: The PHP UI Components: how the components work, how to replace a component template, how to add a component
- UI Components reference
- How to Work with SEO
- Unit Test for a Theme
- CSS Naming Convention
Related packages:
- ThemeAssets: the shared JavaScript and CSS files, with
jimbo.js. A theme includes it as itsassetsdirectory. - SmartAdmin: the primary theme for admin panels and dashboards.
Theme Structure
A theme is a directory in the project: project_directory/themes/[ThemeName].
templates- The directory of the PHTML templates of the theme.templates/main.phtml- The main layout file. The scope of this file is an object that implements theIThemeinterface. The default class isDefaultTheme.templates/head.phtml- The content of the<head>section. Common variables:jsVersion-js_versionfrom the settings tablesiteCaption-site_captionfrom the settings tablehost-hostfrom the settings table
css- The directory of the CSS files of the theme.js- The directory of the JavaScript files of the theme.assets- The ThemeAssets files (a git submodule).Theme.php- (Optional) A file in the root of the theme that extends the default theme class.tests- The directory of the unit tests of the theme.
See How to Make a Custom Theme for the full procedure.
Theme Class
By default, the DefaultTheme class is used for theme functionality. If you want to add custom logic or override the existing behavior, you need to create a Theme.php file in the root of your theme directory. Inside this file, you will define a Theme class that either implements the ITheme interface or extends the DefaultTheme class.
If you want to create a theme class with a different name (e.g.,
YourThemeClass), you must return that class at the end of theTheme.phpfile:
class YourThemeClass extends DefaultTheme
{
public const EVENT_THEME_HEADER_CONTENT_RIGHT = 'ThemeHeaderContentRight';
public function fetchRightHeaderContent(): string
{
$list = $this->doThemeEvent(static::EVENT_THEME_HEADER_CONTENT_RIGHT);
return join('', $list);
}
}
return YourThemeClass::class;
Theme-Triggered Events
The theme triggers events that let a plugin add content to a specified location of the layout, for example the header or the menu. Add a listener for an event to change that part of the page.
All events of ITheme:
| Event | When | Result of a listener |
|---|---|---|
EVENT_THEME_BEFORE_FETCH_MAIN |
Before the theme renders the layout | The listener can change content (by reference) or register static files. |
EVENT_THEME_HEAD_BOTTOM |
fetchHeadBottom() |
The returned HTML goes before </head>. |
EVENT_THEME_FETCH_MENU |
fetchMenu(), fetchCustomMenu() |
The returned HTML replaces the default menu. |
EVENT_THEME_FETCH_FOOTER |
fetchFooter() |
The returned HTML goes into the footer. |
EVENT_THEME_FETCH_HEADER_ELEMENT |
A theme renders a header element (SmartAdmin) | The returned HTML goes into the header. |
EVENT_THEME_HEADER_USER_ACTIVITY |
fetchUserActivities() |
The returned HTML goes into the user activity block. |
EVENT_THEME_PROJECTS_LIST |
fetchProjectsList() |
The returned HTML goes into the projects list block. |
EVENT_THEME_FETCH_GOOGLE_ANALYTICS |
fetchGoogleAnalytics() |
The returned HTML replaces the default snippet. |
EVENT_THEME_ON_PREPARE_LOGO |
fetchLogo() |
The listener can change link, logo, or set content in the target. |
EVENT_THEME_TITLE |
fetchThemeTitle() |
The listener can change title in the target. |
EVENT_THEME_USER_INFO_URL |
fetchUserInfoUrl() |
The listener can set user_info_url in the target. |
EVENT_THEME_VIEWPORT |
fetchViewport() |
The listener can change directives in the target. |
The sections below give details and examples.
ITheme::EVENT_THEME_HEADER_USER_ACTIVITY
Triggered when rendering the user activity block in the header, right next to the logo. Listener return values are concatenated and rendered in that block.

Listening:
$core->addEventListener(
ITheme::EVENT_THEME_HEADER_USER_ACTIVITY,
[&$core->getPluginInstance('Notifications'), 'onFetchUserActivity']
);
Handler example (return rendered string):
public function onFetchUserActivity(FestiEvent &$event)
{
$search = [
'id_user' => $this->getUserID(),
'status' => 'new'
];
$this->notifications = $this->object->get($search);
return $this->fetch('notifications.phtml');
}
ITheme::EVENT_THEME_PROJECTS_LIST
Triggered when rendering the project list block in the header, next to the user activity block.

Listening:
$core->addEventListener(
ITheme::EVENT_THEME_PROJECTS_LIST,
[&$systemPlugin, 'onThemeHeaderProjectsList']
);
ITheme::EVENT_THEME_FETCH_MENU
Triggered before rendering the left menu. If a listener returns a value, the default menu is not displayed.
fetchMenu() fires this event. A layout that prepares the menu items one time and renders them with fetchMenuItems() must call fetchCustomMenu() first. fetchCustomMenu() returns the HTML of the listeners, or null when no listener returns a menu.
Listening:
$core->addEventListener(
ITheme::EVENT_THEME_FETCH_MENU,
[&$systemPlugin, 'onThemeFetchMenu']
);
ITheme::EVENT_THEME_USER_INFO_URL
Triggered before rendering the user info link in the header or menu. If a listener returns a value, it is used as the profile URL.
Listening:
$core->addEventListener(
ITheme::EVENT_THEME_USER_INFO_URL,
[&$systemPlugin, 'onThemeUserInfoUrlHandler']
);
ITheme::EVENT_THEME_FETCH_GOOGLE_ANALYTICS
Triggered before rendering the Google Analytics block. If a listener returns a value, the default snippet is not displayed.
Listening:
$core->addEventListener(
ITheme::EVENT_THEME_FETCH_GOOGLE_ANALYTICS,
[&$systemPlugin, 'onThemeFetchGoogleAnalytics']
);
ITheme::EVENT_THEME_HEAD_BOTTOM
Triggered before the </head> tag is closed. Use it to inject markup or script (e.g. snippets, extra CSS/JS) into the head.
Listening:
$core->addEventListener(
ITheme::EVENT_THEME_HEAD_BOTTOM,
[&$chatraPlugin, 'onThemeHeadAddChat']
);
Returning a string from the listener (e.g. snippet):
$this->core->addEventListener(
ITheme::EVENT_THEME_HEAD_BOTTOM,
function (ThemeEvent &$event) {
return $this->fetch('snippet_code_submit.phtml');
}
);
ITheme::EVENT_THEME_BEFORE_FETCH_MAIN
Triggered in DisplayPlugin::fetchMain() right before theme->fetchMain(). Use it to include plugin static (CSS/JS) so the theme outputs them in the head.
Target:
* content – the HTML of the page content (by reference; a listener can change it)
Listening (init.php):
$this->addEventListener(
ITheme::EVENT_THEME_BEFORE_FETCH_MAIN,
function (FestiEvent &$event) {
$plugin = Core::getInstance()->getPluginInstance('My');
$plugin->includeStatic('js/plugin.js');
$plugin->includeStatic('css/plugin.css');
}
);
Get the plugin instance in the listener, not in init.php. When init.php runs, the database connection is not ready.
Custom Theme Events
A custom theme class can dispatch its own events. doThemeEvent() returns an array with the results of the listeners:
echo join('', $this->doThemeEvent('ThemeSidebar'));
See The theme class for a full example.
MenuEvent::EVENT_ON_PREPARE_MENU_ITEMS
Triggered when the theme prepares the menu items. Use it to add, remove, or change menu items. The event class is Festi\Theme\Event\MenuEvent.
Target:
* items – array of menu items (handlers may mutate)
* area – the area of the menu
* activeItem – name of the current active item
* currentItem – current URL
Listening:
$core->addEventListener(
MenuEvent::EVENT_ON_PREPARE_MENU_ITEMS,
function (MenuEvent &$event) {
$items = &$event->getItems();
// modify $items
}
);
ISEODataProvider::EVENT_TITLE
Allows overriding the page title. Modify the target to change the title.
Target:
* title – the title of the page
Listening:
$this->core->addEventListener(
ISEODataProvider::EVENT_TITLE,
function (ThemeEvent &$event) use ($data) {
$target = &$event->getTarget();
$target['title'] = $data['meta_title'];
}
);
ISEODataProvider::EVENT_DESCRIPTION
Same structure as ISEODataProvider::EVENT_TITLE; target key description.
ISEODataProvider::EVENT_KEYWORDS
Same structure as ISEODataProvider::EVENT_TITLE; target key keywords.
BreadcrumbsEvent::EVENT_ON_BEFORE_UPDATE_ITEMS
Triggered when a plugin sets the items of the breadcrumbs component ($this->ui->breadcrumbs()->setItems()). Use it to add, remove, or change breadcrumb items. The event class is Festi\Theme\Event\BreadcrumbsEvent.
Event data
* getInstance() – the breadcrumbs component (IBreadcrumbs). Its method getItems() returns the items by reference.
Listening:
$core->addEventListener(
BreadcrumbsEvent::EVENT_ON_BEFORE_UPDATE_ITEMS,
function (BreadcrumbsEvent &$event) {
$items = &$event->getInstance()->getItems();
$items = [__('Home') => '/'] + $items;
}
);
ISEODataProvider::EVENT_HEAD_TAGS
Allows plugins to inject custom <meta>, <link>, or other tags into the <head> section. Use this for hreflang, Open Graph locale alternates, custom meta directives, or any structured head tag.
Tags are associative arrays with a tag key (element name) and arbitrary attribute key-value pairs.
Target:
* tags – array of tag arrays (handlers may append, modify, or filter)
Listening:
$core->addEventListener(
ISEODataProvider::EVENT_HEAD_TAGS,
function (ThemeEvent &$event) {
$event->target['tags'][] = [
'tag' => 'link',
'rel' => 'alternate',
'hreflang' => 'uk',
'href' => 'https://example.com/uk/page/',
];
$event->target['tags'][] = [
'tag' => 'link',
'rel' => 'alternate',
'hreflang' => 'en',
'href' => 'https://example.com/en/page/',
];
$event->target['tags'][] = [
'tag' => 'link',
'rel' => 'alternate',
'hreflang' => 'x-default',
'href' => 'https://example.com/uk/page/',
];
}
);
You can also add tags imperatively via DefaultSEODataProvider::addHeadTag():
$provider = $theme->getSEODataProvider();
$provider->addHeadTag('meta', ['name' => 'robots', 'content' => 'noindex']);
Theme template requirement: For custom head tags to render, the theme's
head.phtmlmust include the rendering block. Add this before<?php echo $this->fetchHeadBottom(); ?>:
<?php
if (!empty($headTags)) {
foreach ($headTags as $headTag) {
$tagName = $headTag['tag'] ?? 'meta';
unset($headTag['tag']);
$attrs = '';
foreach ($headTag as $key => $value) {
$attrs .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
}
echo ' <' . htmlspecialchars($tagName) . $attrs . ' />' . "\n";
}
}
?>
Unit Test for a Theme
Unit testing a theme helps ensure that the theme behaves as expected when interacting with events, rendering content, and applying custom logic. Below is a sample unit test that verifies if content is correctly injected into the right section of the header using the Theme::EVENT_THEME_HEADER_CONTENT_RIGHT event.
class HeaderThemeTest extends ThemeTestCase
{
public function testAddContentRight(): void
{
$testContent = 'testAddContentRight';
$this->core->addEventListener(
Theme::EVENT_THEME_HEADER_CONTENT_RIGHT,
function (FestiEvent &$event) use ($testContent) {
return $testContent;
}
);
$content = '';
$displayPlugin = new DisplayPlugin();
$result = $displayPlugin->fetchMain($content);
$this->assertTrue(is_int(strpos($result, $testContent)));
}
}
How to Work with SEO
In the Festi Framework, you have multiple ways to manage and modify SEO data such as titles, descriptions, and keywords. This can be done through the core's seo array, by using events, or by extending the DisplayPlugin class to handle SEO data more dynamically.
Here’s how you can work with SEO data:
- Set SEO Data in Core: You can define SEO attributes like title, description, keywords, og:type, and og:image by assigning them to the core's seo array:
$this->core->seo = [ 'title' => $data['meta_title'], // Meta title of the page 'description' => $data['meta_description'], // Meta description of the page 'keywords' => $data['meta_keywords'], // Meta keywords for SEO 'type' => $data['og_type'], // Open Graph type (e.g., article, website) 'image' => $data['og_image'] // Open Graph image (for social media sharing) ]; - Override SEO Title Using Event: If you need to dynamically change the title (or any other SEO attributes) based on certain conditions or logic, you can listen to the
ISEODataProvider::EVENT_TITLEevent and modify the title:php $this->core->addEventListener( ISEODataProvider::EVENT_TITLE, function (ThemeEvent &$event) use ($data) { // Overriding the page title $target = &$event->getTarget(); $target['title'] = $data['meta_title']; } ); - Override
onMeta()in DisplayPlugin: If your plugin extends theDisplayPlugin, you can override theonMeta()method to further customize or extend SEO functionality. This method allows you to modify the seo data and merge it with other fields if needed.protected function onMeta(array &$seo): void { // If SEO data exists in core, use it if (isset($this->core->seo)) { $seo = $this->core->seo; } // Define additional meta fields $metaFields = [ 'title' => Entity::FIELD_TYPE_STRING_NULL, 'description' => Entity::FIELD_TYPE_STRING_NULL, 'keywords' => Entity::FIELD_TYPE_STRING_NULL, 'image_src' => Entity::FIELD_TYPE_STRING_NULL ]; // Merge and extend existing SEO data with additional fields $seo = $this->getExtendData($seo, $metaFields); }
CSS Naming Convention
Festi uses a structured CSS class naming system that improves clarity and maintainability. This system categorizes styles by their role: block, element, or modifier.
Naming Rules
b-– Block: Represents a standalone UI section or component.e-– Element: A part within a block that fulfills a specific function.m-– Modifier: A variant or state of a block or element.
Example
<div class="b-form">
<div class="e-title m-uppercase"></div>
</div>
b-form: Defines the main form container as a reusable block.e-title: A title element that exists within the form block.m-uppercase: A modifier that transforms the element’s text to uppercase.
Guidelines
- Use full prefixes for clarity (
b-,e-,m-). - Keep class names descriptive and consistent.
- Avoid chaining multiple modifiers (e.g.,
m-bold--red); instead, use separate modifier classes for composability.
This naming convention ensures styles remain modular, predictable, and easy to maintain across different themes and components.