Developers
Components
A super group can carry components of its own: MVC extensions that live in the group's directory, answer at a URL under the group, and never appear in the hub's extension manager.
When you want one
The Coastal Resilience Center starts with a PHP page for
its tide-gauge status board, and that is the right size for one page. It
outgrows it the moment the centre wants a page per gauge, at
/groups/coastal/gauges/pier-7, with a list, a detail view, and a form for
the technician who corrects a bad reading. That is a controller, a view per
screen, a model and a router: a component.
| The group needs | Build |
|---|---|
| One page, no URLs below it | a PHP page |
| A snippet inside editor-written prose | a macro |
| Several screens, records, and its own URL space, for this group | a super group component |
| The same thing for the whole hub | an ordinary component |
The last row is the one to think hardest about. A super group component is
invisible to the hub: no entry in the extension manager, no access.xml, no
configuration screen, no administrator face, and nothing else on the hub can
link to it by option name. If a second group would want it, write a real
component and let both groups link to it.
Turning them on
Super group components are off by default. An administrator switches them on once for the whole hub:
- Go to Users → Groups and select Options.
- On the Super Groups tab, set Super Group Components to yes.
- Select Save & Close.
With the option off, the group's components/ directory is never looked at
and requests fall through to PHP pages and group pages instead — silently, so
a component that "does not exist" on a hub where it worked yesterday is worth
checking here first. The option is super_components in
config/config.xml;
see the generated reference.
The smallest thing that works
Two files, no router, no model:
app/site/groups/1051/components/com_gauges/gauges.php
<?php
echo '<h2>Tide gauges</h2>';
That answers at /groups/coastal/gauges. Add the router and the controllers
once it renders.
Structure
A super group component is the site half of a CMS component, flattened:
app/site/groups/1051/components/com_gauges/
├── gauges.php entry file, required
├── router.php optional
├── controllers/
│ └── readings.php
├── models/
│ └── reading.php
├── helpers/
├── views/
│ └── readings/
│ └── tmpl/
│ └── display.php
├── language/
│ └── en-GB/
│ └── en-GB.com_gauges.ini
└── assets/
├── css/
└── js/
Two names are fixed: the directory is com_<name> and the entry file inside
it is <name>.php. Everything else is convention that the framework's own
defaults happen to follow.
There is no admin, no api and no site subdirectory. A super group
component is only ever dispatched on the site, so the directories a full
component keeps under site/ sit at the top here. Writing controllers,
views, models and routes is otherwise the same job as in a
CMS component.
How it is reached
The second segment of the group URL becomes the component name:
/groups/coastal/gauges -> components/com_gauges/gauges.php
Components\Groups\Helpers\View::superGroupComponents()
checks for the directory and the entry file, and gives up quietly if either is
missing. It runs before PHP pages and before group pages, so a component
shadows both.
The same collisions apply as for PHP pages:
a name that matches an enabled group plugin, or one of the segments the group
router claims for itself, never reaches the component. So com_files,
com_members, com_wiki and com_media are all names a group cannot use:
the segment is taken before the component is looked for.
What the entry file gets
The file is included and everything it prints becomes the component's
output. Before it runs, the hub defines:
JPATH_GROUPCOMPONENT // /path/to/app/site/groups/1051/components/com_gauges
$group and $tab are in scope, holding the Hubzero\User\Group and the
active tab name.
Class autoloading does not reach into a group directory — the autoloader
maps Components\Gauges\* to core/components/com_gauges and
app/components/com_gauges, neither of which exists. The entry file has to
require what it needs:
<?php
require_once JPATH_GROUPCOMPONENT . DS . 'controllers' . DS . 'readings.php';
$controller = new \Components\Gauges\Controllers\Readings();
$controller->execute();
Forget that require_once and you get Class … not found on a white page,
with the group's own template gone as well, because the fatal happens while
the group is still assembling its content. It is the first thing to check when
a component that worked in one place fails in another.
That constant is also what several parts of the framework key off:
| Behaviour | Where |
|---|---|
Assets::addComponentStylesheet() and addComponentScript() resolve to the component's own assets/css and assets/js |
Hubzero\Document\Assets |
The css() and js() view helpers do the same |
Hubzero\View\Helper\Css |
A model extending Hubzero\Base\Model whose file sits under the component directory gets the group's database instead of the hub's |
Hubzero\Base\Model::initDbo() |
A controller extending
Hubzero\Component\SiteController
needs no configuration: it takes its base path from its own file, two levels
up, which is the component directory. Views therefore resolve to
views/<name>/tmpl, helpers to helpers/, and the component's language file
is loaded from language/en-GB/en-GB.com_<name>.ini inside the component.
Move a controller one directory deeper and every one of those resolves one
level wrong, which shows as Layout "display" not found.
The component's output is then wrapped by
site/views/pages/tmpl/_view_component.php:
<div class="group-component">
<?php echo $this->content; ?>
</div>
Put a _view_component.php in the group's template/ directory to replace
that wrapper.
Routing
URLs built inside a super group component come back prefixed with
/groups/<alias>/<component>/ automatically. Do not add the prefix yourself.
Both halves are done by
core/plugins/system/supergroup:
- Building. A rule appended to the router turns
Route::url('index.php?option=com_gauges&…')into/groups/coastal/gauges/<segments>, where the segments come from the component'srouter.php. It only fires while the current request is already inside/groups/…and the option names a directory the group actually has. - Parsing. Before the component runs, the remainder of the path — what is
left after
groups/<alias>/<component>— is handed to the same router and every variable it returns is set on the request.
That plugin is enabled on a stock install. If every link in your component
comes back as index.php?option=com_gauges&…, check that it still is before
you look at your router.php.
router.php may provide either a class or a pair of functions:
| Form | Parse | Build |
|---|---|---|
Components\<Name>\Site\Router |
parse($segments) |
build($query) |
Components\<Name>\Router |
parse($segments) |
build($query) |
| Plain functions | <Name>ParseRoute($segments) |
<Name>BuildRoute($query) |
A component with no router.php still works; it just has no path segments of
its own beyond the component name.
Query strings
Query string parameters reaching a super group component are moved out of the
way and put back: the group router copies every parameter to an sg_-prefixed
name during parsing, and the system plugin copies them back to their plain
names immediately before the component is included. This keeps a component's
?task=, ?id= and the like from being read as instructions to com_groups.
It happens automatically, and a component reads its parameters through
Request as usual.
Creating one
muse group scaffolding component is documented in older material. On this
release it does not work for a group: scaffolding resolves its install
directory against core/ rather than app/, so the files land in
core/site/groups/<id>/components/, and the template it writes is a full CMS
component with admin/ and site/ directories rather than the flat layout a
group needs. The @FIXME on the path is in the source. Both problems are
recorded in the review findings.
Build the directory by hand, or copy the site/ directory of an existing
component up one level and adjust. It is a small amount of typing compared
with what the component itself will take.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.