Joomla Introduction and Architecture

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

This page is the map of the whole Joomla sub-section. It explains what Joomla is, where it comes from, which programs make up a Joomla installation, what happens between an HTTP request and the HTML that comes back, and which building blocks (extensions, namespaces, the dependency injection container, events and MVC) every later page assumes. It was checked against the official documentation on 2026-10-30. The code and file names refer to Joomla 6.2 (the current release, requiring PHP 8.3 or newer; the entry points in the 6.2 source define JOOMLA_MINIMUM_PHP as 8.3.0), and the text says where Joomla 5.4 (security fixes only) behaves differently. Joomla 4.x and 3.x are end of life.

The examples belong to the Tallpine Hiking Club, a fictitious mountain club whose site grows throughout the sub-section: the package pkg_tallpine holds the component com_tallpine (namespace Tallpine\Component\Tallpine), the module mod_tallpine_nexthikes, several plugins and the child template cassiopeia_tallpine. Release dates and support windows are covered in Versions, Release Cycle and Support, so this page only places the major versions on a timeline.

What Joomla is

Joomla is a free, open source content management system written in PHP. It stores its data in a relational database (MySQL, MariaDB or PostgreSQL) and renders pages from templates. A fresh installation already contains the tools of a publishing site: articles in nested categories, menus, modules, users with groups and access levels, multilingual content, custom fields, tags, contacts, banners, news feeds, a media manager, Smart Search, scheduled tasks, a web services API and a command line interface. Everything beyond that is added with extensions.

Four groups of people use it, and this sub-section addresses each:

  • Editors and site builders create content, menus and modules in the Administrator and rarely read PHP.

  • Administrators install, update, secure and back up the site.

  • Template designers write or override layouts, CSS and JavaScript for the Site application.

  • Extension developers write components, modules, plugins and templates against the APIs described below.

Licence, Open Source Matters and the community

Joomla is licensed under the GNU General Public License, version 2 or later. The entry-point files in the source tree carry the notice "GNU General Public License version 2 or later" and the copyright of Open Source Matters, Inc. In practice this means:

  • You may use, modify and redistribute Joomla and run it commercially.

  • A distributed extension that links to Joomla’s PHP code is normally treated as a derivative work and is therefore also released under the GPL. Selling a GPL extension is allowed, and many commercial extension vendors do so; the restriction is on the source code, not on charging for support, updates or downloads.

  • The Joomla name and logo are trademarks of Open Source Matters, so this documentation does not reproduce the logo.

Open Source Matters, Inc. (OSM) is the non-profit organisation that provides the legal, organisational and financial resources of the project. It holds the trademarks and the project’s assets. The software itself is written by volunteers organised in teams, and the project is also supported by a large community of extension developers, translators, user group organisers and the people who run the events and the official magazine. The official documentation is now published in two places: the Joomla! User Guide (guide.joomla.org) for end users and the Joomla! Developer Manual (manual.joomla.org) for developers. The old wiki at docs.joomla.org is a legacy resource whose pages mark themselves obsolete, and several Manual pages (the lifecycle page, for example) are still stubs, so this page also reads the source tree on GitHub where the Manual says nothing.

Joomla has no built-in backup system and no built-in multisite mode. Backup extensions such as Akeeba Backup are third-party and belong to the ecosystem rather than to the core; see Backups and Recovery.

A short history

Joomla started as a fork of Mambo in August 2005, after the developers objected to the foundation structure set up by the company that owned the Mambo trademark. The name "Joomla!" is an English spelling of the Swahili word jumla, roughly "all together". The table lists the milestones that still matter for reading older material.

Version Released What changed

1.0

September 2005

The first Joomla release, a rebranded Mambo 4.5.2.3.

1.5

January 2008

A rewrite on a new framework with a real MVC layer, language packs and a proper extension API. Many tutorials with JFactory and JText code still date from this era.

2.5

January 2012

A long-term support release of the 1.x line, after which the project moved to a predictable cycle.

3.0

September 2012

Responsive back end built on Bootstrap and jQuery, followed by years of 3.x releases. Namespacing of core classes started gradually inside 3.x.

4.0

August 2021

A deep rewrite: namespaced PSR-4 classes, a dependency injection container, event dispatching based on the Joomla Framework, Bootstrap 5, the Atum administrator template and the Cassiopeia site template, the Web Asset Manager and the Web Services API.

5.0

October 2023

A cleanup release on a newer PHP baseline: concrete event classes, removal of the ES5 assets and of the reCAPTCHA plugin, and the start of the 5.x line that ends with the bridge release 5.4.

6.0

October 2025

