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.
  • DefaultTheme and ITheme: 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

Related packages:

  • ThemeAssets: the shared JavaScript and CSS files, with jimbo.js. A theme includes it as its assets directory.
  • 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 the ITheme interface. The default class is DefaultTheme.
    • templates/head.phtml - The content of the <head> section. Common variables:
      • jsVersion - js_version from the settings table
      • siteCaption - site_caption from the settings table
      • host - host from 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 the Theme.php file:

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.

Theme Event User Activity

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.

Theme Event Projects List

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.

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.

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.phtml must 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:

  1. 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)
    ];
  2. 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_TITLE event 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']; } );
  3. Override onMeta() in DisplayPlugin: If your plugin extends the DisplayPlugin, you can override the onMeta() 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.