Developers

Packaging

A plugin ships as its own directory, laid out exactly as it will sit on disk. Two manifests describe it: {name}.xml, which the CMS reads for the display name and the parameter form, and composer.json, which describes the package for a distribution mechanism this release does not itself run.

Read the next section before you plan a release, because what a plugin "package" is here is probably not what you expect.

There is no package installer

The Install, Update, Discover and Database tabs that the older documentation names do not exist in this release; only orphan language strings remain. You cannot upload a .zip and have it unpack itself. See the extension manager for what the screen actually offers.

A plugin therefore arrives one of two ways:

  • The git-backed Custom Extensions flow. The Extension Manager clones a repository into app/ and updates it by fetching and merging. For a plugin, set the type to plugin, the alias to the plugin's own name — notify, not plg_bookings_notify — and Folder (Plugins Only) to the group, so the clone lands in app/plugins/bookings/notify. That folder value has to match the folder column your migration writes and the group prefix on the events you answer; see Group and directory must match.
  • By hand. Copy the directory into app/plugins/{group}/ yourself. This is what you do while writing the plugin.

Either way the plugin does nothing until its migration has run and written the #__extensions row — see Migrations — because Hubzero\Plugin\Loader builds its list from that table and never scans the filesystem. A hand copy does not run the migration; you do.

The git flow does run it, by shelling out to muse migration -d=up -f -i -r={the extension's directory} — after the first clone, and again after Merge Code has brought new commits into the working tree. So a plugin installed that way registers itself, and a later release that adds a migration applies it at the merge.

Full details of both routes are in Deploying extensions.

Package layout

plg_bookings_notify/
    language/en-GB/en-GB.plg_bookings_notify.ini
    language/en-GB/en-GB.plg_bookings_notify.sys.ini
    migrations/Migration20260210000000PlgBookingsNotify.php
    views/email/tmpl/message.php
    composer.json
    index.html
    notify.php
    notify.xml

The package directory is conventionally named plg_{group}_{name}, but what lands on disk is plugins/{group}/{name} — the group is not a directory inside the package, it is where the package is installed to. A repository used with the Custom Extensions flow holds the contents of that directory at its top level, with notify.xml beside notify.php, not a plg_bookings_notify wrapper directory.

The XML manifest

The XML manifest is the one the CMS reads. It is named after the plugin — notify.php is described by notify.xml — and it is not listed in its own <files> block.

<?xml version="1.0" encoding="utf-8"?>
<extension version="2.5" type="plugin" group="content">
	<name>plg_content_formatwiki</name>
	<creationDate>October 2013</creationDate>
	<author>HUBzero</author>
	<authorUrl>hubzero.org</authorUrl>
	<authorEmail>support@hubzero.org</authorEmail>
	<copyright>Copyright (c) 2005-2020 The Regents of the University of California.</copyright>
	<license>http://opensource.org/licenses/MIT MIT</license>
	<version>2.5.0</version>
	<description>PLG_CONTENT_FORMATWIKI_XML_DESCRIPTION</description>
	<files>
		<filename plugin="formatwiki">formatwiki.php</filename>
		<filename>index.html</filename>
	</files>
	<languages>
		<language tag="en-GB">en-GB.plg_content_formatwiki.ini</language>
		<language tag="en-GB">en-GB.plg_content_formatwiki.sys.ini</language>
	</languages>
Element or attribute Meaning
type="plugin" Required.
group="content" Required. The directory the plugin installs into, and the folder column of its extension row.
version The manifest format version, not the plugin's.
<name> Shown in the Plugin Manager. Either a language key, as here, or a readable string like Bookings - Notify.
<description> A language key, resolved from the .sys.ini.
<files> Files to install. The plugin attribute on one <filename> marks the entry point, and its value must be the plugin name.
<languages> Translation files to copy into place.
<config> The parameter form. See Configuration.

Subdirectories are declared with <folder>:

<files>
	<filename plugin="notify">notify.php</filename>
	<filename>index.html</filename>
	<folder>views</folder>
	<folder>language</folder>
</files>

Naming the plugin for humans

<name> is what an administrator sees in a list of dozens of plugins, so the convention is Group - Name: Bookings - Notify, Groups - Forum, Members - Blog. Using a language key instead, as plg_content_formatwiki does, gets the same string out of the .sys.ini and makes it translatable. Prefer the key.

The Composer manifest

{
	"name": "myorg/plg_bookings_notify",
	"description": "Emails the lab manager when an instrument is reserved",
	"type": "hubzero-plugin",
	"keywords": ["hubzero"],
	"homepage": "https://example.org",
	"license": "MIT",
	"require": {
		"php": "^5.4",
		"hubzero/com_bookings": "dev-master"
	},
	"extra": {
		"install-directory": "/plugins/bookings/notify/"
	}
}

The type must be hubzero-plugin; the recognised types are hubzero-component, hubzero-module, hubzero-plugin, and hubzero-template.

A plugin that extends a component should still declare the dependency, so that a reader can see it. plg_members_blog requires both hubzero/com_members and hubzero/com_blog, because it renders a com_blog archive on a com_members profile page; plg_bookings_notify requires com_bookings, because without the component nothing ever triggers its event.

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