Developers

Controllers

Modules have no controllers. There is no controllers directory, no controller.php, no task dispatch, and no Hubzero\Module\Controller class in the framework. A module never handles a request of its own: it is rendered as a side effect of whatever page the component is already drawing.

What plays the part of a controller is a single unconditional script, the entry file, which the loader includes to produce the module's output.

The entry file

The file is named after the directory and sits at its top level — mod_login/mod_login.php. This is the whole of it:

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

namespace Modules\Login;

require_once __DIR__ . DS . 'helper.php';

with(new Helper($params, $module))->display();

Three lines of work: declare the namespace, pull in the class, construct it and call display(). with() is a global helper that returns the object it is given, so that a method can be called on a freshly constructed instance in one expression.

Write the same file for your module and change the namespace:

<?php

namespace Modules\UpcomingBookings;

require_once __DIR__ . DS . 'helper.php';

with(new Helper($params, $module))->display();

That stub-plus-helper.php pair is not one style among several. It is what Loader::render() is built around:

$path = $this->path($module->module);

if (file_exists($path))
{
	// … load the language file …
	ob_start();
	include $path;
	$module->content = ob_get_contents() . $content;
	ob_end_clean();
}

The loader includes a file. It never instantiates a class, never looks one up by name, and never calls a method on your module. Whatever your entry file echoes is the module's output; whatever it does not echo does not exist. All 100 shipped modules are written this way, and 99 of them put the work in a Helper class beside the stub. The exception, mod_multilangstatus, has no data to gather and constructs the base Hubzero\Module\Module directly so that display() requires its layout — which is the shortest a module can be and still have one.

What is in scope

Hubzero\Module\Loader::render() includes the entry file inside an output buffer, from inside a method, so the file inherits that method's local variables. Two of them matter:

Variable Type Contents
$module stdClass The row from #__modules for this instance: id, title, module, position, content, showtitle, params, menuid, plus name and style added by the loader.
$params Hubzero\Config\Registry The instance's parameters, parsed from $module->params and merged with any parameters passed by the caller.

Both are handed straight to the module class constructor, which stores them as $this->module and $this->params.

Anything the entry file echoes is captured. When the include returns, the loader assigns the buffer to $module->content and then runs the chrome function named by the render style, which is what actually emits the markup into the page.

Doing the work inline

The class in helper.php is a convention, not a requirement. A module with almost no logic can do everything in the entry file:

<?php

namespace Modules\UpcomingBookings;

use Lang;

defined('_HZEXEC_') or die();

echo '<p>' . Lang::txt('MOD_UPCOMING_BOOKINGS_NONE') . '</p>';

That works, and it is the wrong shape for anything you will keep. It gives up both things the module class provides: a template override point, because getLayoutPath() is a method on the class, and the asset helpers css(), js(), and img(). It also means a hub cannot restyle your module without editing your file. Use the class.

Guarding the file

Core module entry files are reached only through the loader, which resolves them from PATH_APP or PATH_CORE, so a direct HTTP request cannot execute them unless the whole modules tree is inside the document root. Layouts and helper files still carry the guard:

defined('_HZEXEC_') or die();

Add it to anything a misconfigured server might serve directly. See Constants for what _HZEXEC_ is and where it is defined.

What replaces a controller

If you find yourself wanting a controller — a module that responds to a form post, or that needs several tasks — the work belongs somewhere else. Post to a component and let it redirect back; the module then only renders the form. mod_login does exactly this: it renders a form whose action points at com_login, and holds no submission logic itself.

Apply that to the booking module and the line falls in an obvious place. A Cancel button beside each reservation is fine, as long as it is a link or a form posting to index.php?option=com_bookings&controller=reservations&task=cancel, which does the work and redirects. The module never cancels anything itself. If it did, the cancellation would run on every page of the hub that carries the module, and there would be no route to it that a menu, a permission check, or a redirect could see.

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