Developers

Controllers

A plugin has one class, in the file named after it, extending Hubzero\Plugin\Plugin. It is called a controller here only by analogy with a component: it has no tasks, no routing, and nothing it can decide to render. Its public methods are its entire interface. Each one is registered under its own name as an event listener and is called when an event of that name fires.

That is a smaller surface than a component controller, and the work of writing a plugin is mostly deciding which events to answer and keeping everything else out of the public interface.

The smallest working thing

plg_bookings_notify answers one event and returns nothing:

<?php
// No direct access
defined('_HZEXEC_') or die();

class plgBookingsNotify extends \Hubzero\Plugin\Plugin
{
	/**
	 * Email the lab manager when a reservation is created.
	 *
	 * @param   object  $reservation  Components\Bookings\Models\Reservation
	 * @return  void
	 */
	public function onReservationCreate($reservation)
	{
		if (!($to = $this->params->get('manager_email')))
		{
			return;
		}

		$body = $this->view('message', 'email')
			->set('reservation', $reservation)
			->loadTemplate();

		$message = new \Hubzero\Mail\Message();
		$message->setSubject(Lang::txt('PLG_BOOKINGS_NOTIFY_SUBJECT', $reservation->instrument->get('title')))
		        ->addFrom(Config::get('mailfrom'), Config::get('sitename'))
		        ->addTo($to)
		        ->addHeader('X-Component', 'com_bookings')
		        ->addPart($body, 'text/plain');

		if (!$message->send())
		{
			$this->setError(Lang::txt('PLG_BOOKINGS_NOTIFY_SEND_FAILED', $to));
		}
	}
}

Nothing registers onReservationCreate anywhere else. The method name is the registration, and com_bookings triggering bookings.onReservationCreate is what calls it.

A real one

plgContentFormatwiki is the same shape with two methods:

// No direct access
defined('_HZEXEC_') or die;

/**
 * Wiki formatted Content Plugin
 */
class plgContentFormatwiki extends \Hubzero\Plugin\Plugin
{

It answers onContentBeforeSave and onContentPrepare, and keeps its two helpers, _isWiki() and _key(), private — which is not a style preference. See the warning below.

What the base class gives you

Member What it is
$params Hubzero\Config\Registry of the plugin's parameters, built in the constructor.
$event The Hubzero\Events\Event currently being dispatched to this plugin.
$option com_ plus the group name. Legacy, and wrong for any group that is not a component; do not rely on it.
$_name, $_type The plugin's element and group, both protected.
$_autoloadLanguage Set true to load the language file in the constructor.
loadLanguage($extension = '', $basePath = PATH_APP) Loads a language file. See Languages.
view($layout = 'default', $name = '') Returns a Hubzero\Plugin\View. See Views.
css(), js(), img() Asset helpers. See Assets.
Plugin::getParams($name, $folder) Static; reads one plugin's params straight from the database.

Because Plugin extends Hubzero\Base\Obj, which uses the ErrorBag trait, setError(), getError(), and getErrors() are available too, and plugins use them to hand a message back to the component that triggered them. The component has to ask: an error set in a plugin goes nowhere on its own.

The constructor

You rarely need one. If you write one, call the parent — it is what populates $params, $_name, and $_type, and what loads the language file:

public function __construct(&$subject, $config)
{
	parent::__construct($subject, $config);

	// Extra initialisation
}

$subject is the dispatcher the plugin was created with, retained only for backward compatibility. $config is the plugin's #__extensions row cast to an array, so $config['name'], $config['type'], and $config['params'] are what the parent reads.

The constructor runs when the group is imported, whether or not any of the plugin's events ever fire. Keep it cheap: no queries, no HTTP. A system plugin's constructor runs on every request on the hub.

How methods become listeners

Hubzero\Events\Dispatcher::addListener() calls get_class_methods() on the plugin instance and registers it once per public method name. There is no list of events to declare and no annotation to write — the method name is the registration.

The failure in the other direction is quieter still. A handler that is private, or misspelled — onReservationCreated for bookings.onReservationCreate — is simply not registered. The event fires, trigger() returns an empty array, and nothing is logged. When a plugin does not appear to run, check in this order: the extension row exists and is enabled, the group directory matches the folder column, the method is public, and the method name matches the event exactly, case included.

How arguments arrive

The dispatcher does not call your method with the raw arguments. It calls the listener with the Event object, and Hubzero\Events\WrappedListener unpacks it: it assigns the event to $this->event on your plugin, takes $event->getArguments(), re-indexes it if the keys are associative, and spreads the values across your method's parameters in order.

So a trigger of

Event::trigger('content.onContentPrepare', array($context, &$article, &$params, $page));

reaches

public function onContentPrepare($context, &$article, &$params, $page = 0)

with the four values in the order they were given. Give every parameter after the ones you require a default, because a caller may pass fewer — and because several core events are triggered from more than one place with different argument counts. A missing default is an ArgumentCountError raised inside the dispatcher, which reads as a framework fault rather than a plugin one.

Whatever the method returns is collected into the event's response array; returning nothing contributes nothing, which is how a plugin declines an event it is registered for. See Loading.

Which events to answer

Event names are prefixed with the plugin group when triggered, in group.eventName form; the method is named after the half on the right. The lifecycle events, all in the system group, are the ones every hub developer ends up using:

Event When
system.onAfterInitialise The application is built, before routing.
system.onAfterRoute The route has been resolved to a component.
system.onAfterDispatch The component has run.
system.onBeforeRender Before the document is rendered into the template.
system.onAfterRender After rendering, before the response is sent.

For everything else, read the events reference. It is generated from the source tree and covers 279 events across 41 groups, with the call sites that fire each one, the arguments they pass, and the plugins that already listen — which is both the list of what you can answer and the set of worked examples for answering it. Start with the page for the group you are extending: content, user, groups, members, system.

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