Creating a Module - Widgets

Creating a Module

Modules are handlers for Widgets - all Widgets have a Module backing them to provide add/edit and render operations. The “Module” is responsible for providing the code for all actions that take place on Widgets, for example: Add, Edit, Preview, and to generate the full HTML to show the module.

A note on future development

Xibo’s Module/Widget system was first conceived for our 1.8 release, the foundations of which were being built at the end of 2014. In the following 8 years the way we write Modules has been squeezed into various different shapes to meet the needs of the product and our users, but it is fair to say that our thinking on how a module system should work, has advanced beyond what our current architecture can support.

As a developer, you will see things below that make you wince - we know, because we do. You’ll be exposed to internal bits of code that you never need to touch, asked to provide URLs you’ve no need to know exist, and generally have to do more and know more than we think you should.

We are working to improve this!!!

Xibo v4 will overhaul how developers create new widgets and their templates, with a focus on the developer experience. We will present a much simplified coding journey, hiding away concepts that do not need to be understood in order to make a Widget which is great looking and functionally brilliant.

Module Type

The “Type” property on a module determines which Widgets that module tries to serve code for. All core modules have a type and so should all custom modules. Type is not unique, but Xibo will only load one type of module at a time, on a first enabled in the list basis.

The type system can be used to override core modules to provide different functionality, for example a different “get tweets” method for the standard Twitter module.

Usually you will want to give your module a unique type - not mymodule as in this example.

The module name/type are used interchangeably until the module is installed, when module->type becomes the important field.
Screenshot/Image

Custom code

All modules provide a class which extends \Xibo\Widget\ModuleWidget and implements \Xibo\Widget\ModuleInterface. They must also provide a <<modulename>>.json file in the /custom folder for installation. Core modules that ship with the software have their json file in the /modules folder.

This json file contains the information necessary for the CMS to instantiate the module the first time it is installed into the module database table. After installation, this file is not used again.

An example is below:

{
  "title": "My Custom Xibo Module",
  "author": "Xibo Signage Ltd",
  "description": "A module for displaying some information I am interested in",
  "name": "mymodule",
  "class": "Xibo\\Custom\\MyModule\\MyModule"
}

Where do I put my code?

When you are making a custom module you should place all of your files under the /custom folder, in their own sub folder. The above example would be called mymodule.json and stored in /custom/mymodule.json with the associated class stored in /custom/MyModule/MyModule.php. Your MyModule class must exist in the \Xibo\Custom namespace for it to be auto-loaded correctly.

Twig templates

Each module entity has a viewPath which should be the location of the Twig Template used to render the forms for that module. You set this during installation below and would typically be located inside the /custom folder, for example /custom/MyModule/*.twig.

Installation

The Module Admin page in the CMS has an action button called “Install Modules”. This button looks in the /custom folder for all *.json files and loads any that are not already installed in the modules table. It will present these Modules according to the contents of the file, in the example above you would expect to see an entry for “My Custom Xibo Module - Xibo Signage Ltd”.

When clicked, the Module is instantiated using the class provided in the JSON file, and the installOrUpdate method is called. An example implementation of this method is below:

/**
 * Install or Update this module
 * @param \Xibo\Factory\ModuleFactory $moduleFactory
 * @throws \Xibo\Support\Exception\GeneralException
 */
public function installOrUpdate($moduleFactory)
{
    if ($this->module == null) {
        // Install
        $module = $moduleFactory->createEmpty();
        $module->name = 'My Module';
        $module->type = 'mymodule';
        $module->class = 'Xibo\Custom\MyModule\MyModule';
        $module->description = 'A module for displaying my information.';
        $module->enabled = 1;
        $module->previewEnabled = 1;
        $module->assignable = 1;
        $module->regionSpecific = 1;
        $module->renderAs = 'html';
        $module->schemaVersion = $this->codeSchemaVersion;
        $module->defaultDuration = 60;
        $module->settings = [];
        $module->viewPath = '../custom/MyModule';

        // Set the newly created module and then call install
        $this->setModule($module);
        $this->installModule();
    }

    // Install and additional module files that are required.
    $this->installFiles();
}