Removes code deprecated in 4.x and 5.x, with most removals still reachable through the Behaviour - Backward Compatibility 6 plugin (BC6), and requires PHP 8.3.

6.2

October 2026

The current release at the check date.

Between those majors, 4.1 added child templates and scheduled tasks, and 4.2 introduced multi-factor authentication (MFA), which replaced the older two-factor authentication. Joomla 5.1 added signature verification of core updates with TUF, and Joomla 5.4 and 6.0 added Automated Core Updates. Version policies (a major every two years in October of odd years, four minors per major, x.4 as the bridge release) are explained in Versions, Release Cycle and Support.

The big picture

A Joomla installation is a single code tree that contains five independent programs, the applications. They share the framework libraries, the dependency injection container, the event dispatcher and the database, and they differ in how a request reaches them and in what they do with it. Extensions of eight types plug into the applications. The figure shows the layers that the rest of the page walks through.

Joomla architecture: five applications over the shared libraries, DI container and event dispatcher, extension types, database and media

The five applications

Each application has its own entry point and its own class. All of them extend from the Joomla Framework application classes and read their services from the same container.

Application Entry point Class Purpose

Site

index.php at the web root

Joomla\CMS\Application\SiteApplication

The public front end. Visitors, members and the template-rendered pages.

Administrator

administrator/index.php

Joomla\CMS\Application\AdministratorApplication

The back end, rendered with the Atum template.

API

api/index.php

Joomla\CMS\Application\ApiApplication

The Web Services API, answering in JSON:API and returning JSON, authenticated with tokens or Basic authentication.

Console

cli/joomla.php

Joomla\Console\Application

The command line application, started with php cli/joomla.php.

Installation

installation/index.php

Joomla\CMS\Installation\Application\InstallationApplication

The web installer. The installation folder is deleted after setup, so this application exists only on a site that has not been installed yet.

How an entry point starts an application

The web entry points are very short. They check the PHP version, define the guard constant _JEXEC and load includes/app.php of their own application. In the Site application that file does the following (the code below is shortened from the Joomla 6.2 source):

<?php
\defined('_JEXEC') or die;

$startTime = microtime(1);
$startMem  = memory_get_usage();

require_once __DIR__ . '/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

$container = \Joomla\CMS\Factory::getContainer();
// session aliases omitted

$app = $container->get(\Joomla\CMS\Application\SiteApplication::class);

\Joomla\CMS\Factory::$application = $app;

$app->execute();

The Administrator and Installation applications do the same with AdministratorApplication and InstallationApplication. The API entry point loads its own includes/app.php, and the console entry point skips that file and asks the container for the console application directly:

<?php
const _JEXEC = 1;
const JOOMLA_MINIMUM_PHP = '8.3.0';

require_once \dirname(__DIR__) . '/defines.php';
require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';

$app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Console\Application::class);
$app->execute();

Three things to notice:

  • defined('_JEXEC') or die; is the guard at the top of every PHP file that is not a class. It stops a file from running when someone requests it directly. Joomla 6.0 no longer defines JPATH_PLATFORM, so any extension code that guarded with that constant must switch to _JEXEC (the old constant remains only under BC6).

  • framework.php builds the container. The services the applications need (database, session, user factory, router, event dispatcher, language factory and so on) are registered as service providers there; the application class is then created by the container with its dependencies.

  • The entry point contains no logic. All behaviour lives in classes, which is what makes the applications testable and lets extensions replace services.

A site that uses the optional public folder (see Installation and the Public Folder) keeps the code outside the web root and exposes only a small set of entry files, but the applications and the lifecycle below are unchanged.

What each application is used for

  • The Site application serves everything a visitor sees. It parses the URL with SiteRouter, selects a menu item, runs the component named by that item, and wraps the result in the active template.

  • The Administrator application is similar but uses AdministratorRouter, no SEF rewriting and a different set of components (com_content in the back end is a list-and-edit interface, while the front end is a viewing interface). Plugins and modules also run here.

  • The API application uses ApiRouter. A web services plugin registers routes, most often with createCRUDRoutes(), and the authentication step is part of routing. It serves JSON:API and also supports non-JSON:API routes for custom formats. See Web Services API.

  • The Console application is based on the Symfony Console component through the Joomla Framework. Commands are contributed by console plugins; see The Command Line Interface and Console Command Plugins. Joomla 6.0 removed the CMS CliApplication base class, so a script that extended it must become a console plugin.

  • The Installation application walks through language, site name, Super User, database and the opt-out for automated core updates, then writes configuration.php.

Plugins are not separated by application. Installed and enabled plugins run in the Site, the Administrator and the API, so a plugin that should act in only one of them must test the application itself, for example with $this->getApplication()->isClient('site').

