Developers
Migrations
A super group's database is versioned the same way the hub's is: with migration classes that muse runs in order and records so it never runs one twice. Putting schema changes in migrations means nobody has to connect to the live database by hand, and means the schema travels with the code — the admin screen that pulls a group's code runs its migrations straight afterwards.
The same runner, pointed somewhere else
There is no separate group migration system. muse group migrate reaches
Hubzero\Content\Migration,
the same class muse migration runs against the hub, with two of its
constructor arguments filled in:
| Argument | For the hub | For a group |
|---|---|---|
$docroot |
omitted, so the runner walks core/ and app/ and every extension under them |
the group's directory, which replaces the search path entirely |
$runDb |
omitted, so migrations run against the hub connection | the group's connection, from config/db.php |
Everything else — the file naming, the class shape, up() and down(), the
dry run, the #__migrations log — is the hub's machinery unchanged. Read
Migrations first; this chapter is only what
the two substituted arguments change.
Both consequences follow from that table and are worth holding on to: only one directory is searched, and the log row still lands in the hub's table even though the schema change lands in the group's.
Where they live
app/site/groups/<gidNumber>/migrations/
That one directory, and nothing under it. Because the group directory becomes
the entire search path, migrations inside the group's own
components/com_*/migrations are not found — the runner looks for
<search path>/migrations and stops. Keep them all in the group's top-level
migrations directory, and put the component name in the class name to tell
them apart.
The skeleton creates the directory when the group is first saved as a super group.
The class
Naming is as it is everywhere else: Migration, a fourteen-digit timestamp,
and the extension in studly case.
Migration20260901120000ComGauges.php
<?php
use Hubzero\Content\Migration\Base;
// No direct access
defined('_HZEXEC_') or die();
/**
* Migration script for the tide gauge table
**/
class Migration20260901120000ComGauges extends Base
{
/**
* Up
**/
public function up()
{
if (!$this->db->tableExists('#__gauges'))
{
$this->db->setQuery("CREATE TABLE `#__gauges` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;");
$this->db->query();
}
}
/**
* Down
**/
public function down()
{
if ($this->db->tableExists('#__gauges'))
{
$this->db->setQuery("DROP TABLE `#__gauges`");
$this->db->query();
}
}
}
$this->db inside the migration is the group's database, because muse
passes it as the alternate connection. #__ expands to nothing there, since
the group's prefix is empty.
There is no supported way back to the hub's connection from inside a group
migration: Base keeps it in a private property and never offers it. If a
group migration genuinely has to touch the hub schema, ask for it explicitly
with \App::get('db') and be sure that is what you meant.
Creating one
muse group scaffolding migration is documented in older material and does
not put the file in the right place on this release — it resolves its
install directory against core/, not app/. Give scaffolding an absolute
directory instead:
php core/bin/muse scaffolding create migration \
-e=com_gauges \
--install-dir=/path/to/hub/app/site/groups/1051
That writes app/site/groups/1051/migrations/Migration<timestamp>ComGauges.php
and opens it in $EDITOR. -e only decides the suffix on the class name;
add -i if the extension does not exist as a directory under the group and
scaffolding refuses it.
Writing the file by hand is equally valid. Nothing but the file name pattern and the class name matters.
Running them
php core/bin/muse group migrate --group=coastal -if
--grouptakes the group's alias. Run muse from inside the group's directory and you may leave it off; the command works the group out from the current directory.-iignores dates, so migrations the group may have missed are offered.-fmakes it a real run. Without-feverything is a dry run that only lists what it would do — which is the safe way to look first.
php core/bin/muse migration -f --group=coastal is the same thing:
group migrate validates the group, sets the group option and hands over to
the migration command, which is where the work happens.
If the group has no migrations directory the command stops with Error:
Migrations directory does not exist, and if config/db.php is missing or
wrong it stops with Error: Could not connect to Group Database. Both are
loud, and both are much better than the silent failure the previous warning
describes, so a dry run that reports nothing to migrate on a directory you
know has files means the class is namespaced or misnamed.
Where the runs are recorded
In the hub's #__migrations table, not the group's. The scope column
holds the path to the directory relative to the document root —
app/site/groups/1051/migrations — which is what keeps one group's history
separate from another's and from the hub's own.
So a group's database can be dropped and rebuilt, but the hub still believes
its migrations have run. Use --force with --file= to re-run one, or
-d=down first.
Running them automatically
You only run migrations by hand in a development environment. On a hub whose
groups are managed through GitLab, an
administrator selecting Merge Groups Code runs, for each group,
muse group update -f followed by muse group migrate -f. New migrations
that arrive with a merge are applied as part of the merge.
Rewritten and checked against 2.4-main @ 91d03d0a23 on 2026-09-10.