As noted earlier in this section, Joomla does not really offer a full implementation of the Active Record pattern — at least, not yet. One of the biggest omissions is relationships.
Let's say that we have a helpdesk ticket system. Each support ticket has one or more posts. Conversely, each post has exactly one ticket. Posts and tickets are database tables with corresponding Table classes. I actually wrote such a component and found myself too often having to get the ticket a post belongs to, usually many times within the same request. Sure I can instantiate a TicketTable object and have it load the data off the database but if I'm doing that several times in a request it's slow and unhelpful. So, how about we “upgrade” our PostTable to return our ticket object? It's quite easy, really!
<?php use Acme\Component\Example\Administrator\Table\TicketTable; class PostTable extends \Joomla\CMS\Table\Table { /** * Ticket this post belongs to * * @var TicketTable|null */ private $ticket; /** * Get the ticket this post belongs to * * @return TicketTable|null */ public function getTicket(): ?TicketTable { if (is_null($this->ticket)) { $this->ticket = new TicketTable($this->getDbo(), $this->getDispatcher()); if ($this->ticket->load($this->ticket_id) === false) { throw new RuntimeException('There is no such ticket'); } } return $this->ticket; } /** * Set the ticket this post belongs to. * * @param TicketTable|null $ticket The ticket to set. NULL to make ATS reload the ticket on the next getTicket() * method call. * @param bool $force True to reset $this->ticket_id to the $ticket ID (or NULL, if no ticket). * * @return self */ public function setTicket(?TicketTable $ticket, bool $force = false): self { $this->ticket = $ticket; if ($force) { $this->ticket_id = empty($ticket) ? null : $ticket->getId(); return $this; } if (empty($ticket) && $this->ticket->getId() != $this->ticket_id) { throw new InvalidArgumentException('Ticket ID does not correspond to the loaded post'); } return $this; } /** * Remember to remove the loaded relationship when resetting the table. */ public function reset() { $this->ticket = null; parent::reset(); } }
![]() | Note |
---|---|
I am instantiating the TicketTable directly because, in this case, I know I can safely do so. If my TicketTable had dependencies on other tables I should be going through the MVCFactory. I decided to keep it simple here because there's already a lot going on. |
The idea can be broken down in the following steps:
-
Create a private property to hold our related object, in this case the ticket the post belongs to.
-
Reset that property back to NULL in the
reset()
method. -
Create a setter which sets the relationship if and only if the TicketTable object we're passing matches the post table's
ticket_id
column value. The setter is optional; it makes it easier for us when creating both a new post and a new ticket i.e. when a client submits a new helpdesk item. -
Create a getter which returns the already saved relationship object. If none is specified, we load it from the database.
This is very naive, late-bound relationship handling (lazy loading) but it's better than nothing.
If you'd like to do eager relationship loading (when loading a big list of posts) you'd have to implement it yourself. For starters, you'd need something like that in your PostTable:
/** * @param self[] $postTables */ public function eagerLoad(array &$postTables): void { // Get the unique ticket IDs $ticketIDs = array_map( function (PostTable $x) { return $x->ticket_id; }, $postTables ); $ticketIDs = array_unique($ticketIDs); // Get the raw ticket data, keyed by ticket ID $db = $this->getDbo(); $query = $db->getQuery(true) ->select('*') ->from('#__example_tickets') ->whereIn($db->quoteName('id'), $ticketIDs); $tickets = $db->setQuery($query)->loadObjectList('id') ?: []; // Convert the raw ticket data into TicketTable objects $tickets = array_map( function (object $rawData): TicketTable { $ticket = new TicketTable($this->getDbo(), $this->getDispatcher()); $ticket->bind($rawData); return $ticket; }, $tickets ); // Set the TicketTable objects to each PostTable object foreach ($postTables as $postTable) { $postTable->setTicket($tickets[$postTable->ticket_id]); } }
Now, whenever you write code which returns an array of PostTable objects just run the resulting through that method and it set the related TicketObject instances to each member of the array of PostTable objects.
Why do eager loading? With eager loading you only need 2 queries: one to get the list of posts data, one to get the list of tickets data. With lazy loading you'd need N + 1 database queries: one to get the list of posts data, one per post (N queries in total) to get each post's ticket data.
Eager loading only makes sense if you are going to process a large number of records all at once and you are definitely going to need access to their related objects.