This method is only called once! So it is important that the information is correct before you install.

What do all these properties mean?

There are a lot of properties to set on a module, which govern how that module is loaded, where its templates are stored and how the Player renders it. Each property is explained below:

Property Explanation Default
name This is the friendly name of your module. It will be shown in the Layout Designer
type This is the internal identifier for your module and is what gets recorded on the Layout XLF file and sent to the Player. This does not have to be unique, the CMS will choose the first available module of a particular type to render a Widget.
class This is the PHP class associated with the Module. It will be instantiated whenever this module is used. This should always include the namespace.
description A description for the module, only used on the Module admin page
imageUri DEPRECATED - this option will be removed forms/library.gif
enabled A flag indicating whether the module is enabled 1
previewEnabled Should the Layout designer render a preview of the module or not? 1
assignable Should this module be assignable - used for Library modules. 1
regionSpecific Is this Module for the Library (0) or a Widget on a Layout (1) 1
renderAs Render natively or as HTML. If HTML then the Module should provide the HTML from getResource, and if native the player must understand how to render the module and its options. html
schemaVersion Schema Version - can use used to determine different rendering from past versions. 1
defaultDuration When the user has declined to provide a duration for the Widget, what should the duration be. 60
settings An array of settings. []
viewPath The view path containing the Twig forms for add, edit and settings. This should be /custom/MyModule

Complimentary Files

The Module must also provide an installFiles() method which is called after Install and also on upgrade or when the user manually verifies their module files.

The responsibility of this method is to install any complimentary files required by the module - for example if the HTML rendered uses jQuery, or one of the Xibo rendering libraries (layout scaler, etc). An example of this method is below:

/**
 * Install Files
 */
public function installFiles()
{
  // Use JQuery and some Xibo resources
  $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/vendor/jquery-1.11.1.min.js')->save();
  $this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/modules/xibo-layout-scaler.js')->save();
  
  // Install files from a folder
  $folder = PROJECT_ROOT . '/custom/MyModule/resources';
  foreach ($this->mediaFactory->createModuleFileFromFolder($folder) as $media) {
    /* @var Media $media */
    $media->save();
  }
}

These files are then copied to the library and available for later reference in the CMS and also on the Players.

Please note it is important that all files have unique names, discounting the folder structure. Files are transferred to the Player and stored in a flat folder structure.

Adding an Icon to a Custom Module

You will need to add the CSS for your custom module icon to override.css in either the default or custom theme, whichever is in use. The ModuleWidget class creates an empty CSS class called .module-icon-<module_name> which can be used with override.css.

To set the custom module icon you can also use Layout Designer JavaScript explained below.

Settings

It may be necessary to provide global settings for a module, typically useful for something like a user entered API key. This is done via a settings form accessible on the Module Admin page.

There are two methods that should be implemented to support this.

/**
 * Form for updating the module settings
 */
public function settingsForm()
{
  // Return the name of the TWIG file to render the settings form
  return 'mymodule-form-settings';
}

/**
 * Process any module settings
 */
public function settings()
{
  // Process any module settings you asked for.
  $apiKey = $this->getSanitizer()->getString('apiKey');

  if ($apiKey == '')
    throw new \InvalidArgumentException(__('Missing API Key'));

  $this->module->settings['apiKey'] = $apiKey;
}

The form returned from settingsForm should be accessible in the viewPath you configured for you module during installation.

The Twig file should extend the default settings for as shown below:

{% extends "module-form-settings.twig" %}
{% import "forms.twig" as forms %}

