Developers

Models

A model represents one thing the component stores: an instrument, a reservation, a comment. It owns the table, the validation rules, the relationships to other models, and any behaviour that belongs to the record rather than to a screen.

Put the rules here, not in the controller. com_bookings has two ways to create a reservation — a user booking a slot on the site, and a lab manager adding one in the administrator — and a rule that lives on the model is enforced by both. A rule written into saveTask() is enforced by one.

Models live in com_{componentname}/models/, above the client directories, because the site, the administrator, and the API all use the same ones. A file models/reservation.php holds Components\Bookings\Models\Reservation, and the class loader finds it from the class name alone — no require is needed.

The smallest one

namespace Components\Bookings\Models;

use Hubzero\Database\Relational;

class Instrument extends Relational
{
	protected $namespace = 'bookings';
}

That is enough to read and write #__bookings_instruments:

$instrument = Instrument::oneOrFail($id);
$instrument->set('title', 'Confocal microscope');
$instrument->save();

The ORM model

New models extend Hubzero\Database\Relational. The class declares what makes it different from every other model and inherits the rest. com_kb's Article shows the full set:

class Article extends Relational implements \Hubzero\Search\Searchable
{
	/**
	 * The table namespace
	 *
	 * @var  string
	 */
	protected $namespace = 'kb';

	/**
	 * Default order by for model
	 *
	 * @var  string
	 */
	public $orderBy = 'title';

	/**
	 * Default order direction for select queries
	 *
	 * @var  string
	 */
	public $orderDir = 'asc';

	/**
	 * Fields and their validation criteria
	 *
	 * @var  array
	 */
	protected $rules = array(
		'title'    => 'notempty',
		'category' => 'positive|nonzero',
		'fulltxt'  => 'notempty'
	);

	/**
	 * Automatically fillable fields
	 *
	 * @var  array
	 **/
	public $always = array(
		'alias',
		'modified',
		'modified_by'
	);

	/**
	 * Automatic fields to populate every time a row is created
	 *
	 * @var  array
	 */
	public $initiate = array(
		'created',
		'created_by'
	);

	/**
	 * Fields to be parsed
	 *
	 * @var  array
	 **/
	protected $parsed = array(
		'fulltxt'
	);
Property Meaning
$namespace the table prefix after #__
$table the table name, when the derived one is wrong
$orderBy, $orderDir the default sort
$rules per-field validation, checked on save
$always fields recomputed on every save
$initiate fields filled once, when the row is created
$parsed fields the content parser may be run over

The table name is derived

The constructor pluralises the lowercased class name and prefixes it with #__ and the namespace:

$namespace = (!$this->namespace ? '' : $this->namespace . '_');
$plural    = \Hubzero\Utility\Inflector::pluralize(strtolower($this->getModelName()));
$this->table = $this->table ?: '#__' . $namespace . $plural;

So Reservation with $namespace = 'bookings' is #__bookings_reservations, and Instrument is #__bookings_instruments. Set $table explicitly when an inherited table does not fit that pattern.

Validation

$rules names a validator per field — notempty, positive|nonzero, and so on. save() returns false and fills the error bag when one fails, which is why a save task reads:

if (!$row->save())
{
	Notify::error($row->getError());
	return $this->editTask($row);
}

Checking the return value is not optional. save() does not throw on a failed rule; ignore the false and the task redirects with a success message over a record that was never written.

$always and $initiate are filled by convention: a field named alias in either list looks for an automaticAlias($data) method on the model, created for automaticCreated(), and so on. com_kb's Article defines automaticAlias() to slugify the title, so the alias is maintained without a controller ever setting it. com_bookings uses the same hook for a reservation's created and created_by.

setup() runs after construction. com_kb uses it to merge the article's own params over the component's, giving $article->params as one registry.

Relationships

Relationships are methods that return a relation object:

	/**
	 * Get parent category
	 *
	 * @return  object
	 */
	public function parentCategory()
	{
		return $this->belongsToOne('Components\Categories\Models\Category', 'category');
	}

	/**
	 * Get a list of comments
	 *
	 * @return  object
	 */
	public function comments()
	{
		return $this->oneToMany('Comment', 'entry_id');
	}

belongsToOne(), oneToMany(), manyToMany(), oneShiftsToMany(), and oneToManyThrough() are the ones in common use. For com_bookings, an Instrument has many Reservations and a Reservation belongs to one Instrument:

public function reservations()
{
	return $this->oneToMany('Reservation', 'instrument_id');
}

Call the method to get the related rows, $instrument->reservations()->rows(), or read it as a property, $reservation->instrument->get('title'), and the query runs lazily.

Lazily is the word to watch. Reading a relation as a property inside a loop over a list runs one query per row, and the page is slow for reasons the code does not show. See the ORM chapter for eager loading.

Querying

Relational is also the query builder. Static calls start a query and instance calls chain onto it:

$rows = Reservation::all()
	->whereEquals('instrument_id', $id)
	->whereEquals('state', Reservation::STATE_PUBLISHED)
	->ordered('filter_order', 'filter_order_Dir')
	->paginated('limitstart', 'limit')
	->rows();

$one = Reservation::oneOrNew($id);       // existing row, or a blank one
$one = Reservation::oneOrFail($id);      // or throw

ordered() and paginated() take the names of request variables, read the sort column, direction, and page from the request, and remember them in model state — which is why an administrator list controller passes 'filter_order' and 'limitstart' rather than values. Passing a column name straight into ordered() sorts by whatever request variable happens to carry that name, which is usually none, so the list comes back in the default order and nothing complains. Full details are in the ORM chapter.

Events a save fires

save() triggers two events, and they are the hook a hub uses to react to a component's records without editing the component:

if ($this->isNew())
{
	\Event::trigger($this->getTableName() . '_new', ['model' => $this]);
}

\Event::trigger('system.onContentSave', array($this->getTableName(), $this));

system.onContentSave carries a group — the part before the dot — so the dispatcher loads the system plugin group before firing it. That is how plg_system_content gets a chance to index the record.

Firing your own event

To let a hub react to a reservation reliably, trigger an event yourself, with a group of your own:

Event::trigger('bookings.onReservationCreate', array($reservation));

The dispatcher splits on the dot, loads every enabled plugin whose #__extensions row has folder = 'bookings', and calls onReservationCreate($reservation) on each — the arguments array is spread onto the method positionally. Nothing else has to be declared; the group is whatever you name.

A plugin answering it is one class in app/plugins/bookings/notify/notify.php with its own migration. See Events, Plugins and the events reference.

The failure here is quiet in the usual way: a plugin whose directory exists but whose #__extensions row does not is never loaded, so the event fires, nothing answers, and Event::trigger() returns an empty array.

Plain model classes

Not everything that lives in models/ is a Relational. com_kb's Archive is an ordinary class that assembles cross-model queries — the category list with article counts, the most popular articles — and has no table of its own. Controllers build one in execute() and hand it to the view.

That is the right shape whenever the thing you are modelling is a view over several tables rather than a row in one: for com_bookings, "the calendar for this week" is an Archive, not a record.

The older base class

Hubzero\Base\Model predates the ORM and is still present; older components extend it. It wraps a Hubzero\Database\Table object, exposes state constants, and offers get()/set() through Hubzero\Base\Obj. Do not use it for new work — Relational does the same job with far less code, and new tables should be modelled with it. If you are reading a component that uses it, you are reading inherited code, not a pattern to copy.

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