joomla3-legacy

star 7

Joomla 3.x legacy patterns with JFactory, non-namespaced MVC, JDatabase, and plugin structure

zb-ss By zb-ss schedule Updated 2/9/2026

name: joomla3-legacy description: Joomla 3.x legacy patterns with JFactory, non-namespaced MVC, JDatabase, and plugin structure license: MIT compatibility: opencode metadata: cms: joomla version: "3.x"

Component Structure (Joomla 3)

com_example/
├── administrator/
│   ├── controllers/
│   │   ├── example.php        # JControllerForm
│   │   └── items.php          # JControllerAdmin
│   ├── helpers/
│   │   └── example.php
│   ├── models/
│   │   ├── fields/            # Custom form fields
│   │   ├── forms/
│   │   │   └── item.xml
│   │   ├── item.php           # JModelAdmin
│   │   └── items.php          # JModelList
│   ├── sql/
│   │   ├── install.mysql.utf8.sql
│   │   └── uninstall.mysql.utf8.sql
│   ├── tables/
│   │   └── item.php           # JTable
│   ├── views/
│   │   ├── item/
│   │   │   ├── tmpl/
│   │   │   │   └── edit.php
│   │   │   └── view.html.php  # JViewLegacy
│   │   └── items/
│   │       ├── tmpl/
│   │       │   └── default.php
│   │       └── view.html.php
│   ├── controller.php         # Main JControllerLegacy
│   └── example.php            # Entry point
├── site/
│   ├── controllers/
│   ├── models/
│   ├── views/
│   ├── controller.php
│   └── example.php
├── media/
│   ├── css/
│   └── js/
└── language/
    └── en-GB/
        ├── en-GB.com_example.ini
        └── en-GB.com_example.sys.ini

Entry Point

<?php
// administrator/components/com_example/example.php

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

// Access check
if (!JFactory::getUser()->authorise('core.manage', 'com_example')) {
    throw new Exception(JText::_('JERROR_ALERTNOAUTHOR'), 403);
}

// Include helper
JLoader::register('ExampleHelper', JPATH_COMPONENT . '/helpers/example.php');

// Get controller
$controller = JControllerLegacy::getInstance('Example');

// Execute task
$controller->execute(JFactory::getApplication()->input->get('task'));

// Redirect
$controller->redirect();

Controllers

Main Controller

<?php
// administrator/components/com_example/controller.php

defined('_JEXEC') or die;

class ExampleController extends JControllerLegacy
{
    protected $default_view = 'items';
    
    public function display($cachable = false, $urlparams = array())
    {
        $view = $this->input->get('view', 'items');
        $layout = $this->input->get('layout', 'default');
        $id = $this->input->getInt('id');
        
        // Check edit form
        if ($view === 'item' && $layout === 'edit' && !$this->checkEditId('com_example.edit.item', $id)) {
            $this->setError(JText::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id));
            $this->setMessage($this->getError(), 'error');
            $this->setRedirect(JRoute::_('index.php?option=com_example&view=items', false));
            return false;
        }
        
        parent::display($cachable, $urlparams);
        
        return $this;
    }
}

Admin Controller (for single item CRUD)

<?php
// administrator/components/com_example/controllers/item.php

defined('_JEXEC') or die;

class ExampleControllerItem extends JControllerForm
{
    // JControllerForm handles add, edit, save, cancel, batch automatically
    
    protected function allowAdd($data = array())
    {
        return JFactory::getUser()->authorise('core.create', 'com_example');
    }
    
    protected function allowEdit($data = array(), $key = 'id')
    {
        $id = isset($data[$key]) ? $data[$key] : 0;
        $user = JFactory::getUser();
        
        return $user->authorise('core.edit', 'com_example.item.' . $id)
            || ($user->authorise('core.edit.own', 'com_example.item.' . $id)
                && $data['created_by'] == $user->id);
    }
}

List Controller (for bulk actions)

<?php
// administrator/components/com_example/controllers/items.php

defined('_JEXEC') or die;

class ExampleControllerItems extends JControllerAdmin
{
    public function getModel($name = 'Item', $prefix = 'ExampleModel', $config = array('ignore_request' => true))
    {
        return parent::getModel($name, $prefix, $config);
    }
}

Models

List Model

<?php
// administrator/components/com_example/models/items.php

defined('_JEXEC') or die;

JLoader::register('ExampleHelper', JPATH_COMPONENT . '/helpers/example.php');

