Developers

Packaging

There is no build step and no package format. A distributable component is the directory exactly as it appears once installed, plus two descriptive files at its root.

com_bookings/
    composer.json
    bookings.xml
    config/
        config.xml
        access.xml
    migrations/
        Migration20260901000000ComBookings.php
    models/
        instrument.php
        reservation.php
    site/
        bookings.php
        router.php
        controllers/instruments.php
        views/instruments/tmpl/display.php
        assets/css/bookings.css
        language/en-GB/en-GB.com_bookings.ini
    admin/
        bookings.php
        controllers/instruments.php
        views/instruments/tmpl/display.php
        views/instruments/tmpl/edit.php
        language/en-GB/en-GB.com_bookings.ini
        language/en-GB/en-GB.com_bookings.sys.ini

Copy that directory into app/components/ and you have something installed.

The practical consequence: ship the component as a git repository, laid out so that the repository root is the component directory. That is what the supported route consumes, and it is how a hub gets updates afterwards.

composer.json

The package definition names the component and declares its type:

{
	"name": "hubzero/com_kb",
	"description": "Knowledge Base article manager component for the HUBzero CMS",
	"type": "hubzero-component",
	"keywords": ["hubzero"],
	"homepage": "https://hubzero.org",
	"license": "MIT",
	"authors": [
		{
			"name": "HUBzero",
			"email": "support@hubzero.org"
		}
	],
	"support": {
		"email": "support@hubzero.org",
		"issues": "https://help.hubzero.org"
	},
	"require": {
		"php" : "^5.4"
	}
}

"type": "hubzero-component" distinguishes it from a library package. The require block is where a component states what it needs — a PHP version, or a third-party library it will use.

Most shipped composer.json files still declare "php": ">=5.4" or similar. Those constraints are stale and nothing enforces them. Hubzero 2.4 needs PHP 8.2; write that.

The manifest

{componentname}.xml carries the metadata the administrator displays: name, author, copyright, licence, description, and version. Its root element is <extension type="component">.

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="1.5">
    <name>COM_BOOKINGS</name>
    <author>Your Organisation</author>
    <authorUrl>example.org</authorUrl>
    <authorEmail>support@example.org</authorEmail>
    <version>1.0.0</version>
    <copyright>Copyright (c) 2026 Your Organisation</copyright>
    <license>http://opensource.org/licenses/MIT MIT</license>
    <description>COM_BOOKINGS_XML_DESCRIPTION</description>
    <administration>
        <menu>COM_BOOKINGS</menu>
    </administration>
</extension>

<name> and <description> may be language keys, resolved from the administrator .sys.ini file. Write keys unless the component is for one hub in one language.

The manifest is metadata. It installs nothing, creates nothing and is read by nothing at request time; a component with no manifest at all still runs. It exists so the administrator's lists have something to show.

Older manifests, com_kb's included, also carry <files>, <languages>, <install><sql>, and <params> blocks. They are inherited from when a package installer read them. None of them do anything now, and most are stale — com_kb's <files> list names files that were deleted years ago. Do not read an existing <files> block as a description of the component, and do not copy one into a new component:

Old block What to use instead
<files> nothing; the directory is the manifest of itself
<install><sql> a migration
<params> config/config.xml
<languages> nothing; language files are found by path

What actually installs it

Nothing in the package creates database tables or the #__extensions row. That is the job of the component's migrations, run after the files are in place:

php core/bin/muse migration -f -e=com_bookings

Ship the migrations with the component and installation is one command that works the same on every hub, is idempotent, and can be rolled back. Leave them out and the component is invisible to the administrator on every hub it reaches. See Migrations.

Where it goes

app/components/com_bookings/. Never core/components/ — that directory is the platform's, and an upgrade will overwrite it. The class loader and Component::path() both check app before core, so a component in app takes precedence over a core component of the same name, and everything a hub adds survives an upgrade untouched.

The full deployment procedure, including the by-hand route and what to do when the Extension manager cannot write to the filesystem, is in Deploying extensions.

Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.