The request lifecycle

The Manual page called "The Lifecycle" is still marked unfinished, so the order below comes from the application classes in the source tree and from the Manual’s list of application events. The Site application is the reference.

Step by step

  1. Entry point and bootstrap. index.php loads includes/app.php, which loads includes/framework.php, creates the container and obtains SiteApplication.

  2. execute(). The shared CMSApplication::execute() checks the environment, sets up logging, builds the map of extension namespaces, imports the behaviour and system plugin groups and dispatches onBeforeExecute. Then it calls the subclass’s doExecute().

  3. Initialise. doExecute() calls initialiseApp(): the identity of the current user is read from the session, the language is chosen (guest default, browser or the language filter), and onAfterInitialise is dispatched. This is the first event most plugins see, and the earliest moment at which the router exists.

  4. Route. route() parses the URL with SiteRouter, finds the active menu item and copies the parsed variables (option, view, id, Itemid) into the input. An alias menu item with redirect enabled produces a 301 here. The system plugins then receive onAfterRoute, and the access level of the menu item is checked, which may redirect to the login page or throw a 403.

  5. Dispatch. dispatch() creates the document (HTML, JSON, feed, raw), loads the template parameters and the language files, registers the Web Asset Manager files, and dispatches onAfterInitialiseDocument. Then ComponentHelper::renderComponent() boots the component named by option, asks its extension class for a dispatcher and runs it. The component’s output is captured into a buffer, and onAfterDispatch is dispatched.

  6. Render. CMSApplication::render() parses the template, dispatches onBeforeRender, and then renders the template file: each <jdoc:include type="..."> tag is replaced by the component buffer, a module position, the message queue or the head. While the head is compiled, onBeforeCompileHead fires, which is the last chance to add scripts, styles and metadata. When the document is complete, onAfterRender is dispatched.

  7. Compress and respond. If gzip is enabled the body is compressed (onAfterCompress). Then onBeforeRespond is dispatched, the headers and body are sent, and onAfterRespond follows.

  8. Errors. An uncaught exception anywhere in execute() dispatches onError and is handed to the exception handler, which renders the error page.

The sequence diagram shows a visitor opening the page of an alpine trail report, in which a menu item of type "Category Blog" shows articles of Trails > Alpine and the page contains a module in the sidebar-right position.

sequenceDiagram autonumber participant V as Visitor participant E as index.php and includes/app.php participant A as SiteApplication participant P as System plugins participant R as SiteRouter participant D as Component dispatcher participant C as com_content participant T as Template (index.php) participant M as Modules V->>E: GET /index.php/trails/alpine E->>A: container get SiteApplication, execute A->>P: onBeforeExecute A->>A: initialiseApp (user, language) A->>P: onAfterInitialise A->>R: parse URL R-->>A: option=com_content, view=category, Itemid A->>P: onAfterRoute A->>A: create document, load template, assets A->>P: onAfterInitialiseDocument A->>D: renderComponent com_content D->>C: controller execute (DisplayController display) C-->>D: HTML for the category blog D-->>A: component buffer A->>P: onAfterDispatch A->>P: onBeforeRender A->>T: render template file T->>M: jdoc:include type=modules name=sidebar-right M-->>T: module output T-->>A: complete HTML A->>P: onBeforeCompileHead (while building head) A->>P: onAfterRender A->>P: onBeforeRespond A-->>V: HTTP response A->>P: onAfterRespond

The system events

The Manual lists these application events, each backed by a concrete event class in the Joomla\CMS\Event\Application namespace (the two extension events live in Joomla\CMS\Event). A plugin subscribes to them by event name.

Event Class When it fires

onAfterInitialise

AfterInitialiseEvent

After Joomla finished its initialisation, once the router is available.

onAfterRoute

AfterRouteEvent

After the request has been routed.

onAfterInitialiseDocument

AfterInitialiseDocumentEvent

After the document object has been initialised.

onAfterDispatch

AfterDispatchEvent

After the main component ran and its output is buffered.

onBeforeRender

BeforeRenderEvent

Just before the template file is processed to fill in the jdoc:include tags.

onBeforeCompileHead

BeforeCompileHeadEvent

While the metas tag is processed, to add items to the HTML head.

onAfterRender

AfterRenderEvent

After the output is generated, before the response is sent.

onBeforeRespond

BeforeRespondEvent

Just before the HTTP response is sent.

onAfterRespond

AfterRespondEvent

Just after the HTTP response is sent.

onBeforeExtensionBoot

Joomla\CMS\Event\BeforeExtensionBootEvent

