Developers
Extensions
Everything a hub does beyond serving a request is an extension. The CMS itself is a thin application that boots a container, works out which component owns the URL, and asks a template to draw the result; the features — resources, groups, projects, publications, the wiki — are components, plugins, modules and templates sitting on top of it.
This section covers what all four kinds have in common: what a package must contain, how parameters are declared, how strings are translated, and how code gets onto a running hub. Each kind then has its own chapter set — Components, Plugins, Modules, Templates — for the parts that differ.
Read this section once before you start, and the four rules it establishes will save you the four days they otherwise cost: an extension is a row in a table, that row comes from a migration, every facade must be imported, and every visible string comes from a language file whose client you have to get right.
Which kind do you want
Decide by asking what owns the page.
| The thing you are adding | Kind | Because |
|---|---|---|
| A screen with records behind it — booking a lab's instruments | Component | it owns the URL and the request |
| A reaction to something happening — mirroring a publication to an external service when it is saved | Plugin | it owns no page and answers events |
| A block that appears beside other people's pages — the instruments free right now | Module | it renders into a template position |
| A different look for a partner institution | Template | it is the page around everything else |
Pick wrong and you fight the framework. A component that only ever renders a sidebar box competes for a URL it never uses; a plugin that wants a settings screen of its own is a component with the wrong base class.
The worked example carried through the component chapters — com_bookings,
which books a lab's instruments — is referred to here too, along with the
bookings plugin group it triggers.
The four kinds
| Kind | Lives in | Owns | Registered as |
|---|---|---|---|
| Component | components/com_{name} |
The page's main content, one per request | type = 'component' |
| Plugin | plugins/{group}/{name} |
A response to an event | type = 'plugin', folder = '{group}' |
| Module | modules/mod_{name} |
A block in a template position | type = 'module' |
| Template | templates/{name} |
The page around the component | type = 'template' |
Components
A component is an application in its own right: its own controllers, models,
database tables, routes, views and administrative interface. Exactly one
component handles each request — the one named by option in the URL, which
the router derives from the menu item — and it renders into the template's
main content area. A menu is, in effect, a switch between components.
A component is the only kind with three faces: site/ for hub visitors,
admin/ for the administrator interface, and api/ for the REST API. See
Components.
Plugins
A plugin answers events. It declares no routes and owns no page; instead its
public methods are named after events — onAfterRoute, onContentPrepare,
onGroupView — and the dispatcher calls them when something triggers one.
Plugins are grouped by the kind of thing they extend, and the group is the
directory: plugins/authentication/, plugins/content/,
plugins/members/. Most of the CMS's pluggable behaviour — the tabs on a
group page, the login methods, the cron jobs — is a plugin group. The
events reference lists what the tree
triggers. See Plugins.
Modules
A module renders a small block of HTML into a named position in the template: a login form, a breadcrumb trail, a list of recent entries. It never owns the request. The same module can be published in different positions on different templates and appear several times with different parameters. See Modules.
Templates
A template is the page around whatever the component produced — the markup,
the CSS, the positions modules render into, and the error and offline pages.
The templates that ship are in core/templates: kimera for the site,
kameleon for the administrator interface, plus system, lucent and
welcome. See Templates.
Where extensions live
Two trees hold the same shapes:
| Tree | What is in it |
|---|---|
core/ |
The extensions the release ships. Updated wholesale by an upgrade. |
app/ |
This hub's own extensions, and its overrides of core ones. Not in the repository. |
The loaders check app/ before core/ and use the first directory they
find, so a hub replaces a core extension by putting a directory of the same
name under app/. That is an all-or-nothing replacement: once
app/components/com_blog exists, nothing under core/components/com_blog
is used. To change a few files rather than a component, use a
template override instead.
The class loader,
Hubzero\Base\ClassLoader,
follows the same rule for classes: Components\Blog\Models\Entry is looked
for under app/components/com_blog first, then core/components/com_blog.
Composer's PSR-4 map covers only the Hubzero\ and Bootstrap\ namespaces;
every extension class comes through this loader.
How an extension is found
Nothing scans the filesystem. Every extension has a row in the
#__extensions table, and that row — not the directory — is what the
loaders read. The row also holds the extension's parameters, in its params
column.
What a missing row costs depends on the kind, and only one of the four fails loudly:
| Kind | With the directory but no row |
|---|---|
| Component | still runs. Hubzero\Component\Loader::load() manufactures a default record with enabled set to 1 when the query finds nothing. What is lost is everything stored against the row: the administrator's Components menu, the parameters, the permissions asset. |
| Plugin | never loads. The plugin loader selects rows WHERE type = 'plugin' AND enabled >= 1; no row, no listener, and the event fires into nothing. |
| Module | cannot be published. The row registers the module type; an administrator creates instances from it. |
| Template | cannot be assigned to the site or the administrator. |
A component that exists on disk and is invisible in the administrator interface is the single most common symptom of a missed migration, and it is confusing precisely because the component's own pages work.
An extension creates its own row from a
migration, by calling addComponentEntry(),
addPluginEntry(), addModuleEntry() or addTemplateEntry(). Nothing else
creates it: there is no package installer that reads a manifest and does it
for you. See Deploying extensions.
Extensions in the Foundation section covers each loader in detail; Requirements covers what else a package must carry.
In this section
- Requirements — what an extension package has to contain before the platform will load it.
- Parameters — declaring the settings an administrator edits, and the field types available.
- Languages — where an extension's strings go, and how they are named.
- Deploying extensions — installing code on a running hub.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.