Developers
Structure
Everything about a module hangs off its directory name. Pick the name once —
mod_upcoming_bookings — and the entry file, the manifest, the language
files, the CSS, the extension row, and the namespace all follow from it.
Getting one of them out of step is the usual reason a module that looks
complete renders nothing, and none of the mismatches raise an error.
Directory layout
app/modules/mod_upcoming_bookings/
assets/
css/mod_upcoming_bookings.css
img/
js/mod_upcoming_bookings.js
language/en-GB/
en-GB.mod_upcoming_bookings.ini
en-GB.mod_upcoming_bookings.sys.ini
migrations/
Migration20260101000000ModUpcomingBookings.php
tmpl/
default.php
index.html
composer.json
helper.php
index.html
mod_upcoming_bookings.php
mod_upcoming_bookings.xml
Only mod_upcoming_bookings.php and mod_upcoming_bookings.xml are required.
In practice a module that renders anything worth reading also has helper.php
and tmpl/default.php.
| Path | Purpose |
|---|---|
mod_upcoming_bookings.php |
The entry point. Included by the loader with $params and $module in scope. |
mod_upcoming_bookings.xml |
The manifest: name, client, files, and the parameter fields shown in the Module Manager. |
helper.php |
The module class, extending Hubzero\Module\Module. |
tmpl/ |
Layouts. default.php is used unless a layout parameter says otherwise. |
assets/ |
CSS, JavaScript, and images, split by type. |
language/ |
Translation files, one directory per language tag. |
migrations/ |
The registration migration and any schema changes. |
composer.json |
Package metadata for the installer. |
index.html |
An empty file in every directory, so a misconfigured web server cannot list it. |
Only the first three produce output. The manifest and the migration are what make the module reachable — see Migrations — so write them early rather than last, or you will have a module you cannot place.
Where the loader looks
Hubzero\Module\Loader::path()
resolves a module name to its entry file by trying four paths in order and
returning the first that exists:
app/modules/upcoming_bookings/upcoming_bookings.phpapp/modules/mod_upcoming_bookings/mod_upcoming_bookings.phpcore/modules/upcoming_bookings/upcoming_bookings.phpcore/modules/mod_upcoming_bookings/mod_upcoming_bookings.php
Two things follow from that order. First, app/ wins over core/, so a hub
overrides a shipped module by copying the whole directory into app/modules
and editing it there — nothing else has to change. Second, the unprefixed
directory form is accepted, but no core module uses it; stick to the prefixed
form so the directory, the extension element, and the asset paths all agree.
The name is normalised first by Loader::canonical(), which strips anything
outside [A-Za-z0-9_.-] and adds the mod_ prefix if it is missing. That is
why Module::name('login') and Module::name('mod_login') reach the same
module.
path() returns an empty string when none of the four exists, and render()
skips the include rather than complaining. So a module whose entry file is
misnamed — upcoming_bookings.php inside mod_upcoming_bookings/, a rename
that missed one file — renders nothing at all. The name in the
#__extensions row, the directory name, and the entry filename all have to be
the same string.
Namespaces and class names
The module class lives under the Modules namespace, in StudlyCase, with the
mod_ prefix dropped:
| Directory | Namespace |
|---|---|
mod_login |
Modules\Login |
mod_mygroups |
Modules\MyGroups |
mod_articles_archive |
Modules\ArticleArchive |
mod_upcoming_bookings |
Modules\UpcomingBookings |
Note the third row: the namespace tracks the directory loosely, not
mechanically. Nothing enforces it, because the entry file require_onces
helper.php and instantiates the class directly — no autoloader ever has to
map the class name back to a path. For the same reason the class itself is
conventionally, and freely, named Helper. Ninety-nine of the 100 shipped
modules use Helper, and so should yours: the loader finds the file, not the class, so a
different class name buys nothing and surprises the next reader.
Site and administrator modules
A module belongs to one client, declared as client="site" or
client="administrator" on the manifest's <extension> element and as the
$client argument (0 or 1) to addModuleEntry() in the migration. Both live in the same modules
directory; the client is a property of the registration, not of the path.
Loader::all() filters on m.client_id, so an administrator module is
invisible to the site and vice versa.
mod_upcoming_bookings is a site module, client="site" and $client = 0.
A lab manager's version of the same list, in the administrator's control
panel, is a second module with its own directory and its own row — one
module cannot serve both clients. If the two would share query code, put that
code on the Reservation model where both can call it.
Adding more
Nothing stops a module from carrying models, helpers, or a views directory,
and a few core modules do. But a module that has grown a controller and
several screens is a component wearing a disguise; see
Components. mod_upcoming_bookings stays small
precisely because everything it knows about a reservation lives in
com_bookings: the module queries a model and renders a list.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.