Just before a plugin, module or component is booted for the first time.

onAfterExtensionBoot

Joomla\CMS\Event\AfterExtensionBootEvent

Just after an extension is booted for the first time.

The source code adds onBeforeExecute (dispatched by execute() before doExecute()), onAfterCompress and onError. Typical uses at Tallpine: a system plugin that adds a Content Security Policy in onAfterRender, one that redirects old trail URLs in onAfterRoute, and one that registers extra assets in onBeforeCompileHead. Chosen carelessly, work done in onAfterInitialise runs on every request including cached pages and the API, so it must stay cheap.

How the API and Console lifecycles differ

The API application follows the same outline with a different route step. Its route() imports the webservices plugin group and dispatches onBeforeApiRoute, which is where plg_webservices_tallpine registers its routes; it negotiates the response format from the Accept header, dispatches onAfterApiRoute, and then authenticates. Unless a route is marked public, authentication calls login() with the action core.login.api, and a failure throws an authentication error. Authentication is therefore part of routing and not a separate step. Dispatch then runs the component’s controller, whose view renders JSON:API. Fresh Joomla 6.2 installations have the API token plugin enabled and the Basic Auth API plugin disabled.

The Console application does not route a URL. It collects its commands (core commands and those contributed by plugins of the console group), runs one command and exits with a status code, which is why a command such as php cli/joomla.php tallpine:import can be used in cron jobs.

Common lifecycle pitfalls

  • Reading the user too early. Before onAfterInitialise the session user is not available; code that runs in a constructor of a system plugin should read it in an event instead.

  • Echoing output from a plugin. Output sent before onBeforeRespond corrupts the headers and the page; return data or set it on the document.

  • Assuming the Site lifecycle in the API. There is no template, no module position and no menu item in an API request.

  • Calling the removed JFactory::getApplication()->route() style APIs. CMSApplication::route() is deprecated since 4.0 and will be removed in 7.0; the subclasses implement their own routing.

Extension types

Everything that is not the core is an extension. An extension is a folder tree with an XML manifest that tells the installer what to copy and register. The type attribute of the manifest root names one of eight types.

Type What it is Tallpine example

component

A full application area with its own MVC code, administrator interface, site views, database tables and API endpoints. It is the main content of a page.

com_tallpine with a trail entity

module

A small block shown in a template position, often reusing a component’s data. It has a site or an administrator client.

mod_tallpine_nexthikes

plugin

Code that subscribes to events, grouped by type (system, content, user, webservices, task, console, and so on).

plg_content_tallpine_conditions

template

The page frame for the Site or the Administrator, with layouts, overrides, positions and assets.

cassiopeia_tallpine, a child of Cassiopeia

language

A translation pack for the Site, the Administrator or both.

German (de-DE)

library

Shared PHP code outside any other extension.

A small helper library

package

A manifest-only extension that installs a list of other extensions together and removes them together.

pkg_tallpine

file

A set of loose files copied to chosen locations in the tree.

Rarely needed

The manifest root element is always <extension>. Its method attribute is install by default, which stops the installer if files already exist, or upgrade, which allows installing a newer version over the old one. A minimal manifest for the Tallpine component shows the namespace element that ties the folder tree to PHP classes:

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" method="upgrade">
    <name>com_tallpine</name>
    <version>1.0.0</version>
    <description>COM_TALLPINE_XML_DESCRIPTION</description>
    <namespace path="src">Tallpine\Component\Tallpine</namespace>

    <administration>
        <menu>COM_TALLPINE</menu>
        <files folder="administrator/components/com_tallpine">
            <folder>forms</folder>
            <folder>services</folder>
            <folder>sql</folder>
            <folder>src</folder>
            <folder>tmpl</folder>
        </files>
    </administration>

    <files folder="components/com_tallpine">
        <folder>src</folder>
        <folder>tmpl</folder>
    </files>
</extension>

Packaging, install scripts, SQL files and update servers have their own pages: Extension Types, Manifests and Packaging. The component, module and plugin guides are Component MVC and the Administrator, Module Development and Plugin Development and Events.

Where the files go

After installation the folders are scattered over the standard directories. The administrator and site parts of a component have separate folders, and the API part has its own:

Part Directory

Component, administrator part

administrator/components/com_tallpine

Component, site part

components/com_tallpine

Component, API part

api/components/com_tallpine

Site module

modules/mod_tallpine_nexthikes

Administrator module

administrator/modules/mod_name

Plugin

plugins/<type>/<name>

Site template

templates/cassiopeia_tallpine

Administrator template

administrator/templates/atum

Language pack

language/de-DE and administrator/language/de-DE

Library

libraries/<name>