class ExampleModelItems extends JModelList
{
    public function __construct($config = array())
    {
        if (empty($config['filter_fields'])) {
            $config['filter_fields'] = array(
                'id', 'a.id',
                'title', 'a.title',
                'state', 'a.state',
                'ordering', 'a.ordering',
            );
        }
        
        parent::__construct($config);
    }
    
    protected function populateState($ordering = 'a.ordering', $direction = 'ASC')
    {
        $app = JFactory::getApplication();
        
        // Filter: search
        $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string');
        $this->setState('filter.search', $search);
        
        // Filter: state
        $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string');
        $this->setState('filter.state', $state);
        
        parent::populateState($ordering, $direction);
    }
    
    protected function getListQuery()
    {
        $db = $this->getDbo();
        $query = $db->getQuery(true);
        
        $query->select('a.*')
            ->from($db->quoteName('#__example_items', 'a'));
        
        // Filter: state
        $state = $this->getState('filter.state');
        if (is_numeric($state)) {
            $query->where($db->quoteName('a.state') . ' = ' . (int) $state);
        }
        
        // Filter: search
        $search = $this->getState('filter.search');
        if (!empty($search)) {
            $search = $db->quote('%' . $db->escape($search, true) . '%');
            $query->where($db->quoteName('a.title') . ' LIKE ' . $search);
        }
        
        // Ordering
        $orderCol = $this->state->get('list.ordering', 'a.ordering');
        $orderDir = $this->state->get('list.direction', 'ASC');
        $query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
        
        return $query;
    }
}

Admin Model (single item)

<?php
// administrator/components/com_example/models/item.php

defined('_JEXEC') or die;

class ExampleModelItem extends JModelAdmin
{
    protected $text_prefix = 'COM_EXAMPLE';
    
    public function getTable($type = 'Item', $prefix = 'ExampleTable', $config = array())
    {
        return JTable::getInstance($type, $prefix, $config);
    }
    
    public function getForm($data = array(), $loadData = true)
    {
        $form = $this->loadForm(
            'com_example.item',
            'item',
            array('control' => 'jform', 'load_data' => $loadData)
        );
        
        if (empty($form)) {
            return false;
        }
        
        return $form;
    }
    
    protected function loadFormData()
    {
        $data = JFactory::getApplication()->getUserState('com_example.edit.item.data', array());
        
        if (empty($data)) {
            $data = $this->getItem();
        }
        
        return $data;
    }
    
    protected function prepareTable($table)
    {
        $date = JFactory::getDate();
        $user = JFactory::getUser();
        
        if (empty($table->id)) {
            // New item
            $table->created = $date->toSql();
            $table->created_by = $user->id;
            
            // Ordering
            if (empty($table->ordering)) {
                $db = $this->getDbo();
                $query = $db->getQuery(true)
                    ->select('MAX(ordering)')
                    ->from($db->quoteName('#__example_items'));
                $db->setQuery($query);
                $table->ordering = (int) $db->loadResult() + 1;
            }
        } else {
            // Existing item
            $table->modified = $date->toSql();
            $table->modified_by = $user->id;
        }
    }
}

Table Class

<?php
// administrator/components/com_example/tables/item.php

defined('_JEXEC') or die;

class ExampleTableItem extends JTable
{
    public function __construct(&$db)
    {
        parent::__construct('#__example_items', 'id', $db);
        
        // Enable asset tracking (for permissions)
        $this->setColumnAlias('published', 'state');
    }
    
    public function bind($array, $ignore = '')
    {
        // Handle params as JSON
        if (isset($array['params']) && is_array($array['params'])) {
            $registry = new JRegistry($array['params']);
            $array['params'] = (string) $registry;
        }
        
        return parent::bind($array, $ignore);
    }
    
    public function check()
    {
        // Validate title
        if (trim($this->title) === '') {
            $this->setError(JText::_('COM_EXAMPLE_ERROR_TITLE_REQUIRED'));
            return false;
        }
        
        // Generate alias
        if (empty($this->alias)) {
            $this->alias = $this->title;
        }
        $this->alias = JFilterOutput::stringURLSafe($this->alias);
        
        return true;
    }
    
    public function store($updateNulls = false)
    {
        $date = JFactory::getDate();
        $user = JFactory::getUser();
        
        if ($this->id) {
            $this->modified = $date->toSql();
            $this->modified_by = $user->id;
        } else {
            if (!(int) $this->created) {
                $this->created = $date->toSql();
            }
            if (empty($this->created_by)) {
                $this->created_by = $user->id;
            }
        }
        
        return parent::store($updateNulls);
    }
}

