Skip to main content

4. Syncing your own Download ID field into Joomla's update sites

If you want customers to enter their license key once, in your extension's own Options page, rather than hunting for the Update Sites screen, you need to sync that value into #__update_sites.extra_query yourself. A safe pattern is a self-healing sync that runs on every admin page load via a lightweight system plugin, throttled so it only does real work when the value actually changes:

public static function syncDownloadIdToUpdateSites(): void
{
    try {
        $downloadId = trim((string) ComponentHelper::getParams('com_yourpackage')->get('download_id', ''));
        $db = Factory::getContainer()->get(DatabaseDriver::class);

        $query = $db->getQuery(true)
            ->select($db->quoteName(['update_site_id', 'extra_query']))
            ->from($db->quoteName('#__update_sites'))
            ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%yoursite.com/updates/%'));
        $rows = (array) $db->setQuery($query)->loadObjectList();

        $desired = $downloadId !== '' ? 'dlid=' . $downloadId : '';

        foreach ($rows as $row) {
            if ((string) ($row->extra_query ?? '') === $desired) {
                continue;
            }
            $update = $db->getQuery(true)
                ->update($db->quoteName('#__update_sites'))
                ->set($db->quoteName('extra_query') . ' = ' . $db->quote($desired))
                ->where($db->quoteName('update_site_id') . ' = ' . (int) $row->update_site_id);
            $db->setQuery($update)->execute();
        }
    } catch (\Throwable $e) {
        // See the warning below before you write this line.
    }
}
TWO BUGS WE SHIPPED, BOTH INVISIBLE FOR WEEKS

First: #__update_sites's primary key column is update_site_id, not id. If you write quoteName(['id', 'extra_query']) out of habit, the query throws on every single call — and if your catch block silently swallows the exception (as ours originally did, with a comment reading "housekeeping only, never let this break an admin page load"), the sync never runs, ever, and there is nothing in any log to tell you that.

Second: if you ever change your update stream's URL (say, moving from a raw component URL to a clean SEF alias), your location LIKE match has to change with it. We moved URLs and forgot to update the match pattern, so the sync silently stopped finding its own update-site row. Match on a stable fragment of your domain and path (%yoursite.com/updates/%) rather than the exact historical query string, so future URL changes don't quietly break it again.

The lesson underneath both bugs is the same: a bare catch (\Throwable $e) {} around anything you'll need to debug later is a trap. At minimum, log the exception message somewhere you'll actually see it. We only found these two bugs by reading the code line by line after the symptom (an empty Download Key field, despite the customer having entered a real value) forced us to stop trusting that the sync had ever run at all.