Assets of an extension

media/com_tallpine, media/mod_tallpine_nexthikes, media/plg_content_tallpine_conditions

Namespaces and PSR-4 autoloading

Joomla 4 namespaced nearly all core classes and expected extensions to follow. Two mechanisms find a class from its name: PSR-4 for components, modules and plugins, and a Composer class map for the libraries.

Namespace prefixes

The Manual documents these prefixes for the core:

Kind Prefix Directory

Component, administrator

Joomla\Component\<Name>\Administrator\

administrator/components/com_<name>/src

Component, site

Joomla\Component\<Name>\Site\

components/com_<name>/src

Component, API

Joomla\Component\<Name>\Api\

api/components/com_<name>/src

Module, site

Joomla\Module\<Name>\Site\

modules/mod_<name>/src

Module, administrator

Joomla\Module\<Name>\Administrator\

administrator/modules/mod_<name>/src

Plugin

Joomla\Plugin\<Type>\<Name>\

plugins/<type>/<name>/src

CMS library

Joomla\CMS\

libraries/src

Framework and other libraries

Joomla\<Something>\

libraries/vendor/<something>/src

Third-party extensions use the same shape with their own vendor segment in place of Joomla. For Tallpine, the manifest line <namespace path="src">Tallpine\Component\Tallpine</namespace> yields these prefixes:

  • Tallpine\Component\Tallpine\Administrator maps to administrator/components/com_tallpine/src

  • Tallpine\Component\Tallpine\Site maps to components/com_tallpine/src

  • Tallpine\Component\Tallpine\Api maps to api/components/com_tallpine/src

A module declares Tallpine\Module\NextHikes and gets Tallpine\Module\NextHikes\Site for modules/mod_tallpine_nexthikes/src. A plugin needs an extra segment for its group, for instance Tallpine\Plugin\Content\TallpineConditions, which maps to the src folder of the plugin directory under plugins/content.

Naming rules from the Manual: the vendor segment can be any name; components use the segment Component, modules Module, and plugins Plugin followed by the plugin group; the final segment should match the extension name; and capitalisation must match directory and file names exactly. A Windows laptop forgives a wrong case but a Linux server does not, so an extension that works on a developer machine can fail on the production server with a class-not-found error. Joomla does not validate the namespace during installation.

How a class name becomes a file

PSR-4 lookup compares the fully qualified class name with the list of registered prefixes, from the left. The prefix is removed and the remainder, with namespace separators turned into directory separators, is appended to the prefix’s directory. For example Tallpine\Component\Tallpine\Administrator\View\Trails\HtmlView becomes administrator/components/com_tallpine/src/View/Trails/HtmlView.php.

The loader is JLoader::loadByPsr, registered with spl_autoload_register in libraries/loader.php. The prefix map is not hard-coded. When the application initialises, Joomla scans the component, module and plugin directories of the site and the administrator, reads the <namespace> element of every manifest, and builds the prefix-to-directory map, which it caches in administrator/cache/autoload_psr4.php. That file also lists the prefixes of the installed extensions, which makes it the first thing to read when a class is not found. Library classes are not found by PSR-4; they use the Composer class map at libraries/vendor/composer/autoload_classmap.php.

Form XML needs a hint because XML has no namespace statement. A custom field or validation rule class is named on the <form> element with addfieldprefix and addruleprefix:

<form addfieldprefix="Tallpine\Component\Tallpine\Administrator\Field"
      addruleprefix="Tallpine\Component\Tallpine\Administrator\Rule">
    <field name="difficulty" type="TrailDifficulty" label="COM_TALLPINE_FIELD_DIFFICULTY_LABEL"/>
</form>

Common mistakes with namespaces:

  • Wrong case in a folder or file name. src/Model/trailsModel.php is not found for the class TrailsModel on Linux.

  • Edited the manifest by hand on a live site. The cached autoload_psr4.php can be stale; reinstall or discover the extension from the Administrator’s Extensions > Discover screen, or delete the cache file so that it is rebuilt.

  • Using the legacy J class names. The old global names (JFactory, JText, JModelAdmin) came from the class-alias map in libraries/classmap.php. In Joomla 6.0 that mapping is no longer loaded by default, and the roughly 450 aliases live in the Backward Compatibility 6 plugin, which is off on new installations. Import the real classes instead: Joomla\CMS\Factory, Joomla\CMS\Language\Text and so on.

See Namespaces, Autoloading and Dependency Injection for the deeper treatment.

The dependency injection container

Joomla 4 introduced dependency injection "to try to enable better testing of the Joomla code". Instead of calling JoomlaClass::getInstance() from deep inside a method, code asks a container for an instance, or receives one through its constructor. A test then registers a mock in the container in place of the real class.

