Developers

Views

A module's markup belongs in a layout file under tmpl/, separate from the class that gathers the data. Keeping them apart is not only tidiness: a layout in tmpl/ can be overridden by a template, and markup echoed from the class cannot.

That override point is the whole argument. A module ends up in the sidebar of a hub whose designer you will never meet, and the only way they can make your booking list match the rest of the page — without forking the module and inheriting your bugs — is to drop a file into their template. Echo the markup from helper.php and you have taken that away.

The smallest one

mod_upcoming_bookings/tmpl/default.php, in full:

<?php
defined('_HZEXEC_') or die();

$this->css();
?>
<h3><?php echo Lang::txt('MOD_UPCOMING_BOOKINGS_HEADING'); ?></h3>
<?php if (!count($this->reservations)) : ?>
	<p><?php echo Lang::txt('MOD_UPCOMING_BOOKINGS_NONE'); ?></p>
<?php else : ?>
	<ul id="upcoming-bookings-<?php echo $this->module->id; ?>">
		<?php foreach ($this->reservations as $reservation) : ?>
			<li><?php echo Lang::txt(
				'MOD_UPCOMING_BOOKINGS_SLOT',
				$this->escape($reservation->get('starts')),
				$this->escape($reservation->get('ends'))
			); ?></li>
		<?php endforeach; ?>
	</ul>
<?php endif; ?>

Everything the class set is on $this. There is no view object between them.

Where layouts live

core/modules/mod_mygroups/
    tmpl/
        default.php
        simple.php
        _item.php
        index.html

default.php is the layout used when nothing says otherwise. Other names are alternate layouts, selected either by the class or by the instance's layout parameter. A leading underscore is a convention for a partial meant to be required from another layout, not chosen directly.

How a layout is found

getLayoutPath($layout) on the module class delegates to Hubzero\Module\Loader::getLayoutPath(), which returns the first of three paths:

  1. {templates}/{template}/html/mod_upcoming_bookings/{layout}.php — the active template's override.
  2. {module directory}/tmpl/{layout}.php — the module's own layout.
  3. {module directory}/tmpl/default.php — the fallback.

Only the first is checked for existence against the template; if neither the override nor the named layout exists, the third path is returned whether or not the file is there, so a typo in a layout name silently renders default.

A layout name may be qualified with a template: getLayoutPath('beez:list') looks for list.php under the beez template rather than the active one, and _ as the template name means "the active template". Core modules do not use this form.

Choosing the layout

The base display() reads the instance parameter:

require $this->getLayoutPath($this->params->get('layout', 'default'));

A class that overrides display() chooses for itself, as mod_mygroups does when its show_recent parameter is off:

$layout = 'default';
if (!$this->params->get('show_recent', 1))
{
    $layout = 'simple';
}

require $this->getLayoutPath($layout);

Writing one

The layout is required from inside a method of the module class, so $this is the module object. Every property the class set is available, along with $this->params, $this->module, $this->escape(), and the asset helpers:

<?php
/**
 * @package    hubzero-cms
 * @copyright  Copyright (c) 2005-2020 The Regents of the University of California.
 * @license    http://opensource.org/licenses/MIT MIT
 */

// no direct access
defined('_HZEXEC_') or die();

// Push the module CSS to the template
$this->css()
     ->js();
?>
<div<?php echo ($this->moduleclass) ? ' class="' . $this->moduleclass . '"' : '';?>>

Two things to note. The layout has no namespace declaration, so it runs in the global namespace and can call Lang::txt(), Route::url(), and User without importing anything — unlike helper.php, which is namespaced and must import each facade it uses. And $this->module->id is used to build element ids: two instances of the same module on one page would otherwise collide.

Escaping

Anything that came from the database or the request goes through $this->escape():

<h3><?php echo $this->escape($this->module->title); ?></h3>

Content that is deliberately HTML — a mod_custom body, output already through the content plugins — is echoed directly.

Partials

A layout can require another layout from the same module through the same resolver, which keeps the override point intact. mod_mygroups renders each row this way:

foreach ($this->recentgroups as $group)
{
    if ($group->published)
    {
        $status = $this->getStatus($group);

        require $this->getLayoutPath('_item');
    }
}

$group and $status are locals of the layout, and the partial sees them because require shares the enclosing scope. That is convenient and fragile in equal measure: rename the loop variable and the partial breaks with no warning beyond an undefined-variable notice.

No view class

There is no view object in a module and no loadTemplate(). The class is the view context, the layout is the template, and the output goes to the buffer that Hubzero\Module\Loader::render() opened around the entry file. Plugins work differently — see Plugin views.

Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.