Joomla
CMS positioned between WordPress and Drupal. Well-balanced between usability and functionality.
CMS
Joomla
Overview
Joomla is an open-source content management system that was born in 2005 as a fork from Mambo. As of 2024, version 5.x has been released, adopting a modern architecture that requires PHP 8.1+, MySQL 8.0.13+, MariaDB 10.4+, and PostgreSQL 12+. It is widely used for corporate websites, portal sites, e-commerce sites, and government websites.
Details
Joomla adopts a Model-View-Controller (MVC) architecture and allows functionality expansion through three types of extensions: components, modules, and plugins. It utilizes parts of the Symfony framework and leverages modern PHP features such as dependency injection (DI) containers and event dispatchers.
Key Features:
- Extension Ecosystem: Over 8,000 official extensions
- Multilingual Support: Language packs available for 70+ languages
- Accessibility: Jooa11y accessibility checker included
- Security: Regular security updates and vulnerability fixes
- API Support: Supports both RESTful API and GraphQL
- Joomla Component Builder: Efficiently build complex components
- Guided Tours: Features to support admin interface operations
Pros and Cons
Pros
- Powerful Access Control List (ACL) system
- Rich official extension library
- Active community support
- Advanced enterprise features
- Excellent security features
- Easy multilingual site construction
- MVC architecture for development efficiency
- Adoption of modern PHP development practices
Cons
- Somewhat steep learning curve
- Lower usage rate compared to WordPress
- Extension compatibility issues
- Complex admin interface
- High hosting requirements
- Careful attention needed during upgrades
References
- Joomla Official Site
- Joomla Documentation
- Joomla Extensions Directory
- Joomla Developer Manual
- Joomla Component Builder
- Joomla Community Magazine
Code Examples
1. Hello World (Basic Setup)
Basic Module Structure (mod_hello.xml)
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="site" method="upgrade">
<name>MOD_HELLO</name>
<author>Your Name</author>
<version>1.0.0</version>
<description>MOD_HELLO_DESCRIPTION</description>
<namespace path="src">My\Module\Hello</namespace>
<files>
<folder>src</folder>
<folder>tmpl</folder>
<folder>language</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="greeting"
type="text"
label="MOD_HELLO_GREETING_LABEL"
description="MOD_HELLO_GREETING_DESC"
default="Hello World!"
/>
</fieldset>
</fields>
</config>
</extension>
Service Provider (services/provider.php)
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\Module as ModuleServiceProvider;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory as ModuleDispatcherFactoryServiceProvider;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class () implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->registerServiceProvider(
new ModuleDispatcherFactoryServiceProvider('\\My\\Module\\Hello')
);
$container->registerServiceProvider(new ModuleServiceProvider());
}
};
2. Theme Development
Template Information File (templateDetails.xml)
<?xml version="1.0" encoding="utf-8"?>
<extension type="template" client="site" method="upgrade">
<name>MyTemplate</name>
<version>1.0.0</version>
<creationDate>2024-01</creationDate>
<author>Your Name</author>
<authorEmail>[email protected]</authorEmail>
<description>TPL_MYTEMPLATE_DESCRIPTION</description>
<files>
<filename>index.php</filename>
<filename>templateDetails.xml</filename>
<filename>error.php</filename>
<filename>offline.php</filename>
<folder>css</folder>
<folder>images</folder>
<folder>js</folder>
<folder>html</folder>
</files>
<positions>
<position>header</position>
<position>navigation</position>
<position>breadcrumb</position>
<position>content-top</position>
<position>content-bottom</position>
<position>sidebar-left</position>
<position>sidebar-right</position>
<position>footer</position>
</positions>
<config>
<fields name="params">
<fieldset name="advanced">
<field name="siteTitle"
type="text"
default=""
label="TPL_MYTEMPLATE_SITE_TITLE"
description="TPL_MYTEMPLATE_SITE_TITLE_DESC"
/>
</fieldset>
</fields>
</config>
</extension>
Main Template (index.php)
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
$app = Factory::getApplication();
$doc = $app->getDocument();
$user = $app->getIdentity();
$params = $app->getTemplate(true)->params;
// Web Asset Manager
$wa = $doc->getWebAssetManager();
$wa->useScript('core')
->useScript('keepalive')
->useScript('form.validate');
// Add template styles
$wa->registerAndUseStyle('template.main', 'media/templates/site/mytemplate/css/template.css');
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->language; ?>"
lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="site <?php echo $app->getInput()->getCmd('option', ''); ?>">
<header id="header" class="header">
<div class="container">
<div class="logo">
<a href="<?php echo Uri::root(); ?>">
<?php echo $params->get('siteTitle', $app->get('sitename')); ?>
</a>
</div>
<?php if ($doc->countModules('navigation')) : ?>
<nav class="navigation">
<jdoc:include type="modules" name="navigation" style="none" />
</nav>
<?php endif; ?>
</div>
</header>
<?php if ($doc->countModules('breadcrumb')) : ?>
<div class="breadcrumb-wrap">
<div class="container">
<jdoc:include type="modules" name="breadcrumb" style="none" />
</div>
</div>
<?php endif; ?>
<main id="content" class="main-content">
<div class="container">
<div class="row">
<?php if ($doc->countModules('sidebar-left')) : ?>
<aside class="sidebar-left col-md-3">
<jdoc:include type="modules" name="sidebar-left" style="well" />
</aside>
<?php endif; ?>
<div class="content col">
<jdoc:include type="message" />
<jdoc:include type="component" />
</div>
<?php if ($doc->countModules('sidebar-right')) : ?>
<aside class="sidebar-right col-md-3">
<jdoc:include type="modules" name="sidebar-right" style="well" />
</aside>
<?php endif; ?>
</div>
</div>
</main>
<footer id="footer" class="footer">
<div class="container">
<jdoc:include type="modules" name="footer" style="none" />
<p>© <?php echo date('Y'); ?> <?php echo $app->get('sitename'); ?></p>
</div>
</footer>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>
3. Plugin Development
Plugin Definition (plg_content_example.xml)
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="content" method="upgrade">
<name>PLG_CONTENT_EXAMPLE</name>
<author>Your Name</author>
<version>1.0.0</version>
<description>PLG_CONTENT_EXAMPLE_DESCRIPTION</description>
<namespace path="src">My\Plugin\Content\Example</namespace>
<files>
<folder>src</folder>
<folder>language</folder>
</files>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="search_text"
type="text"
label="PLG_CONTENT_EXAMPLE_SEARCH_TEXT"
description="PLG_CONTENT_EXAMPLE_SEARCH_TEXT_DESC"
/>
<field
name="replace_text"
type="text"
label="PLG_CONTENT_EXAMPLE_REPLACE_TEXT"
description="PLG_CONTENT_EXAMPLE_REPLACE_TEXT_DESC"
/>
</fieldset>
</fields>
</config>
</extension>
Plugin Class (src/Extension/Example.php)
<?php
namespace My\Plugin\Content\Example\Extension;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
class Example extends CMSPlugin implements SubscriberInterface
{
protected $autoloadLanguage = true;
public static function getSubscribedEvents(): array
{
return [
'onContentPrepare' => 'onContentPrepare',
'onContentBeforeDisplay' => 'onContentBeforeDisplay',
'onContentAfterDisplay' => 'onContentAfterDisplay',
];
}
public function onContentPrepare(Event $event)
{
[$context, $article, $params, $page] = array_values($event->getArguments());
// Check if plugin is in valid context
if ($context !== 'com_content.article') {
return;
}
// Get search and replace text from parameters
$searchText = $this->params->get('search_text', '');
$replaceText = $this->params->get('replace_text', '');
if ($searchText && $replaceText) {
$article->text = str_replace($searchText, $replaceText, $article->text);
}
}
public function onContentBeforeDisplay(Event $event)
{
[$context, $article, $params, $limitstart] = array_values($event->getArguments());
if ($context !== 'com_content.article') {
return '';
}
// Add content before article
$html = '<div class="content-before">';
$html .= '<p>Article ID: ' . $article->id . '</p>';
$html .= '</div>';
$event->setArgument('result', $html);
}
public function onContentAfterDisplay(Event $event)
{
[$context, $article, $params, $limitstart] = array_values($event->getArguments());
if ($context !== 'com_content.article') {
return '';
}
// Add content after article
$html = '<div class="content-after">';
$html .= '<p>Last Modified: ' . $article->modified . '</p>';
$html .= '</div>';
$event->setArgument('result', $html);
}
}
4. Custom Post Types (Components)
Component Definition (administrator/components/com_products/products.xml)
<?xml version="1.0" encoding="utf-8"?>
<extension type="component" method="upgrade">
<name>COM_PRODUCTS</name>
<author>Your Name</author>
<version>1.0.0</version>
<description>COM_PRODUCTS_DESCRIPTION</description>
<namespace path="src">My\Component\Products</namespace>
<install>
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
</sql>
</install>
<administration>
<menu>COM_PRODUCTS</menu>
<files folder="administrator/components/com_products">
<folder>sql</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
</administration>
<files folder="components/com_products">
<folder>src</folder>
<folder>tmpl</folder>
</files>
</extension>
Database Table Definition (sql/install.mysql.utf8.sql)
CREATE TABLE IF NOT EXISTS `#__products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`alias` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`description` text,
`price` decimal(10,2) DEFAULT NULL,
`sku` varchar(64) DEFAULT NULL,
`state` tinyint(3) NOT NULL DEFAULT 0,
`catid` int(11) NOT NULL DEFAULT 0,
`created` datetime NOT NULL,
`created_by` int(11) NOT NULL DEFAULT 0,
`modified` datetime NOT NULL,
`modified_by` int(11) NOT NULL DEFAULT 0,
`publish_up` datetime,
`publish_down` datetime,
`version` int(11) NOT NULL DEFAULT 1,
`ordering` int(11) NOT NULL DEFAULT 0,
`metakey` text,
`metadesc` text,
`access` int(11) NOT NULL DEFAULT 1,
`hits` int(11) NOT NULL DEFAULT 0,
`metadata` text,
`featured` tinyint(3) unsigned NOT NULL DEFAULT 0,
`language` char(7) NOT NULL DEFAULT '*',
PRIMARY KEY (`id`),
KEY `idx_access` (`access`),
KEY `idx_catid` (`catid`),
KEY `idx_state` (`state`),
KEY `idx_created_by` (`created_by`),
KEY `idx_language` (`language`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci;
5. Database Operations
Model Class (src/Model/ProductModel.php)
<?php
namespace My\Component\Products\Site\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Table\Table;
use Joomla\Database\ParameterType;
class ProductModel extends BaseDatabaseModel
{
/**
* Get a product
*/
public function getItem($pk = null)
{
$pk = (!empty($pk)) ? $pk : (int) $this->getState('product.id');
if ($this->_item === null) {
$this->_item = [];
}
if (!isset($this->_item[$pk])) {
try {
$db = $this->getDatabase();
$query = $db->getQuery(true);
$query->select('a.*')
->from($db->quoteName('#__products', 'a'))
->where($db->quoteName('a.id') . ' = :id')
->bind(':id', $pk, ParameterType::INTEGER);
// Check published state
$query->where($db->quoteName('a.state') . ' = 1');
$db->setQuery($query);
$data = $db->loadObject();
if (empty($data)) {
throw new \Exception('Product not found', 404);
}
$this->_item[$pk] = $data;
} catch (\Exception $e) {
$this->setError($e->getMessage());
$this->_item[$pk] = false;
}
}
return $this->_item[$pk];
}
/**
* Get product list
*/
public function getItems()
{
$db = $this->getDatabase();
$query = $db->getQuery(true);
$query->select([
$db->quoteName('a.id'),
$db->quoteName('a.title'),
$db->quoteName('a.alias'),
$db->quoteName('a.price'),
$db->quoteName('a.sku'),
$db->quoteName('a.created'),
$db->quoteName('a.hits')
])
->from($db->quoteName('#__products', 'a'))
->where($db->quoteName('a.state') . ' = 1');
// Category filter
$categoryId = $this->getState('filter.category_id');
if ($categoryId) {
$query->where($db->quoteName('a.catid') . ' = :catid')
->bind(':catid', $categoryId, ParameterType::INTEGER);
}
// Search filter
$search = $this->getState('filter.search');
if (!empty($search)) {
$search = '%' . $db->escape($search, true) . '%';
$query->where('(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' .
$db->quoteName('a.description') . ' LIKE :search2)')
->bind(':search1', $search)
->bind(':search2', $search);
}
// Ordering
$orderCol = $this->getState('list.ordering', 'a.ordering');
$orderDirn = $this->getState('list.direction', 'ASC');
$query->order($db->escape($orderCol . ' ' . $orderDirn));
// Pagination
$limit = $this->getState('list.limit', 10);
$start = $this->getState('list.start', 0);
$db->setQuery($query, $start, $limit);
try {
$items = $db->loadObjectList();
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
return $items;
}
/**
* Save a product
*/
public function save($data)
{
$table = $this->getTable();
$key = $table->getKeyName();
$pk = (!empty($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id');
$isNew = true;
try {
// Check for existing data
if ($pk > 0) {
$table->load($pk);
$isNew = false;
}
// Bind data
if (!$table->bind($data)) {
$this->setError($table->getError());
return false;
}
// Validate data
if (!$table->check()) {
$this->setError($table->getError());
return false;
}
// Store data
if (!$table->store()) {
$this->setError($table->getError());
return false;
}
// Update state
$this->setState($this->getName() . '.id', $table->$key);
} catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
return true;
}
/**
* Get table object
*/
public function getTable($name = 'Product', $prefix = 'ProductsTable', $options = [])
{
return parent::getTable($name, $prefix, $options);
}
}
6. API Integration
API Controller (src/Controller/ProductsController.php)
<?php
namespace My\Component\Products\Api\Controller;
use Joomla\CMS\MVC\Controller\ApiController;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
class ProductsController extends ApiController
{
protected $contentType = 'products';
protected $default_view = 'products';
/**
* Get product list (GET)
*/
public function displayList()
{
$model = $this->getModel('Products', 'Site');
// Set filter parameters
$app = $this->app;
$model->setState('filter.category_id', $app->input->get('category_id', 0, 'int'));
$model->setState('filter.search', $app->input->get('search', '', 'string'));
$model->setState('list.limit', $app->input->get('limit', 20, 'int'));
$model->setState('list.start', $app->input->get('offset', 0, 'int'));
$items = $model->getItems();
if ($items === false) {
throw new \Exception($model->getError(), 500);
}
// Prepare response
$data = [];
foreach ($items as $item) {
$data[] = $this->prepareItem($item);
}
return $data;
}
/**
* Get single product (GET)
*/
public function displayItem()
{
$id = $this->app->input->get('id', 0, 'int');
if (!$id) {
throw new \Exception('Invalid product ID', 400);
}
$model = $this->getModel('Product', 'Site');
$item = $model->getItem($id);
if (!$item) {
throw new \Exception('Product not found', 404);
}
return $this->prepareItem($item);
}
/**
* Create product (POST)
*/
public function add()
{
$this->checkToken();
$data = $this->input->get('data', [], 'array');
// Validate required fields
if (empty($data['title'])) {
throw new \Exception('Title is required', 400);
}
$model = $this->getModel('Product', 'Administrator');
if (!$model->save($data)) {
throw new \Exception($model->getError(), 500);
}
$id = $model->getState('product.id');
return [
'id' => $id,
'message' => 'Product created successfully'
];
}
/**
* Update product (PATCH)
*/
public function edit()
{
$this->checkToken();
$id = $this->app->input->get('id', 0, 'int');
if (!$id) {
throw new \Exception('Invalid product ID', 400);
}
$data = $this->input->get('data', [], 'array');
$data['id'] = $id;
$model = $this->getModel('Product', 'Administrator');
if (!$model->save($data)) {
throw new \Exception($model->getError(), 500);
}
return [
'id' => $id,
'message' => 'Product updated successfully'
];
}
/**
* Delete product (DELETE)
*/
public function delete()
{
$this->checkToken();
$id = $this->app->input->get('id', 0, 'int');
if (!$id) {
throw new \Exception('Invalid product ID', 400);
}
$model = $this->getModel('Product', 'Administrator');
if (!$model->delete($id)) {
throw new \Exception($model->getError(), 500);
}
return [
'message' => 'Product deleted successfully'
];
}
/**
* Prepare item for API
*/
protected function prepareItem($item)
{
return [
'id' => (int) $item->id,
'title' => $item->title,
'alias' => $item->alias,
'description' => $item->description,
'price' => (float) $item->price,
'sku' => $item->sku,
'created' => $item->created,
'modified' => $item->modified,
'hits' => (int) $item->hits
];
}
}