The container is Joomla\DI\Container from the Joomla Framework. It is a repository of key-value pairs:

  • set($key, $value, $shared, $protected) stores an entry. The key is usually a class or interface name; the value is either an instance or a closure that creates one. A protected entry raises an error if it is set again.

  • share() is set() with shared enabled, so every get() returns the same instance. With a non-shared entry, every get() builds a new object.

  • get($key) runs the closure if needed and returns the instance. Aliases let one entry be reached under several names, and lazy() wraps a class in a PHP lazy proxy so that rarely used plugins are only built when first used.

  • registerServiceProvider() hands the container to a service provider, an object implementing Joomla\DI\ServiceProviderInterface whose register(Container $container) method adds entries.

The keys are strings. Using a class or interface name is a convention, which is why code that stores an object under MyInterface::class must retrieve it with the same key.

Child containers and services/provider.php

Each time Joomla boots an extension it creates a child container for that extension. set() writes to the child. get() looks in the child first and falls back to the parent, the main application container. This gives each extension its own private services while it can still use the core ones.

An extension describes its services in a file called services/provider.php, which returns a service provider. Loading an extension is two steps: the provider registers an extension class in the child container, and Joomla then gets that class from the container, which creates the instance. If the class implements BootableExtensionInterface, Joomla calls boot($container) immediately. The code in libraries/src/Extension/ExtensionManagerTrait.php does this for components, modules and plugins alike.

The provider for the Tallpine component registers the three factories the component needs and the extension class:

<?php

\defined('_JEXEC') or die;

use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Tallpine\Component\Tallpine\Administrator\Extension\TallpineComponent;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->registerServiceProvider(new MVCFactory('\\Tallpine\\Component\\Tallpine'));
        $container->registerServiceProvider(new ComponentDispatcherFactory('\\Tallpine\\Component\\Tallpine'));
        $container->registerServiceProvider(new RouterFactory('\\Tallpine\\Component\\Tallpine'));

        $container->set(
            ComponentInterface::class,
            function (Container $container) {
                $component = new TallpineComponent(
                    $container->get(ComponentDispatcherFactoryInterface::class)
                );
                $component->setMVCFactory($container->get(MVCFactoryInterface::class));
                $component->setRouterFactory($container->get(RouterFactoryInterface::class));

                return $component;
            }
        );
    }
};

Common errors with providers: a provider that does not return the anonymous class (it must end with return new class implements ServiceProviderInterface), a typo in the factory namespace so the MVC factory builds the wrong class names, and registering an entry under a key the consumer never asks for. Providers run on every boot of the extension, so heavy work belongs in the closure, which is only executed when the service is requested.

The core extensions in the Joomla CMS repository, such as the services/provider.php file of com_content, are the best templates to compare against.

Extension classes and dispatchers

Other parts of Joomla (the router, com_categories, com_fields, com_associations) talk to a component through its extension class, not through helper files as in Joomla 3. Code obtains it from the application:

$extension = $app->bootComponent('com_tallpine');

Joomla caches the instance after the first call, so later bootComponent() calls return the same object without instantiating it or calling boot() again. The extension class normally extends MVCComponent and implements BootableExtensionInterface and RouterServiceInterface:

<?php

namespace Tallpine\Component\Tallpine\Administrator\Extension;

\defined('_JEXEC') or die;

use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
use Psr\Container\ContainerInterface;

class TallpineComponent extends MVCComponent implements
    BootableExtensionInterface,
    RouterServiceInterface
{
    use RouterServiceTrait;
    use HTMLRegistryAwareTrait;

    public function boot(ContainerInterface $container): void
    {
        // Register helper classes, keep a reference to the child container.
    }
}

The dispatcher runs the component. The extension supplies it through getDispatcher(), and the application calls $extension->getDispatcher()->dispatch(). The standard ComponentDispatcher in libraries/src/Dispatcher does this:

  1. It decides which controller to use from the task request parameter, written as <controller>.<method>.

  2. It creates the controller through the component’s MVCFactory, using the first part as the name and the current client (site, administrator or api) as prefix.

  3. It calls the controller’s execute() method.

  4. It calls the controller’s redirect() method, which sends any pending HTTP redirect.

When task is missing or incomplete, display is the default: the class DisplayController is instantiated and its display() method runs. So ?option=com_tallpine&task=trail.save runs the method save() of TrailController, while ?option=com_tallpine&view=trails runs DisplayController::display() and loads the view named in the request. A custom dispatcher is only needed when the component must run checks before anything else, such as an extra access rule or a redirect for a missing parameter. For modules and plugins the same extension-and-dispatcher idea exists; the module version is covered in Module Development.

