Developers
Helpers
The helper class is where a module does its work. It gathers whatever data the
layout needs, exposes it as properties, and requires the layout. By convention
it lives in helper.php beside the entry file and is named Helper, and it
extends Hubzero\Module\Module.
It exists for a reason that has nothing to do with tidiness. The loader
includes the entry file once per instance, so anything declared at that
file's top level is declared twice the second time the module appears on a
page. Putting the class behind a require_once in a separate file is what
makes a module safe to place more than once. See
Controllers.
The base class
Hubzero\Module\Module extends Hubzero\Base\Obj and mixes in two traits,
AssetAware and Escapable. It is small enough to hold in your head:
| Member | What it is |
|---|---|
$params |
Hubzero\Config\Registry of the instance's parameters. |
$module |
The #__modules row for this instance. |
__construct($params, $module) |
Stores both. Takes them positionally, in that order. |
display() |
Requires the layout named by the layout parameter, defaulting to default. |
getLayoutPath($layout = 'default') |
Resolves a layout name to a file path, honouring template overrides. |
getCacheContent() |
Returns cached output for this instance, generating it by calling run() if the cache is cold. |
css(), js(), img() |
From AssetAware. See Assets. |
escape($var), setEscape($spec) |
From Escapable. Escapes a value for output; htmlspecialchars by default. |
Because it extends Obj, the class also carries the error bag: setError(),
getError(), getErrors().
The smallest one that works
The whole of mod_upcoming_bookings/helper.php:
<?php
namespace Modules\UpcomingBookings;
use Hubzero\Module\Module;
use Components\Bookings\Models\Reservation;
use Date;
use User;
defined('_HZEXEC_') or die();
class Helper extends Module
{
public function display()
{
$query = Reservation::all()
->whereEquals('created_by', User::get('id'))
->whereEquals('state', 1)
->where('starts', '>=', Date::of('now')->toSql());
if ($instrument = intval($this->params->get('instrument_id', 0)))
{
$query->whereEquals('instrument_id', $instrument);
}
$this->reservations = $query
->order('starts', 'asc')
->limit(intval($this->params->get('limit', 5)))
->rows();
require $this->getLayoutPath();
}
}
Properties set on $this before the require are visible to the layout,
because the layout is included in the method's scope. There is no separate
view object and no assign() step.
Components\Bookings\Models\Reservation needs no require: the class loader
maps Components\ namespaces to component directories on its own. Several
shipped modules still write include_once Component::path('com_groups') . DS . 'models' . DS . 'recent.php'; before naming the class. That is the older
convention, from before the class loader covered extension namespaces; it is
harmless but do not copy it. See
Autoloading.
Choosing a layout
display() on the base class requires the layout named by the layout
parameter. If you override display(), you choose the layout yourself.
mod_mygroups switches on a parameter:
$layout = 'default';
if (!$this->params->get('show_recent', 1))
{
$layout = 'simple';
}
require $this->getLayoutPath($layout);
Parameters
$this->params is a Registry built from the instance's params column, so
every field declared in the manifest's <config> block is readable by name,
with a fallback for the case where an administrator never saved the form:
$this->limit = intval($this->params->get('limit', 100));
Parameters are what let one module serve two placements. mod_upcoming_bookings
declares limit and instrument_id, so the same code renders "your next five
bookings, anywhere" in the sidebar and "your next booking on the confocal" on
the instrument's own page, without a second module.
Caching
Cache a module when the query behind it is expensive and its output is the
same for everybody. getCacheContent() gives an opt-in output cache keyed on
the instance id. It returns an empty string — meaning "no cached content, do
the work" — unless a cache store is configured, the instance's cache
parameter is on, cache_time is non-zero, and the application is not in debug
mode. On a miss it buffers a call to run(), appends an HTML comment
recording when the copy was made, stores it, and returns it.
That contract splits the class in two: run() produces the output, and
display() decides whether to reuse a stored copy. mod_findresources is the
pattern in full:
/**
* Module class for displaying ways to find resources
*/
class Helper extends Module
{
/**
* Generate module contents
*
* @return void
*/
public function run()
{
require_once Component::path('com_tags') . '/models/cloud.php';
require_once Component::path('com_resources') . '/models/type.php';
$this->tags = Tag::all()
->whereEquals('admin', 0)
->limit((int)$this->params->get('limit', 25))
->order('objects', 'desc')
->rows();
// Get major types
$this->categories = \Components\Resources\Models\Type::getMajorTypes();
require $this->getLayoutPath();
}
/**
* Display module contents
*
* @return void
*/
public function display()
{
if ($content = $this->getCacheContent())
{
echo $content;
return;
}
$this->run();
}
}
cache_time is stored in seconds by the Module Manager but the cache store
takes minutes, so getCacheContent() divides values above 120 by 60 and
passes smaller values through unchanged. A cache_time of 900 caches for
fifteen minutes; a cache_time of 30 caches for thirty minutes, not thirty
seconds.
More than one class
Nothing limits a module to one class. Put additional classes in their own
files under the module directory and require_once them from helper.php,
keeping them in the module's namespace so they cannot collide with another
extension's.
require_once them by path, not by name. The class loader resolves
Modules\… by collapsing the namespace segment to one word, so a class in
mod_upcoming_bookings is looked for under mod_upcomingbookings and never
found — see Structure and
Autoloading.
Before you add the second class, ask whether it belongs in the component. A
"does this reservation clash" method is the Reservation model's, and putting
it in the module hides it from the administrator screens that need the same
rule.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.