Views

List View

<?php
// administrator/components/com_example/views/items/view.html.php

defined('_JEXEC') or die;

class ExampleViewItems extends JViewLegacy
{
    protected $items;
    protected $pagination;
    protected $state;
    protected $filterForm;
    protected $activeFilters;
    
    public function display($tpl = null)
    {
        $this->items = $this->get('Items');
        $this->pagination = $this->get('Pagination');
        $this->state = $this->get('State');
        $this->filterForm = $this->get('FilterForm');
        $this->activeFilters = $this->get('ActiveFilters');
        
        // Check for errors
        if (count($errors = $this->get('Errors'))) {
            throw new Exception(implode("\n", $errors), 500);
        }
        
        $this->addToolbar();
        
        parent::display($tpl);
    }
    
    protected function addToolbar()
    {
        $user = JFactory::getUser();
        
        JToolbarHelper::title(JText::_('COM_EXAMPLE_ITEMS_TITLE'), 'list');
        
        if ($user->authorise('core.create', 'com_example')) {
            JToolbarHelper::addNew('item.add');
        }
        
        if ($user->authorise('core.edit', 'com_example') || $user->authorise('core.edit.own', 'com_example')) {
            JToolbarHelper::editList('item.edit');
        }
        
        if ($user->authorise('core.edit.state', 'com_example')) {
            JToolbarHelper::publish('items.publish', 'JTOOLBAR_PUBLISH', true);
            JToolbarHelper::unpublish('items.unpublish', 'JTOOLBAR_UNPUBLISH', true);
        }
        
        if ($user->authorise('core.delete', 'com_example')) {
            JToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'items.delete');
        }
        
        if ($user->authorise('core.admin', 'com_example')) {
            JToolbarHelper::preferences('com_example');
        }
    }
}

List View Template

<?php
// administrator/components/com_example/views/items/tmpl/default.php

defined('_JEXEC') or die;

JHtml::_('bootstrap.tooltip');
JHtml::_('behavior.multiselect');
JHtml::_('formbehavior.chosen', 'select');

$user = JFactory::getUser();
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
$saveOrder = $listOrder === 'a.ordering';

if ($saveOrder) {
    $saveOrderingUrl = 'index.php?option=com_example&task=items.saveOrderAjax&tmpl=component';
    JHtml::_('sortablelist.sortable', 'itemList', 'adminForm', strtolower($listDirn), $saveOrderingUrl);
}
?>
<form action="<?php echo JRoute::_('index.php?option=com_example&view=items'); ?>" method="post" name="adminForm" id="adminForm">
    <div id="j-sidebar-container" class="span2">
        <?php echo JHtmlSidebar::render(); ?>
    </div>
    <div id="j-main-container" class="span10">
        <?php echo JLayoutHelper::render('joomla.searchtools.default', array('view' => $this)); ?>
        
        <?php if (empty($this->items)) : ?>
            <div class="alert alert-no-items">
                <?php echo JText::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
            </div>
        <?php else : ?>
            <table class="table table-striped" id="itemList">
                <thead>
                    <tr>
                        <th width="1%" class="nowrap center hidden-phone">
                            <?php echo JHtml::_('searchtools.sort', '', 'a.ordering', $listDirn, $listOrder, null, 'asc', 'JGRID_HEADING_ORDERING', 'icon-menu-2'); ?>
                        </th>
                        <th width="1%" class="center">
                            <?php echo JHtml::_('grid.checkall'); ?>
                        </th>
                        <th width="1%" class="nowrap center">
                            <?php echo JHtml::_('searchtools.sort', 'JSTATUS', 'a.state', $listDirn, $listOrder); ?>
                        </th>
                        <th>
                            <?php echo JHtml::_('searchtools.sort', 'JGLOBAL_TITLE', 'a.title', $listDirn, $listOrder); ?>
                        </th>
                        <th width="1%" class="nowrap center hidden-phone">
                            <?php echo JHtml::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($this->items as $i => $item) :
                        $canEdit = $user->authorise('core.edit', 'com_example.item.' . $item->id);
                        $canChange = $user->authorise('core.edit.state', 'com_example.item.' . $item->id);
                    ?>
                    <tr class="row<?php echo $i % 2; ?>">
                        <td class="order nowrap center hidden-phone">
                            <?php
                            $iconClass = '';
                            if (!$canChange) {
                                $iconClass = ' inactive';
                            } elseif (!$saveOrder) {
                                $iconClass = ' inactive tip-top hasTooltip" title="' . JHtml::_('tooltipText', 'JORDERINGDISABLED');
                            }
                            ?>
                            <span class="sortable-handler<?php echo $iconClass; ?>">
                                <span class="icon-menu" aria-hidden="true"></span>
                            </span>
                            <input type="text" style="display:none" name="order[]" size="5" value="<?php echo $item->ordering; ?>" class="width-20 text-area-order" />
                        </td>
                        <td class="center">
                            <?php echo JHtml::_('grid.id', $i, $item->id); ?>
                        </td>
                        <td class="center">
                            <?php echo JHtml::_('jgrid.published', $item->state, $i, 'items.', $canChange); ?>
                        </td>
                        <td>
                            <?php if ($canEdit) : ?>
                                <a href="<?php echo JRoute::_('index.php?option=com_example&task=item.edit&id=' . $item->id); ?>">
                                    <?php echo $this->escape($item->title); ?>
                                </a>
                            <?php else : ?>
                                <?php echo $this->escape($item->title); ?>
                            <?php endif; ?>
                        </td>
                        <td class="center hidden-phone">
                            <?php echo (int) $item->id; ?>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
            <?php echo $this->pagination->getListFooter(); ?>
        <?php endif; ?>
    </div>
    
    <input type="hidden" name="task" value="" />
    <input type="hidden" name="boxchecked" value="0" />
    <?php echo JHtml::_('form.token'); ?>