The event dispatcher and plugins

Events are Joomla’s extension mechanism. A plugin belongs to a group (the subfolder of plugins/), subscribes to named events and reacts when they are dispatched. The mechanism is the Joomla Framework event dispatcher (Joomla\Event\DispatcherInterface) with CMS-specific event classes.

Loading is a two-step process:

  1. PluginHelper::importPlugin('system') loads the enabled plugins of one group. For each plugin Joomla finds the database record, runs its services/provider.php and reads which events the plugin subscribes to. The subscriptions are registered with the dispatcher.

  2. The application or a component dispatches an event, and the dispatcher calls the matching method on each subscribed plugin, in priority order.

Importing by group keeps requests fast: plugins in a group that nothing triggers are never built. Once imported, a plugin continues to receive every event it subscribes to, even events normally triggered for other groups.

Since Joomla 4 the recommended plugin type implements Joomla\Event\SubscriberInterface and declares its events in getSubscribedEvents(). Since Joomla 5.0 handlers receive a concrete event class with typed getters instead of a generic event with an argument array. A Tallpine content plugin that adds the trail difficulty to article text looks like this:

<?php

namespace Tallpine\Plugin\Content\TallpineConditions\Extension;

\defined('_JEXEC') or die;

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

final class TallpineConditions extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'onContentPrepare' => 'onContentPrepare',
        ];
    }

    public function onContentPrepare(ContentPrepareEvent $event): void
    {
        if ($this->getApplication()->isClient('administrator')) {
            return;
        }

        $item = $event->getItem();

        if (!isset($item->text)) {
            return;
        }

        $item->text = str_replace('{difficulty}', 'Moderate', $item->text);
    }
}

Notes on this style:

  • The application and the database come from the Aware traits (getApplication(), getDatabase()) and no longer from protected $app and $db properties. Joomla 7.0 removes the auto-detection of those two properties, so a plugin that still reads $this->app must be converted now.

  • Core plugin classes are final; a third-party plugin that extends one will break.

  • Captcha, editor and editors-xtd plugins use on...Setup events.

  • Code that calls $event->getArgument('subject') and indexes an array is the Joomla 4 style. It still works on legacy events but concrete event classes are the supported route.

The details, including task, webservices and console plugins, are in Plugin Development and Events and Scheduled Task Plugins.

MVC in a component

Joomla components use a model-view-controller layout. In the Manual’s words: the controller checks permissions and chooses a model, a view or a redirect; the model encapsulates the data and sets up the form; a table class maps one database table and provides CRUD for single records; the view has a view class and a layout (tmpl) file; and the layout runs in the context of the view, which is what makes layout overrides in templates possible.

The MVCFactory creates the controllers, views, models and tables of a component. Each component has its own factory, built with the component’s namespace prefix. With the default naming and view=trails the classes are DisplayController, View\Trails\HtmlView, TrailsModel and TrailsTable, all under the component namespace in src. A Tallpine administrator list view therefore has this structure:

administrator/components/com_tallpine/
  forms/trail.xml
  services/provider.php
  sql/install.mysql.utf8.sql
  src/
    Controller/DisplayController.php
    Controller/TrailController.php
    Extension/TallpineComponent.php
    Model/TrailsModel.php
    Model/TrailModel.php
    Table/TrailsTable.php
    View/Trails/HtmlView.php
    View/Trail/HtmlView.php
  tmpl/
    trails/default.php
    trail/edit.php

Controllers extend Joomla\CMS\MVC\Controller\BaseController (or FormController and AdminController for edit and list screens). Views extend Joomla\CMS\MVC\View\HtmlView; the display() method selects the layout and includes it. The controller gives a view its model with setModel(), and a table cannot reach the MVC factory. The full set of screens, forms and the router is built in Component MVC and the Administrator and Component Site Views and Routing.

Putting it together: one Tallpine request

A visitor opens /index.php/trails/alpine/ridge-traverse, a trail report in the category Trails > Alpine:

  1. The web server runs index.php, which obtains SiteApplication from the container and calls execute().

  2. execute() builds the extension namespace map, imports the behaviour and system plugins and runs initialiseApp(); onAfterInitialise lets the Remember Me plugin log in a returning member.

  3. SiteRouter parses the URL. The "Category Blog" menu item for Trails > Alpine is found and the request becomes option=com_content&view=article&id=.... onAfterRoute runs and the access level of the menu item is checked.

  4. The Cassiopeia child template cassiopeia_tallpine is selected from the template style assigned to the menu item.

  5. dispatch() boots com_content, whose extension class returns its dispatcher; the controller and the view build the article. The content plugins run: plg_content_tallpine_conditions subscribes to onContentPrepare and adds the difficulty from the article’s custom fields.

  6. The template file is rendered. mod_tallpine_nexthikes appears in the sidebar-right position; its dispatcher asks a helper for the next three hikes.

  7. The head is compiled and the response is sent.