{% block moduleFormFields %}
    {% set title %}{% trans "API Key" %}{% endset %}
    {% set helpText %}{% trans "Enter your API Key" %}{% endset %}
    {{ forms.input("apiKey", title, module.getSetting("apiKey"), helpText, "", "required") }}
{% endblock %}

It is not necessary to use trans blocks, but if you do it is possible that your string is already translated elsewhere in the software.

Layout Editor

The Layout Editor is where your Module code interacts with Xibo’s user interface. An icon for your widget will be shown in the toolbar and users can add that to a region.

The module’s code should implement methods to handle these interactions:

  • Default options
  • Property panel (view properties)
  • Edit
  • Preview
  • Validity

Default options

When a user adds your widget to a region or playlist, Xibo calls a final method on the ModuleWidget base class to handle adding a new widget to the database, linking it to the playlist, etc.

During this process the setDefaultWidgetOptions() method will be called, and will handle duration and proof of play statistics.

If you would like to set any default settings on a new Widget, you can override this method and handle that.

/** @inheritdoc */
public function setDefaultWidgetOptions()
{
    parent::setDefaultWidgetOptions();

    // Set a the scaleType option from our module settings.
    $this->setOption('scaleType', $this->getSetting('defaultScaleTypeId', 'center'));
}

Remember to call parent::setDefaultWidgetOptions(); so that duration/proof of play are handled.
Screenshot/Image

In the above sample we are setting the scaleType option on all new widgets to be the defaultScaleTypeId module setting.

Property panel

The property panel, sometimes called the edit form, appears in the right hand panel on the Layout Editor when your Widget is selected. It is used to configure the widget, selecting any options needed for that widget to render.

Each module should provide a Twig file for this edit form with the naming convention: <<module_type>>-form-edit.twig. This edit form should have a few key statements.

  • It should extend the form-base: {% extends "form-base.twig" %}
  • It can use the form helper: {% import "forms.twig" as forms %}
  • It should provide a title block: {% block formTitle %}Edit{% endblock %}
  • It should provide a HTML block: {% block formHtml %}...{% endblock %}
  • The HTML block should have bootstrap tabs
  • The HTML block should have a form pointing to the widget edit URL

We have an example form on Github which covers the above points and can be extended with additional fields and tabs as needed.

Designer JavaScript

You may find that you need to add some custom JavaScript to make your form more user friendly. This can be done by implementing the layoutDesignerJavaScript method:

/**
 * Template for Layout Designer JavaScript
 * @return string
 */
public function layoutDesignerJavaScript()
{
  return 'my-module-javascript';
}

The string returned should be the name of a TWIG file, example below. The contents of this file are included in the Layout Designer page for use on your form - in the below example you could call myModuleFormOpen on your Add/Edit form callBack to execute the code when the form opens.

<script type="text/javascript">
function myModuleFormOpen(dialog) {
  console.log("Opened!");
}
</script>

For all modules, edit from open event can be called with <moduleName>_form_edit_open() Which can be used without setting a callback in the edit form Twig for your custom Module. As an example, the Text Widget edit from open is text_form_edit_open().

You can use the designer-javascript file to add a custom icon to your Module as well, all that’s required is <style> tag and CSS class for your custom module .module-icon-<module_name> with the desired icon, for example:

<style>
    .module-icon-myModule:before {
        content: "\f031" !important;
    }
</style>

Validity

Xibo needs to know when your module thinks its valid and ready to go. It will call the isValid() method on your widget to do this. Oftentimes you will want your module to be invalid until it is fully configured, and you can check those conditions in the isValid method.

/** @inheritDoc */
public function isValid()
{
    if (empty($this->getOption('my_required_setting'))) {
    	return self::$STATUS_INVALID;
    } else {
    	return self::$STATUS_VALID;
    }
    // STATUS_PLAYER can be used if it is not possible for the CMS to say whether or not the widget is valid
    //  for example, it depends on some local player web service.
    // return self::$STATUS_PLAYER;
}

Preview