</form>

JFactory Static Calls

// Application
$app = JFactory::getApplication();
$app->enqueueMessage(JText::_('COM_EXAMPLE_MESSAGE_SUCCESS'), 'success');
$app->redirect(JRoute::_('index.php?option=com_example', false));

// Input
$input = JFactory::getApplication()->input;
$id = $input->getInt('id', 0);
$task = $input->get('task', '', 'cmd');
$data = $input->get('jform', array(), 'array');

// Database
$db = JFactory::getDbo();
$query = $db->getQuery(true);

// User
$user = JFactory::getUser();
$userId = $user->id;
$isGuest = $user->guest;

// Session
$session = JFactory::getSession();
$session->set('my_value', $value);
$value = $session->get('my_value', 'default');

// Document
$doc = JFactory::getDocument();
$doc->addStyleSheet(JUri::root() . 'media/com_example/css/style.css');
$doc->addScript(JUri::root() . 'media/com_example/js/script.js');

// Language
$lang = JFactory::getLanguage();
$lang->load('com_example', JPATH_ADMINISTRATOR);

// Date
$date = JFactory::getDate();
$now = $date->toSql();

Database Queries

$db = JFactory::getDbo();
$query = $db->getQuery(true);

// SELECT
$query->select($db->quoteName(array('id', 'title', 'state')))
    ->from($db->quoteName('#__example_items'))
    ->where($db->quoteName('state') . ' = 1')
    ->where($db->quoteName('catid') . ' = ' . (int) $catid)
    ->order($db->quoteName('ordering') . ' ASC');

$db->setQuery($query);

// Single value
$result = $db->loadResult();

// Single row as object
$row = $db->loadObject();

// Single row as array
$row = $db->loadAssoc();

// All rows as objects
$rows = $db->loadObjectList();

// All rows keyed by column
$rows = $db->loadObjectList('id');

// Single column
$ids = $db->loadColumn();

// INSERT
$query->clear()
    ->insert($db->quoteName('#__example_items'))
    ->columns($db->quoteName(array('title', 'state', 'created')))
    ->values($db->quote($title) . ', 1, ' . $db->quote(JFactory::getDate()->toSql()));

$db->setQuery($query);
$db->execute();
$newId = $db->insertid();

// UPDATE
$query->clear()
    ->update($db->quoteName('#__example_items'))
    ->set($db->quoteName('title') . ' = ' . $db->quote($title))
    ->set($db->quoteName('state') . ' = 0')
    ->where($db->quoteName('id') . ' = ' . (int) $id);

$db->setQuery($query);
$db->execute();

// DELETE
$query->clear()
    ->delete($db->quoteName('#__example_items'))
    ->where($db->quoteName('id') . ' = ' . (int) $id);

$db->setQuery($query);
$db->execute();

Plugins (Joomla 3)

<?php
// plugins/system/example/example.php

defined('_JEXEC') or die;

class PlgSystemExample extends JPlugin
{
    /**
     * Load language automatically
     */
    protected $autoloadLanguage = true;
    
    /**
     * Application object
     */
    protected $app;
    
    /**
     * Database object
     */
    protected $db;
    
