Moko Consulting

Open-source software for Joomla, Gitea, and web platforms. Home of MokoSuite, MokoGitea, and MokoCLI.

Tennessee
architecture/joomla-mvc-patterns.-

Joomla MVC Patterns

Joomla 5/6 MVC architecture used in MokoSuite components.

Namespace Convention

MokoConsulting\Component\{ExtensionName}\Administrator\{Layer}\{Class}
MokoConsulting\Component\{ExtensionName}\Site\{Layer}\{Class}

Example for MokoSuiteBackup:

MokoConsulting\Component\MokoSuiteBackup\Administrator\Controller\BackupController
MokoConsulting\Component\MokoSuiteBackup\Administrator\Model\BackupsModel
MokoConsulting\Component\MokoSuiteBackup\Administrator\View\Backups\HtmlView
MokoConsulting\Component\MokoSuiteBackup\Administrator\Table\BackupTable
MokoConsulting\Component\MokoSuiteBackup\Administrator\Extension\MokoSuiteBackupComponent

Component Extension Class

The main component class registers services and boots the component:

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\Extension;

use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Component\Router\RouterServiceInterface;

final class MokoSuiteBackupComponent extends MVCComponent
    implements BootableExtensionInterface, RouterServiceInterface
{
    public function boot(ContainerInterface $container): void
    {
        // Register custom services
    }
}

Controllers

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\Controller;

use Joomla\CMS\MVC\Controller\FormController;

final class ProfileController extends FormController
{
    protected $text_prefix = 'COM_MOKOSUITEBACKUP_PROFILE';

    public function getModel($name = 'Profile', $prefix = 'Administrator', $config = []): ProfileModel
    {
        return parent::getModel($name, $prefix, $config);
    }
}

Models

List Model (multiple records)

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\Model;

use Joomla\CMS\MVC\Model\ListModel;

final class BackupsModel extends ListModel
{
    protected function getListQuery(): DatabaseQuery
    {
        $db = $this->getDatabase();
        $query = $db->getQuery(true)
            ->select('a.*')
            ->from($db->quoteName('#__mokosuitebackup_records', 'a'))
            ->order($db->quoteName('a.created_at') . ' DESC');

        // Filter by published state
        $published = $this->getState('filter.published');
        if (is_numeric($published)) {
            $query->where($db->quoteName('a.published') . ' = :published')
                ->bind(':published', $published, ParameterType::INTEGER);
        }

        return $query;
    }

    protected function populateState($ordering = 'a.created_at', $direction = 'DESC'): void
    {
        $this->setState('filter.published', $this->getUserStateFromRequest(
            $this->context . '.filter.published', 'filter_published', '', 'string'
        ));
        parent::populateState($ordering, $direction);
    }
}

Form Model (single record CRUD)

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\Model;

use Joomla\CMS\MVC\Model\AdminModel;

final class ProfileModel extends AdminModel
{
    public function getForm($data = [], $loadData = true): Form|false
    {
        return $this->loadForm(
            'com_mokosuitebackup.profile',
            'profile',
            ['control' => 'jform', 'load_data' => $loadData]
        );
    }

    protected function loadFormData(): mixed
    {
        return $this->getItem();
    }
}

Views

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\View\Backups;

use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;

final class HtmlView extends BaseHtmlView
{
    protected $items;
    protected $pagination;

    public function display($tpl = null): void
    {
        $this->items = $this->get('Items');
        $this->pagination = $this->get('Pagination');
        $this->addToolbar();
        parent::display($tpl);
    }

    protected function addToolbar(): void
    {
        ToolbarHelper::title(Text::_('COM_MOKOSUITEBACKUP_BACKUPS'), 'archive');
        ToolbarHelper::deleteList('', 'backups.delete');
    }
}

Tables

namespace MokoConsulting\Component\MokoSuiteBackup\Administrator\Table;

use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;

final class BackupTable extends Table
{
    public function __construct(DatabaseDriver $db)
    {
        parent::__construct('#__mokosuitebackup_records', 'id', $db);
    }

    public function check(): bool
    {
        // Validation before save
        if (empty($this->profile_id)) {
            $this->setError('Profile is required');
            return false;
        }
        return true;
    }
}

Event Subscribers (Plugins)

Modern Joomla 5 plugins use SubscriberInterface:

namespace MokoConsulting\Plugin\System\MokoSuiteBackup\Extension;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;

final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onAfterExtensionUpdate' => 'handleAutoBackup',
            'onTaskOptionsList'      => 'registerTaskOptions',
        ];
    }

    public function handleAutoBackup(Event $event): void
    {
        // Auto-backup before extension updates
    }
}