Once a widget is added and configured the Layout Editor will attempt to load a preview for that Widget. By default this will be an icon preview if previewEnabled = 0, and a “preview as player” rendering if previewEnabled = 1.

Previewing as a player means that Xibo will call the “rendering” code dealt with in the following section, putting the result inside an iframe sized according to the region your widget is inside.

Rendering

Widgets are rendered in the Layout Designer, Preview and through XMDS for display on the Player. Each Module must provide a getResource method which generates HTML to be returned in the above situations.

The core software provides a get-resource TWIG template which sets out a boilerplate HTML file that can then be filled in using the following methods:

/**
* Initialise getResource
* @return $this
*/
$this->initialiseGetResource();

/**
* @return bool Is Preview
*/
$this->isPreview();

/**
* Finalise getResource
* @param string $templateName an optional template name
* @return string the rendered template
*/
$this->finaliseGetResource($templateName = 'get-resource');

/**
* Append the view port width - usually the region width
* @param int $width
* @return $this
*/
$this->appendViewPortWidth($width);

/**
* Append CSS File
* @param string $uri The URI, according to whether this is a CMS preview or not
* @return $this
*/
$this->appendCssFile($uri);

/**
* Append CSS content
* @param string $css
* @return $this
*/
$this->appendCss($css);

/**
* Append JavaScript file
* @param string $uri
* @return $this
*/
$this->appendJavaScriptFile($uri);

/**
* Append JavaScript
* @param string $javasScript
* @return $this
*/
$this->appendJavaScript($javasScript);

/**
* Append Body
* @param string $body
* @return $this
*/
$this->appendBody($body);

/**
* Append Options
* @param array $options
* @return $this
*/
$this->appendOptions($options);

/**
* Append Items
* @param array $items
* @return $this
*/
$this->appendItems($items);

/**
 * Append raw string
 * @param string $key
 * @param string $item
 * @return $this
 */
$this->appendRaw($key, $item)

The two most important helper methods are initialiseGetResource and finaliseGetResource as these control the behaviour. Initialise sets up internal data tracking for the subsequent method calls, and finalise renders the template. isPreview() can be called to determine whether to render a CMS preview or a Player HTML file.

A simplified getResource might look like:

public function GetResource($displayId = 0)
{
    $this
        ->initialiseGetResource()
        ->appendViewPortWidth($this->region->width)
        ->appendJavaScriptFile('vendor/jquery-1.11.1.min.js')
        ->appendFontCss())
        ->appendBody('<h1>My HTML</h1>')
        ->appendJavaScript('$(document).ready(function() { $("h1").html("My Altered HTML"); } ');
        
    return $this->finaliseGetResource();
}

These helpers are optional, but if they are used once then the entire getResource must be served with them.

Please note it is entirely correct and feasible to render the HTML using your own methodology. Alternatively a Twig template can be rendered using $this->renderTemplate([], $templateName);, where the first parameter is an array of data to provide to the template and the second is the template name.

Complimentary Files

Similar to how you’ve added files in installFiles() you will also want to reference these files. Perhaps they are CSS or JavaScript, or perhaps images. Whatever the files are you can use a helper method to render them and the Module code will handle whether they are shown in the CMS or Player.

To reference installed module files use the method below:

$this->getResourceUrl('<<file name>>');

The file name should be the same as the file name used when you installed it.

Please note that files installed via this way are only used in the rendering of HTML for the Player/Preview - they are not for PHP files and won’t be loaded by PHP in any way.

Core HTML rendering

Xibo provides some helper JavaScript and CSS for rendering core aspects of the solution. Typical examples of this are the way we render text and the Layout scaler (which is a tool that keeps content at the correct scale regardless of the region changing size).

The key options for the Layout scaler are:

// Layout Scaler Options
$options = array(
  'originalWidth' => $this->region->width,
  'originalHeight' => $this->region->height,
  'previewWidth' => $this->getSanitizer()->getDouble('width', 0),
  'previewHeight' => $this->getSanitizer()->getDouble('height', 0),
  'scaleOverride' => $this->getSanitizer()->getDouble('scale_override', 0)
);

$this
  ->appendOptions($options)
  ->appendJavaScript('
$(document).ready(function() {
    $("body").xiboLayoutScaler(options);
});
');

Please note: preview width, height and scale override are “special” elements and will be replaced at design time with any real time updates to the region positioning.

Accessing Display information

It may be desirable to use information related to the Display in a Widget’s output. The recommended way to achieve this is using the Player Control library to retrieve that information at playtime, however this mechanism does have some limitations:

  • It is not currently possible to get tags (see improvement #2773).
  • It only works for HTML based widgets

In v4 the player control library will be the only supported mechanism of doing this, and will provide all tags/values for the display.
Screenshot/Image

An alternative is to inject that information on the CMS during the getResource() method call, for example:

public function getResource($displayId = 0)
{
    if ($displayId === 0) {
        // Pretend to be a display for the purposes of debugging.
        $display = $this->displayFactory->query(null, ['start' => 0, 'length' => 1])[0];
    } else {
        $display = $this->displayFactory->getById($displayId);
    }
    $display->load();

	// Build some info to use in our module.
    $info = [
        'displayId' => $displayId,
        'hardwareKey' => $display->license
    ];
    foreach ($display->displayGroups as $displayGroup) {
        if ($display->displayGroupId === $displayGroup->displayGroupId) {
            $displayGroup->load();
            foreach ($displayGroup->tags as $tag) {
                $info['tags'] = ['tag' => $tag->tag, 'value' => $tag->value];
            }
        }
    }
    return json_encode($info);
}

Because this gets generated CMS side, we need to make sure that we generate a cache per display

/** @inheritdoc */
public function getCacheKey($displayId)
{
    return $this->getWidgetId() . $displayId;
}

The CMS will cache this output for the default of 15 minutes. It won’t get updated when the data on the display changes, so you may wish to override the getCacheDuration().
Screenshot/Image

Downloading

As your module is rendered you may want to download data and/or resources for use when rendering the Widget. An example would be a JSON data source giving weather data, tweets, Facebook data, etc.

Whenever you interact with a 3rd party resource please take care to respect that resource and cache appropriately. Remember that your Widget might be used on a network with 1000s of Displays!

Data

Xibo includes the popular Guzzle HTTP client and is the recommended way to get third party data sources. Please refer to their documentation to learn how to use Guzzle.

Images

You may also want to download images to use with your module, for example with Media RSS or Facebook posts.

Xibo provides a MediaFactory to help with this - the MediaFactory will queue your downloads to happen concurrently, add them to the library and assign them to your Layout so that they are downloaded to the Player correctly.

An example of using the MediaFactory is below:

// Determine a number of seconds to keep this media before expiring
$expires = 86400;

$file = $this->mediaFactory->queueDownload(md5('http://example.com/myfile'), 'http://example.com/myfile', $expires);

// After queueing the download you can use the returned object to get an ID reference to the file. This is used to refer to it later.
$downloadUrlToUseLater = $this->getFileUrl($file, 'image');

// After you've queued all of your downloads
// Process queued downloads
$this->mediaFactory->processDownloads(function($media) {
  // Tag this layout with this file
  $this->assignMedia($media->mediaId);
});

The download URL provided by the getFileUrl is valid for the CMS Layout Designer, Preview and also for the Player.

If downloading resources in this way you must ensure that these images are included in your cache, or exist outside your cache. For example, if your cache key is not specific to your Layout then you would want to cache the mediaId of every assigned media.

Caching

Caching is an important part of ensuring your Widget runs well on one display through to 100s of displays. A widget which doesn’t take advantage of caching uses extra resources on the CMS and network which may be fine initially but will ultimately cause an issue as your network grows or as others use your widget.

There are two types of caching:

  • Widget
  • Resource

Widget Caching

Widget HTML output is cached to ensure the CMS isn’t doing more work than necessary and that the Players can download their Widget in a reasonable time frame. By default the Widgets HTML is cached on a per widgetId basis for 15 minutes and modified only when an option on the Widget is modified.

The following methods can be overridden to control the behaviour of the cache.

/**
 * Get the Modified Date of this Widget
 * @param int $displayId The displayId, or 0 for preview
 * @return Date the date this widgets was modified
 */
public function getModifiedDate($displayId);

/**
 * Get Cache Duration
 * @return int the number of seconds the widget should be cached.
 */
public function getCacheDuration();

/**
 * Get Cache Key
 * @param int $displayId The displayId we're requesting for, or 0 for preview
 * @return string
 */
public function getCacheKey($displayId);

/**
 * Is the Cache for this Module display specific, or can we assume that if we've calculated the
 * cache for 1 display, we've calculated for all.
 * this should reflect the cacheKey
 * @return bool
 */
public function isCacheDisplaySpecific();

getModifiedDate() should be implemented if the Module can be updated by a source external to the Module. For example, Tickers using a DataSet are updated when that DataSet gets updated and therefore should implement a getModifiedDate() which returns the DataSet modified date.

It is likely that getCacheDuration() should be modified to be the updateInterval of the Widget, or the cache duration setting of the Module if it has one.

getCacheKey() determines the granularity of the cache and defaults to the widgetId. As the Module developer you may know that your Module will generate different HTML according to the displayId, in which case you should include that in the cache key. Likewise you may know that it will always generate the same content for a particular resource - you can set the cache key accordingly.

isCacheDisplaySpecific() available in 1.8.12 onward should be set to true if the Module generates a separate cache record per Display, it defaults to false. It is used to improve the performance of Widget Caching when they are spread across multiple Displays. When set to true the cache validity has to be checked for each Display.

Resource Caching

Xibo uses a resource caching called Stash, accessible from a module using the $this->getPool() method. This is a PSR compliant caching interface. An example of using the cache is provided below:

/** @var \Stash\Item $cache */
$cache = $this->getPool()->getItem($this->makeCacheKey('some key for this widget'));
$cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);

$items = $cache->get();

// Check our cache to see if the key exists
// Ticker cache holds the entire rendered contents of the feed
if ($cache->isHit()) {
  return $items;
} else {
  // Lock this cache item (120 seconds)
  // Your module might be accessed concurrently!!
  $cache->lock(120);
  
  // Do something to get my items
  $items = [];
  
  $cache->set($items);
  $cache->expiresAfter(<some cache period>);
  $this->getPool()->saveDeferred($cache);
  
  return $items;
}

Whether you implement a resource cache will depend on whether your external data source can be used across multiple widgets and whether that can be handled by the getCacheKey() mechanism.

Concurrency

In a network consisting of many Displays it is conceivable that getResource will be called concurrently by 2 or more Players, or the preview. It is therefore important that we build getResource in a concurrent aware manner. By default a request is locked to the widgetId meaning each Widget can only have 1 request running at once. As the Module author you may decide that this is not granular enough, for example if your Widget downloads content from a 3rd party resource.

The concurrency lock key can be set by overriding the following method:

/**
 * Get the lock key for this widget.
 * should return the most unique lock key required to prevent concurrent access
 * normally the default is fine, unless the module fetches some external images
 * @return string
 */
public function getLockKey();

Summary

As you might have imagined, this is the “thin end of the wedge”, meaning there is a lot more you can do with Modules. Most of the complexity behind a module comes from the desired goal rather than the tools. For example you may need to parse some very difficult content, or have some difficult HTML to make.

It is intended that the tools above provide a basis for generating good quality HTML from getResource, that is compatible with the Player and making sure that any 3rd party content is cached.