The same data is reachable from the headless trail map: a request to /api/index.php/v1/tallpine/trails goes through the API application, the plg_webservices_tallpine routes, token authentication and the com_tallpine API controller, and returns JSON:API.

Security notes for the architecture

  • Keep configuration.php and the cache, tmp and logs folders unreadable from the web. The optional public folder makes this structural; without it, the web server rules from htaccess.txt and the hosting configuration do the job.

  • The _JEXEC guard in every non-class file is not a security feature by itself but a defence in depth; class files are safe because they only define classes.

  • Authentication on the API happens in routing, so a route registered as public is reachable without a token. Register routes as public only when the data is open to everybody.

  • Automated Core Updates (introduced in 5.4 and 6.0) are enabled by default only for new installations, and need a publicly reachable site. Core updates have been verified with TUF since 5.1.

  • A system plugin runs on every request in all three web applications, which makes it the highest-impact kind of extension to review.

Version notes

  • Joomla 6.0 removes the CMS classes Joomla\CMS\Input (use Joomla\Input), BaseApplication and CliApplication (use console plugins), moves Joomla\CMS\Filesystem into BC6 for removal in 7.0, and makes AdminModel::getItem() return stdClass instead of CMSObject. Views also lose $this->db. Everything marked as BC6 is available only while the Behaviour - Backward Compatibility 6 plugin is enabled.

  • Deprecated code is removed in the second major after deprecation, and minimum requirements only rise in the first release of a major. Extensions written for 6.x should therefore watch deprecation notices that name 7.0 or 8.0.

  • Error handling: use exceptions instead of setError() (setUseExceptions(true) since 5.4); core stops calling setError() in 7.0 and the trait goes in 8.0. See Error Handling and Legacy APIs.

  • Joomla’s tests use Cypress for the system tests, and the coding style has followed PSR-12 since 4.2.

Where older books and tutorials differ

The two books used while preparing this sub-section target platforms that are no longer current.

  • Developing Extensions for Joomla! 5 (Packt, 2023) is the best development reference for the namespaced MVC, services/provider.php and the Web Services API. Some details predate the current guidance. A non-namespaced CMSPlugin webservices plugin with onBeforeApiRoute(&$router) is replaced by a SubscriberInterface plugin that receives a BeforeApiRouteEvent. Code that reads $db as a plugin property, calls Event::getArgument() for every event, or uses Factory::getLanguage() is superseded by getDatabase(), concrete event classes and the container. An installer script that extends nothing is superseded by InstallerScriptInterface and InstallerScriptTrait. The claim that the query builder translates queries for every database does not match the official guidance. The book also describes Codeception as the core test tool, while the core now uses Cypress.

  • Joomla! 4 Masterclass (Packt, 2023) is a strong guide to using the Administrator, but it targets Joomla 4, which reached its end of life in October 2025, and it quotes a market share of 2.7%, whereas W3Techs reported about 1.1% in October 2026 (see CMS Comparison). Statements such as "child templates are brand new" and jQuery as a core dependency no longer hold, and links to docs.joomla.org/J4.x or launch.joomla.org have been replaced by the User Guide and the Developer Manual.

The page Where Older Books and Tutorials Are Outdated collects these corrections for the whole sub-section, and Upgrading Joomla 5 to 6 explains the migration path.

Common errors and how to read them

Symptom Likely cause Fix

Class "Tallpine\Component\Tallpine\..." not found

Wrong namespace in the manifest, wrong case, or a stale autoload_psr4.php

Compare the namespace with the folder, fix the case, reinstall or remove the cache file.

Class "JFactory" not found after updating to 6

Legacy J aliases are no longer loaded by default

Import Joomla\CMS\Factory, or temporarily enable BC6 while migrating.

A plugin runs in the API and breaks it

System plugins run in all web applications

Check isClient() and return early.

Call to undefined method ... getApplication()

The plugin was created without the application aware trait or provider

Use the provider pattern and CMSPlugin's application support.

Blank page after a manual manifest edit

The namespace map is cached; PHP errors are hidden

Enable error reporting in Global Configuration while debugging; see Debugging.

Extension installs but its component shows a 404

No services/provider.php, or the dispatcher factory uses the wrong namespace

Compare the provider with the one above.

What to read next