    public function onAfterInitialise()
    {
        // Runs after Joomla initializes
    }
    
    public function onAfterRoute()
    {
        // Runs after routing
        $option = $this->app->input->get('option');
        $view = $this->app->input->get('view');
    }
    
    public function onContentPrepare($context, &$article, &$params, $page = 0)
    {
        // Modify content before display
        if ($context !== 'com_content.article') {
            return;
        }
        
        $article->text = str_replace('{example}', 'Replacement', $article->text);
    }
    
    public function onUserAfterSave($user, $isNew, $success, $msg)
    {
        if (!$success) {
            return;
        }
        
        if ($isNew) {
            // New user created
        }
    }
}

Plugin XML

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
    <name>plg_system_example</name>
    <version>1.0.0</version>
    <author>Your Name</author>
    <creationDate>2024-01</creationDate>
    <description>PLG_SYSTEM_EXAMPLE_DESC</description>
    
    <files>
        <filename plugin="example">example.php</filename>
    </files>
    
    <languages>
        <language tag="en-GB">language/en-GB/en-GB.plg_system_example.ini</language>
        <language tag="en-GB">language/en-GB/en-GB.plg_system_example.sys.ini</language>
    </languages>
    
    <config>
        <fields name="params">
            <fieldset name="basic">
                <field
                    name="enabled"
                    type="radio"
                    label="PLG_SYSTEM_EXAMPLE_ENABLED_LABEL"
                    default="1"
                    class="btn-group btn-group-yesno">
                    <option value="1">JYES</option>
                    <option value="0">JNO</option>
                </field>
            </fieldset>
        </fields>
    </config>
</extension>

Security Patterns

// CSRF Token check
JSession::checkToken() or jexit(JText::_('JINVALID_TOKEN'));

// In forms
<?php echo JHtml::_('form.token'); ?>

// Permission check
$user = JFactory::getUser();
if (!$user->authorise('core.edit', 'com_example')) {
    throw new Exception(JText::_('JERROR_ALERTNOAUTHOR'), 403);
}

// Input filtering
$input = JFactory::getApplication()->input;
$id = $input->getInt('id');           // Integer
$title = $input->getString('title');   // String (filtered)
$raw = $input->get('html', '', 'raw'); // Raw (dangerous!)
$cmd = $input->get('task', '', 'cmd'); // Command (alphanumeric + .-_)

// Database escaping - ALWAYS use quoteName and quote
$query->where($db->quoteName('id') . ' = ' . (int) $id);
$query->where($db->quoteName('title') . ' = ' . $db->quote($title));

// Output escaping in templates
<?php echo $this->escape($item->title); ?>
<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>

Language Strings

// Basic translation
echo JText::_('COM_EXAMPLE_TITLE');

// With sprintf replacement
echo JText::sprintf('COM_EXAMPLE_ITEMS_COUNT', $count);

// Plural forms
echo JText::plural('COM_EXAMPLE_N_ITEMS_DELETED', $count);

// JavaScript strings
JText::script('COM_EXAMPLE_CONFIRM_DELETE');
// Then in JS: Joomla.JText._('COM_EXAMPLE_CONFIRM_DELETE')

JHtml Helpers

// Date formatting
echo JHtml::_('date', $item->created, JText::_('DATE_FORMAT_LC2'));

// Truncate text
echo JHtml::_('string.truncate', $item->description, 100);

// Boolean icons
echo JHtml::_('jgrid.published', $item->state, $i, 'items.');

// Select lists
echo JHtml::_('select.genericlist', $options, 'myfield', 'class="inputbox"', 'value', 'text', $selected);

// Tooltips
JHtml::_('bootstrap.tooltip');
echo '<span class="hasTooltip" title="' . JHtml::_('tooltipText', 'Title', 'Description') . '">Hover me</span>';

Common Gotchas

  1. No namespaces - Use JLoader::register() for custom classes
JLoader::register('ExampleHelper', JPATH_COMPONENT . '/helpers/example.php');
  1. JFactory deprecation warnings - Still works but generates notices in newer PHP

  2. jQuery conflicts - Joomla 3 uses MooTools by default

JHtml::_('jquery.framework');
  1. Different JInput methods - Use specific type methods
$input->getInt('id');      // Not $input->get('id', 0, 'int')
$input->getString('name');
$input->getCmd('task');
  1. Ordering - Always escape in ORDER BY
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
Install via CLI
npx skills add https://github.com/zb-ss/opencode-workflows --skill joomla3-legacy
Repository Details
star Stars 7
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator