Joomla Namespaces, Autoloading and Dependency Injection

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.

Every Joomla extension written since Joomla 4 is made of namespaced PHP classes that Joomla finds through a generated map, and instantiates through a dependency injection container. Those two mechanisms, namespaces with autoloading on one side and the container with its services/provider.php files on the other, are what turn a folder of files into a component, a module or a plugin. This page explains both in depth: the namespace prefixes that core and third-party extensions use, the namespace element of the manifest, how a class name becomes a file path under PSR-4, the autoload_psr4.php file Joomla generates and the moments when it is rebuilt, the container and its child containers, the services/provider.php file of components, modules and plugins, the service factories that core provides (MVCFactory, ComponentDispatcherFactory, RouterFactory, CategoryFactory, ModuleDispatcherFactory, HelperFactory), and the reason Factory::getDbo()-style static calls are discouraged, with the replacements to use for the application, the database, the user and the language.

It was written against Joomla 6.2 (current, PHP 8.3 or newer) and checked against the Joomla Developer Manual and the 6.2 source tree on 2026-10-30. Joomla 5.4 receives security fixes only; where it differs the text says so. Joomla 4.x and 3.x are end of life. The examples belong to the Tallpine Hiking Club, the fictitious mountain club used across this sub-section, whose package pkg_tallpine holds the component com_tallpine (namespace Tallpine\Component\Tallpine), the module mod_tallpine_nexthikes and the content plugin plg_content_tallpine_conditions. The sibling pages build on this one: Extension types, manifests and packaging shows the whole manifest, Component MVC: administrator, Module development and Plugin development and events fill in the classes that the providers on this page register, and Database API covers what you do with the database once you have it. General PHP namespace syntax and Composer autoloading are not repeated here; they are in Namespaces and autoloading and Composer and packages.

Why Joomla uses namespaces and a container

Until Joomla 3.x, class names were global and prefixed by convention: JFactory, JModelAdmin, ContentModelArticle, PlgContentJoomla. The legacy JLoader found them by splitting the name on capitals. Namespaces were introduced gradually during the 3.x series, and by Joomla 4 nearly every core class was namespaced and extensions were expected to follow. The benefits are the usual ones: no collisions between extensions, class names that tell you which extension and which side (site, administrator, API) they belong to, and a standard autoloading rule (PSR-4) that editors, static analysers and Composer understand.

The container solves a different problem: where does a class get the other objects it needs? In Joomla 3 the answer was a static call, JFactory::getDbo(), JFactory::getUser(), JFactory::getApplication(), written wherever the object was needed. That is convenient to type and hard to test, because a unit test cannot replace the real database with a fake without global state tricks. Joomla 4 introduced the Dependency Injection Container (DIC) of the Joomla Framework package joomla/di: code asks the container for an object by key (usually an interface name) instead of building it, and tests register a fake under the same key. The Developer Manual states the aim directly: easier testing through easier mocking.

Who touches this? The extension developer writes the namespace, the provider and the classes. The administrator or site builder meets it as the occasional fatal error "Class not found" after copying files by hand, or as an extension that works on a laptop and fails on a Linux server. The themer meets it less, but templates also declare a namespace (see Building a template from scratch), and the introduction to Joomla architecture places both mechanisms in the request lifecycle.

Namespace prefixes

A namespace prefix is the leading part of a fully qualified class name (FQN) that Joomla maps to one directory. The first column of the table is the prefix pattern, the second the folder that holds the classes for it.

Extension type Prefix pattern Maps to

Component, administrator side

Vendor\Component\Name\Administrator

administrator/components/com_name/src

Component, site side

Vendor\Component\Name\Site

components/com_name/src

Component, Web Services API side

Vendor\Component\Name\Api

api/components/com_name/src

Module, site

Vendor\Module\Name\Site

modules/mod_name/src

Module, administrator

Vendor\Module\Name\Administrator

administrator/modules/mod_name/src

Plugin

Vendor\Plugin\Group\Name

plugins/group/name/src

Template

Vendor\Template\Name plus Site or Administrator

templates/name/src or administrator/templates/name/src

Library

the namespace of the library manifest

a folder under libraries/

For core extensions the vendor segment is Joomla: Joomla\Component\Content\Administrator\View\Articles\HtmlView lives in administrator/components/com_content/src/View/Articles/HtmlView.php, Joomla\Module\Breadcrumbs\Site\Helper in modules/mod_breadcrumbs/src/Helper, and Joomla\Plugin\Fields\Calendar in plugins/fields/calendar/src. A third-party extension uses its own vendor name. The Tallpine extensions use Tallpine:

  • 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

  • Tallpine\Module\NextHikes\Site maps to modules/mod_tallpine_nexthikes/src

  • Tallpine\Plugin\Content\TallpineConditions maps to plugins/content/tallpine_conditions/src

The vendor segment is free (usually the company or project), the type segment (Component, Module, Plugin, Template) is required, and the name segment should match the extension name. Core classes outside extensions follow two further rules: Joomla\CMS maps to libraries/src, and Joomla\<Other> maps to libraries/vendor/<other>/src, which is where the Joomla Framework packages live (Joomla\Event is in libraries/vendor/joomla/event/src, Joomla\DI in libraries/vendor/joomla/di/src). Names that do not start with Joomla come from other directories under libraries/vendor/: third-party packages such as PHPMailer, Symfony components or Laminas libraries, loaded through Composer’s own class map and PSR-4 entries. The mapping rules above are the general standard and the Manual notes that exceptions exist, so the authoritative list for a given site is the generated map described below.

Reading a fully qualified class name

A class name tells you the extension, the side and the role. Take Tallpine\Component\Tallpine\Administrator\Controller\TrailController:

  • Tallpine is the vendor

  • Component\Tallpine says it belongs to com_tallpine

  • Administrator says it is loaded for the back end only

  • Controller\TrailController is the path under src (Controller/TrailController.php) and the role

Because the site and administrator trees have distinct FQNs, a site class and an administrator class can have the same short name and live together. A site model can even extend its administrator counterpart with an alias:

<?php

namespace Tallpine\Component\Tallpine\Site\Model;

use Tallpine\Component\Tallpine\Administrator\Model\TrailModel as AdministratorTrailModel;

\defined('_JEXEC') or die;

/**
 * The site trail model reuses the administrator model's form and table handling.
 */
class TrailModel extends AdministratorTrailModel
{
}

Several short names recur in core and are easy to confuse, so check the use statement:

  • Registry is either Joomla\Registry\Registry (a data structure, the one you want for parameters) or Joomla\CMS\HTML\Registry (the registry of HTML helper services)

  • CategoryFactory is either the factory Joomla\CMS\Categories\CategoryFactory or the service provider Joomla\CMS\Extension\Service\Provider\CategoryFactory that registers it (the same applies to MVCFactory, RouterFactory, HelperFactory and the two dispatcher factories)

  • DispatcherInterface is Joomla\CMS\Dispatcher\DispatcherInterface for component and module dispatchers, and Joomla\Event\DispatcherInterface for the event dispatcher that plugins use

Declaring a namespace in the manifest

An extension tells Joomla its namespace with a namespace element in its manifest. The path attribute is the folder (relative to the extension folder) that holds the classes:

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

    <administration>
        <files folder="administrator">
            <folder>forms</folder>
            <folder>services</folder>
            <folder>src</folder>
            <folder>tmpl</folder>
            <file>access.xml</file>
            <file>config.xml</file>
        </files>
    </administration>

    <files folder="site">
        <folder>src</folder>
        <folder>tmpl</folder>
    </files>
</extension>

Note what is not in the manifest: the Site, Administrator and Api suffixes. Joomla adds them itself, so the manifest declares only Tallpine\Component\Tallpine. A module declares Tallpine\Module\NextHikes and a plugin declares the group and name (Tallpine\Plugin\Content\TallpineConditions), because a plugin has only one tree.

Form prefixes: the one place XML needs a namespace

XML has no use statements, so a form definition that references a custom field type or validation rule written as a class must give the namespace explicitly with addfieldprefix and addruleprefix on the form element:

<form addfieldprefix="Tallpine\Component\Tallpine\Administrator\Field"
      addruleprefix="Tallpine\Component\Tallpine\Administrator\Rule">
    <fieldset name="trail">
        <field name="difficulty" type="TrailDifficulty" label="COM_TALLPINE_FIELD_DIFFICULTY" />
        <field name="gpx_file" type="media" label="COM_TALLPINE_FIELD_GPX" validate="GpxFile" />
    </fieldset>
</form>

The field class TrailDifficultyField is then found in administrator/components/com_tallpine/src/Field. The forms themselves are covered in Forms and form fields.

How a class name becomes a file path

PSR-4 is a three-step rule that Joomla applies through the map:

  1. Match the longest registered prefix against the start of the class name. For Tallpine\Component\Tallpine\Administrator\View\Trails\HtmlView the prefix is Tallpine\Component\Tallpine\Administrator\.

  2. Look up the base directory of that prefix: administrator/components/com_tallpine/src.

  3. Replace the remaining namespace separators with directory separators and append .php.

Class      Tallpine\Component\Tallpine\Administrator\View\Trails\HtmlView
Prefix     Tallpine\Component\Tallpine\Administrator\
Base dir   administrator/components/com_tallpine/src
Remainder  View\Trails\HtmlView
File       administrator/components/com_tallpine/src/View/Trails/HtmlView.php

The file must contain exactly one class whose name equals the file name, and the folder layout under src is yours: the directory names Controller, Model, View, Table, Extension, Dispatcher, Service, Helper, Field and Rule are only conventions, but several of them are looked up by name by the factories described later, so keep them.

Pitfalls with namespaces

  • Capitalisation. The namespace, the directory names and the file names must match exactly. Windows and the default macOS file system ignore case, Linux does not, so an extension with Models/TrailModel.php declared as ...\Model\TrailModel works on a laptop and throws "Class not found" on the production server.

  • No install-time check. Joomla does not validate the namespace or the layout while installing. A wrong path, a misspelled vendor or a missing src folder shows up only when the class is first needed.

  • The manifest must be where the mapper looks. The map is built from the installed manifests (see the next section), not from your source repository. For a component Joomla reads administrator/components/com_tallpine/tallpine.xml (or com_tallpine.xml), so the manifest has to be copied there by the installer. Hand-copying only the src folder leaves the extension without a namespace.

  • The global namespace. Inside a namespaced file, Exception means Tallpine\...\Exception. Write \Exception, \stdClass and, for speed, \strlen(); core files do this consistently.

  • The first character. Do not start a namespace segment with a digit or an unusual character; use letters.

  • Do not put your classes in a core namespace. A third-party class in Joomla\... is silently wrong when the core one of the same name appears in a later release.

The generated map: autoload_psr4.php

Joomla does not hard-code the prefixes of extensions. Instead, when an application starts, it builds (or loads) one file that lists every prefix and folder:

  • the class JNamespacePsr4Map (file libraries/namespacemap.php) scans the installed components, modules, templates, plugins and libraries

  • for each extension it opens the installed manifest, reads the namespace element and its path attribute, and computes the prefixes: Site, Administrator and Api for components (each only if the folder exists), Site or Administrator for modules and templates, none for plugins and libraries

  • it sorts the result and writes it to administrator/cache/autoload_psr4.php (the constant JPATH_CACHE points to administrator/cache for all three client applications)

  • on every request CMSApplication::execute() calls createExtensionNamespaceMap(), which loads the file and registers each prefix with Composer’s class loader (setPsr4()); the console application and the installer application do the same

The file is plain PHP that returns an array. For a site with the Tallpine package installed, the relevant lines look like this (the real file is much longer and also lists every core extension):

<?php
defined('_JEXEC') or die;
return [
	'Tallpine\\Component\\Tallpine\\Administrator\\' => [JPATH_ADMINISTRATOR . '/components/com_tallpine/src'],
	'Tallpine\\Component\\Tallpine\\Api\\' => [JPATH_API . '/components/com_tallpine/src'],
	'Tallpine\\Component\\Tallpine\\Site\\' => [JPATH_SITE . '/components/com_tallpine/src'],
	'Tallpine\\Module\\NextHikes\\Site\\' => [JPATH_SITE . '/modules/mod_tallpine_nexthikes/src'],
	'Tallpine\\Plugin\\Content\\TallpineConditions\\' => [JPATH_PLUGINS . '/content/tallpine_conditions/src'],
];

Open this file whenever a class is "not found": if your prefix is missing, the map is stale or the manifest has no namespace element; if the prefix is there, the problem is the path or the capitalisation.

When the map is rebuilt

The file is a cache, and the cost of rebuilding it is a scan of every extension folder, so Joomla rebuilds it only when something may have changed:

  • The file does not exist. JNamespacePsr4Map::load() creates it on the spot. Deleting the file is therefore the supported way to force a rebuild.

  • An extension is installed, updated or uninstalled. The plugin Extension - Namespace Map (plg_extension_namespacemap, in the extension plugin group) listens to onExtensionAfterInstall, onExtensionAfterUpdate and onExtensionAfterUninstall and calls create(). Do not disable that plugin: with it off a newly installed extension is not mapped until the file is deleted by hand.

  • Joomla itself is updated. The core update (the Joomla Update component, its extraction and finalisation steps, and the update:core console command) deletes administrator/cache/autoload_psr4.php so the next request regenerates it, because the set of core namespaces may have changed.

  • You copy files by hand. Unpacking an extension into the web root, git pull on a development site, or a Docker bind-mount that shares a src folder do not fire any event. Delete the file yourself.

# Docker Compose development site (service name "joomla", Joomla in /var/www/html)
docker compose exec joomla rm -f administrator/cache/autoload_psr4.php

# Plain server, from the Joomla root
rm -f administrator/cache/autoload_psr4.php

The next request recreates the file, which needs write access to administrator/cache. If the folder is not writable, Joomla logs a warning ("Could not save …​/autoload_psr4.php"), keeps the map in memory for that request and carries on; this is deliberate, so that the page still renders and the real error can be displayed later. The cost is a rescan on every request, which is slow, so fix the permissions (see configuration.php and .htaccess and System information, maintenance and troubleshooting).

Two more points matter when automating deployments:

  • Treat the file as a build artefact of the running site. Do not commit it, and exclude administrator/cache from deployments, so that the target site builds its own.

  • A zero-downtime deployment that swaps the whole web root can leave a map that points to the previous release’s folders; delete the file as the last step of the deployment.

Library classes, the class map and the legacy aliases

The namespace map is only for components, modules, templates, plugins and libraries that declare a namespace. The rest of libraries/ is loaded by the Composer files in libraries/vendor/composer, including the class map for the framework packages. A third category exists for backward compatibility: the pre-4.0 global names (JFactory, JText, JModelAdmin, about 450 aliases) that JLoader maps to the namespaced classes through libraries/classmap.php. The ClassLoader decorator in Joomla\CMS\Autoload calls JLoader::applyAliasFor() when it loads a class, which is how an old JFactory call reaches Joomla\CMS\Factory.

In Joomla 6 those aliases are not part of the default behaviour: they are provided by the Behaviour - Backward Compatibility 6 plugin, which is off by default on new installs. Code written for 3.x that still says JFactory::getDbo() fails on such a site with "Class JFactory not found". The fix is not to switch the plugin on, but to use the namespaced class and the container (next sections). The wider 6.0 removals are collected in Deprecations and preparing for Joomla 7, and the book corrections about it are further down this page.

Third-party libraries and vendor namespaces

An extension can ship its own Composer dependencies in a vendor folder inside its package. Two extensions that bundle different versions of the same library under the same namespace both register that namespace with the autoloader, and whichever runs first wins, which causes errors that depend on the order in which extensions load. There are three ways to stay safe:

  • depend on libraries Joomla already ships (for instance Joomla\Registry, Joomla\Input, Joomla\Filesystem) and declare nothing new

  • prefix the bundled library into your own vendor namespace with a tool such as PHP-Scoper or Mozart, so that GuzzleHttp\Client becomes Tallpine\Vendor\GuzzleHttp\Client

  • wrap the library in a Joomla library extension (type library) with its own namespace element, so that Joomla maps it like any other extension

For development, the Composer file of your source repository can describe the same mapping so that the IDE and static analysis tools find the classes without a running site. The paths below assume the package layout of the Tallpine repository (src/administrator, src/site, src/api, src/mod_tallpine_nexthikes and so on):

{
    "name": "tallpine/pkg-tallpine",
    "type": "project",
    "require": {
        "php": "^8.3"
    },
    "autoload": {
        "psr-4": {
            "Tallpine\\Component\\Tallpine\\Administrator\\": "com_tallpine/administrator/src/",
            "Tallpine\\Component\\Tallpine\\Site\\": "com_tallpine/site/src/",
            "Tallpine\\Component\\Tallpine\\Api\\": "com_tallpine/api/src/",
            "Tallpine\\Module\\NextHikes\\Site\\": "mod_tallpine_nexthikes/src/",
            "Tallpine\\Plugin\\Content\\TallpineConditions\\": "plg_content_tallpine_conditions/src/"
        }
    }
}

This file is for the development tools only. On a real site the generated autoload_psr4.php is the only mapping that counts, and Joomla never reads your Composer file.

The dependency injection container

The container of the Joomla Framework (Joomla\DI\Container, package joomla/di, version 4 in Joomla 6.2) is a repository of keys and values. The key is a string, almost always the fully qualified name of an interface or class; the value is either an object or a function that creates one. Requesting by an interface name or a class name makes no functional difference to the container: the key is just a string, so the same string must be used to store and to fetch.

The container methods

Method What it does

set($key, $value, $shared = false, $protected = false)

Registers a value or a factory function under a key. By default the entry is not shared, so a function is called on every get() and returns a new object. Passing $protected = true makes a later set() on the same key throw a ProtectedKeyException.

share($key, $value, $protected = false)

Like set() with $shared = true: the function is called once and the same instance is returned afterwards. Use this for services (a database connection, a factory).

protect($key, $value, $shared = false)

A shortcut for a protected key. Core uses it so that an extension cannot overwrite a core service in the root container.

get($key)

Returns the resource. Throws KeyNotFoundException when neither this container nor its parents know the key.

has($key)

Tells whether the key is known here or in a parent. Use it before fetching an optional service (the Module service provider does exactly that for the helper factory).

alias($alias, $key)

Makes a second name resolve to the same entry. Core aliases db, DatabaseDriver and the old driver class name to DatabaseInterface, config to JConfig and user.factory to UserFactoryInterface.

createChild()

Returns a new container whose parent is this one. Used for every extension (below).

registerServiceProvider($provider)

Calls register($container) of a ServiceProviderInterface object, so a block of related registrations can be reused.

extend($key, $callable)

Wraps an existing entry, so that its result is modified after it is created.

getNewInstance($key)

Creates a fresh object from a shared key, bypassing the stored instance.

buildObject($class), buildSharedObject($class)

Creates an object of a class by reading the type declarations of its constructor and fetching each class-typed argument from the container (autowiring). Arguments that are scalar and have no default cannot be wired.

lazy($class, $factory)

Returns a callable that creates a PHP lazy proxy of the class: the factory runs only when a property or method of the object is first used.

Two details deserve emphasis. The first is the non-shared default: set() on its own produces a new object on every get(). Joomla core registers its services with share(), which is why a single database connection serves a request; extension providers that register an extension class with set() are saved from repeated construction by the extension manager’s own cache, described below. The second is lazy(). It relies on PHP 8.4 lazy objects; on PHP 8.3, which is the minimum for Joomla 6, it simply returns the factory, so the object is created eagerly and the code still works. Core plugins use it because most plugins are registered on a request but never receive a matching event.

The root container and the core services

The root container is built by Factory::getContainer() the first time it is called. Factory::createContainer() registers about two dozen core service providers from libraries/src/Service/Provider: Application, Authentication, CacheController, CaptchaRegistry, Config, Console, Database, Dispatcher, Document, EditorsRegistry, Form, HTMLRegistry, Input, Language, Logger, Mailer, Menu, Pathway, Router, Session, Toolbar, User and WebAssetRegistry. The keys that extension code uses most are:

Key Type it returns Use it for

Joomla\Database\DatabaseInterface

the database driver

queries (alias db)

Joomla\CMS\User\UserFactoryInterface

UserFactory

loading users by id or username without static calls

Joomla\CMS\Language\LanguageFactoryInterface

LanguageFactory

creating a Language for a specific tag

Joomla\CMS\Mail\MailerFactoryInterface

MailerFactory

creating a mailer

Joomla\CMS\Form\FormFactoryInterface

FormFactory

building Form objects

Joomla\CMS\Cache\CacheControllerFactoryInterface

CacheControllerFactory

caches

Joomla\Event\DispatcherInterface

the event dispatcher

dispatching custom events

Joomla\Session\SessionInterface

the session

reading or writing session data

JConfig (alias config)

a Registry of configuration.php

server-side settings, a protected key

SiteApplication and friends

the application object

normally reached with getApplication(), not directly

The application is itself built by the container: the Application provider creates SiteApplication, AdministratorApplication, ApiApplication or ConsoleApplication and injects the dispatcher, the logger, the session, the user factory and the menu factory. Reading the configuration through the container looks like this, and the Manual’s walkthrough of it is a good first read:

<?php

use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

// Outside of a class that can receive its dependencies (a script file, a legacy layout).
$container = Factory::getContainer();

// Both keys return the same shared Registry.
$config    = $container->get('config');
$sitename  = $config->get('sitename');

// The application also exposes the configuration.
$app       = Factory::getApplication();
$sitename2 = $app->get('sitename', 'Tallpine Hiking Club');

Factory::getContainer() and Factory::getApplication() are the two static entry points that remain supported. Use them at the outer edge only (a provider, a script file, a legacy layout), never in the middle of a class that could receive the object by injection.

Child containers and the extension boot sequence

Joomla does not register extension services in the root container. When an extension is booted, the application creates a child container just for it. A child holds a pointer to its parent and behaves like the parent with two differences:

  • set() writes into the child only, so an extension cannot overwrite core keys or collide with another extension that also registers ComponentInterface, MVCFactoryInterface or PluginInterface

  • get() looks in the child first and, when the key is missing, asks the parent; that is how a module’s HelperFactory receives the root container’s DatabaseInterface without being given it

The code that does this is the trait Joomla\CMS\Extension\ExtensionManagerTrait, used by the application classes. The three public methods are bootComponent(), bootModule() and bootPlugin(); all call the same private loadExtension():

sequenceDiagram participant A as Application participant R as Root container participant C as Child container participant P as services/provider.php participant E as Extension object A->>A: bootComponent(tallpine) A->>R: createChild() R-->>C: new child container A->>R: dispatch onBeforeExtensionBoot A->>P: require_once, then register(child) P->>C: set(ComponentInterface) and registerServiceProvider(...) A->>R: dispatch onAfterExtensionBoot A->>C: get(ComponentInterface) C-->>E: TallpineComponent instance A->>E: boot(child) when BootableExtensionInterface A->>A: cache the extension for this request

The steps, in the order the source performs them:

  1. The name is normalised (com_tallpine and tallpine both work) and the application looks for administrator/components/com_tallpine/services/provider.php; modules look in modules/mod_name or administrator/modules/mod_name, and plugins in plugins/group/name.

  2. If the extension was already booted during this request, the cached object is returned immediately (the cache is a static array in ExtensionHelper).

  3. A child container is created and the event onBeforeExtensionBoot is dispatched with the child as an argument, which lets a system plugin register or override services before the provider runs.

  4. The provider file is loaded with require_once and, if it returned an object implementing Joomla\DI\ServiceProviderInterface, its register() method receives the child container.

  5. If the provider did not register the extension type, Joomla falls back to legacy behaviour: a component becomes a LegacyComponent that includes the old tallpine.php entry file, a module becomes a core Module with empty namespaces, and a plugin is loaded from plugins/group/name/name.php with the old PlgGroupName class naming. A missing or broken services/provider.php therefore does not always fail loudly; it can silently run the legacy path or a DummyPlugin.

  6. onAfterExtensionBoot is dispatched.

  7. The extension object is fetched with get(). If it implements BootableExtensionInterface, boot($container) is called, which is the place to register HTML helper services or other things that need the container.

  8. The object is cached and returned.

The pattern is the same for the three types, but the key differs: ComponentInterface::class, ModuleInterface::class and PluginInterface::class. Because the default entry of set() is not shared, calling the factory function twice is avoided by this extension cache rather than by the container.

Root container with the core services and three child containers, one for com_tallpine, one for mod_tallpine_nexthikes and one for plg_content_tallpine_conditions, each with the services its provider registers

services/provider.php for a component

The provider is a PHP file that returns an object implementing ServiceProviderInterface. Core and the Manual use an anonymous class, which has no name to collide with. For a component the file registers the factories and then the extension class. The com_tallpine provider, modelled on the one of com_content:

<?php

/**
 * @package     Tallpine.Administrator
 * @subpackage  com_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

\defined('_JEXEC') or die;

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

return new class () implements ServiceProviderInterface {
    /**
     * Registers the service provider with a DI container.
     *
     * @param   Container  $container  The DI container (a child container for this component).
     *
     * @return  void
     */
    public function register(Container $container): void
    {
        $namespace = '\\Tallpine\\Component\\Tallpine';

        // Step 1: the factories this component needs.
        $container->registerServiceProvider(new CategoryFactory($namespace));
        $container->registerServiceProvider(new MVCFactory($namespace));
        $container->registerServiceProvider(new ComponentDispatcherFactory($namespace));
        $container->registerServiceProvider(new RouterFactory($namespace));

        // Step 2: the extension class, with its sub-dependencies injected.
        $container->set(
            ComponentInterface::class,
            function (Container $container) {
                $component = new TallpineComponent($container->get(ComponentDispatcherFactoryInterface::class));

                $component->setRegistry($container->get(Registry::class));
                $component->setMVCFactory($container->get(MVCFactoryInterface::class));
                $component->setCategoryFactory($container->get(CategoryFactoryInterface::class));
                $component->setRouterFactory($container->get(RouterFactoryInterface::class));

                return $component;
            }
        );
    }
};

Points to notice:

  • The two steps are the whole pattern: register the dependencies, then build the extension and hand it its dependencies with get() calls inside the closure. The closure runs only when the extension is first fetched, so registering a provider costs almost nothing.

  • The namespace passed to each factory has a leading backslash and no Site, Administrator or Api suffix. The factories add the suffix of the current application, which is how one provider serves all three sides.

  • A component that has no categories, no SEF router or no HTML services simply leaves those lines out. The smallest working provider registers MVCFactory, ComponentDispatcherFactory and an MVCComponent.

  • The provider lives in the administrator folder (administrator/components/com_tallpine/services/provider.php) and serves the site and the API too, because bootComponent() always looks there.

  • The file starts with defined('_JEXEC') or die; like every Joomla PHP file. Since 6.0, JPATH_PLATFORM is no longer defined unless the backward-compatibility plugin runs, so never use the older guard that tests it.

The extension class is where the traits that match the providers are mixed in. It is the component’s entry object:

<?php

namespace Tallpine\Component\Tallpine\Administrator\Extension;

use Joomla\CMS\Categories\CategoryServiceInterface;
use Joomla\CMS\Categories\CategoryServiceTrait;
use Joomla\CMS\Component\Router\RouterServiceInterface;
use Joomla\CMS\Component\Router\RouterServiceTrait;
use Joomla\CMS\Extension\BootableExtensionInterface;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\HTML\HTMLRegistryAwareTrait;
use Psr\Container\ContainerInterface;
use Tallpine\Component\Tallpine\Administrator\Service\HTML\TrailIcon;

\defined('_JEXEC') or die;

/**
 * Component class for com_tallpine.
 */
class TallpineComponent extends MVCComponent implements
    BootableExtensionInterface,
    CategoryServiceInterface,
    RouterServiceInterface
{
    use CategoryServiceTrait;
    use RouterServiceTrait;
    use HTMLRegistryAwareTrait;

    /**
     * Called by Joomla right after the component was fetched from its child container.
     */
    public function boot(ContainerInterface $container): void
    {
        $icon = new TrailIcon();
        $icon->setUserFactory($container->get(\Joomla\CMS\User\UserFactoryInterface::class));

        $this->getRegistry()->register('trailicon', $icon);
    }
}

boot() receives the child container, so everything the provider registered and everything of the root container is available. This is the supported place to read a service when the component loads, instead of calling Factory:: from the constructor.

What each service factory does

Each Joomla\CMS\Extension\Service\Provider\* class is a small ServiceProviderInterface that stores a namespace and, in register(), sets one interface key to a closure that creates the real factory. They all work by building class names from the namespace, which is why the directory conventions mattered earlier.

Provider (constructor argument) Key it registers What the factory it creates does

MVCFactory (component namespace)

MVCFactoryInterface

Creates controllers, models, views and tables for the component by building <namespace>\<Application>\Controller\<Name>Controller, ...\Model\<Name>Model, ...\View\<Name>\<Type>View and ...\Table\<Name>Table, where <Application> is Site, Administrator or Api. After creating an object it injects the services that object declares an interest in: the form factory, the event dispatcher, the database, the site router, the cache controller factory, the user factory, the mailer factory and the language factory (an object announces this by implementing the matching ...AwareInterface). In 6.2 the API application gets an ApiMVCFactory and the class was split into overridable methods.

ComponentDispatcherFactory (component namespace)

ComponentDispatcherFactoryInterface

Creates the dispatcher of the current side, <namespace>\<Application>\Dispatcher\Dispatcher, and falls back to the generic ComponentDispatcher (or ApiDispatcher for the API) when the class does not exist. It receives the MVCFactoryInterface from the same container and hands it to the dispatcher, which uses it to create the controller.

RouterFactory (component namespace)

RouterFactoryInterface

Creates the SEF router <namespace>\<Application>\Service\Router of the component, passing the application, the menu, the category factory (when one is registered) and the database. It throws a RuntimeException "No router available for this application" when the class is missing. See Component: site views and routing.

CategoryFactory (component namespace)

CategoryFactoryInterface

Creates the category tree objects of the component by instantiating <namespace>\Site\Service\<Section>Category (with the database injected), and throws SectionNotFoundException for an unknown section. See Categories, custom fields and workflow integration.

ModuleDispatcherFactory (module namespace)

ModuleDispatcherFactoryInterface

Creates <namespace>\Site\Dispatcher\Dispatcher (or Administrator when the application is the back end) for the module, and falls back to the generic ModuleDispatcher when it does not exist.

HelperFactory (helper namespace)

HelperFactoryInterface

Creates helper objects by name: getHelper('NextHikesHelper') builds <namespace>\NextHikesHelper, injecting the database when the helper implements DatabaseAwareInterface. It returns null when the class does not exist, so check the result.

Module (no argument)

ModuleInterface

Builds the core Joomla\CMS\Extension\Module object that joins the dispatcher factory and the (optional) helper factory for a module.

Because the lookups are by class name, the commonest namespace errors turn into specific symptoms:

  • a controller in Controllers\TrailController instead of Controller\TrailController makes the MVCFactory return null for it, and the component fails with a "controller not found" style error rather than a "class not found" one

  • a router in Service\Router that is missing gives "No router available for this application"

  • a missing Site\Dispatcher\Dispatcher is not an error at all: the generic dispatcher runs, which is correct for simple components and a surprise for a developer who expected their dispatch() override to be called

services/provider.php for a module

A module’s provider is shorter, because a module has no MVC factory. It registers the dispatcher factory, the helper factory and the core Module provider. The module namespace is Tallpine\Module\NextHikes and its helpers live in the sub-namespace Site\Helper:

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  mod_tallpine_nexthikes
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

\defined('_JEXEC') or die;

use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->registerServiceProvider(new ModuleDispatcherFactory('\\Tallpine\\Module\\NextHikes'));
        $container->registerServiceProvider(new HelperFactory('\\Tallpine\\Module\\NextHikes\\Site\\Helper'));

        $container->registerServiceProvider(new Module());
    }
};

The pieces connect like this. ModuleHelper boots the module, so the child container is built and the provider runs. Module::getDispatcher() asks the dispatcher factory for Tallpine\Module\NextHikes\Site\Dispatcher\Dispatcher, and if that class implements HelperFactoryAwareInterface the module object injects the helper factory into it. The dispatcher then asks for helpers by name:

<?php

namespace Tallpine\Module\NextHikes\Site\Dispatcher;

use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
use Joomla\CMS\Helper\HelperFactoryAwareInterface;
use Joomla\CMS\Helper\HelperFactoryAwareTrait;

\defined('_JEXEC') or die;

/**
 * Dispatcher class for mod_tallpine_nexthikes.
 */
class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareInterface
{
    use HelperFactoryAwareTrait;

    protected function getLayoutData(): array
    {
        $data = parent::getLayoutData();

        // The factory builds Tallpine\Module\NextHikes\Site\Helper\NextHikesHelper and injects the database.
        $helper = $this->getHelperFactory()->getHelper('NextHikesHelper');

        $data['hikes'] = $helper->getNextHikes($data['params'], $data['app']);

        return $data;
    }
}

The helper uses the Aware interface for the database; the factory sees DatabaseAwareInterface and calls setDatabase():

<?php

namespace Tallpine\Module\NextHikes\Site\Helper;

use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;

\defined('_JEXEC') or die;

class NextHikesHelper implements DatabaseAwareInterface
{
    use DatabaseAwareTrait;

    /**
     * @return  array<int, object>
     */
    public function getNextHikes(Registry $params, CMSApplicationInterface $app): array
    {
        $db    = $this->getDatabase();
        $limit = (int) $params->get('count', 5);
        $today = (new \DateTimeImmutable('today'))->format('Y-m-d');

        $query = $db->createQuery()
            ->select($db->quoteName(['id', 'title', 'hike_date']))
            ->from($db->quoteName('#__tallpine_hikes'))
            ->where($db->quoteName('hike_date') . ' >= :today')
            ->where($db->quoteName('published') . ' = 1')
            ->bind(':today', $today, ParameterType::STRING)
            ->order($db->quoteName('hike_date') . ' ASC')
            ->setLimit($limit);

        return $db->setQuery($query)->loadObjectList();
    }
}

The module classes in full, together with layouts, parameters and caching, are in Module development.

services/provider.php for a plugin

A plugin’s provider binds PluginInterface to a factory that builds the plugin class and hands it what it needs. The plugin class is final in core and extends CMSPlugin and implements SubscriberInterface; the Manual and core use lazy() so the object is built only when an event reaches it:

<?php

/**
 * @package     Tallpine.Plugin
 * @subpackage  Content.tallpine_conditions
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

\defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Tallpine\Plugin\Content\TallpineConditions\Extension\TallpineConditions;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->set(
            PluginInterface::class,
            $container->lazy(TallpineConditions::class, function (Container $container) {
                $plugin = new TallpineConditions(
                    (array) PluginHelper::getPlugin('content', 'tallpine_conditions')
                );
                $plugin->setApplication(Factory::getApplication());
                $plugin->setDatabase($container->get(DatabaseInterface::class));

                return $plugin;
            })
        );
    }
};

Three details of this provider matter:

  • The plugin object is created with the plugin record (PluginHelper::getPlugin() returns the row with name, type and params), which is how $this->params is filled.

  • Factory::getApplication() appears once, at the edge, in the factory closure. That is the pattern in core’s own providers: it is the single place where the static call is accepted, and it is inside the code that builds the object, not in the object.

  • Joomla 4 and 5 examples pass the event dispatcher as the first constructor argument (new Foo($container->get(DispatcherInterface::class), $config)). Joomla 5 deprecated that, CMSPlugin triggers a deprecation notice for it, and from 7.0 the class no longer implements DispatcherAwareInterface. A plugin that really dispatches its own events should implement DispatcherAwareInterface itself.

The plugin class then uses the Aware traits for what it needs and subscribes to concrete event classes:

<?php

namespace Tallpine\Plugin\Content\TallpineConditions\Extension;

use Joomla\CMS\Event\Content\ContentPrepareEvent;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\User\UserFactoryAwareTrait;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Event\SubscriberInterface;

\defined('_JEXEC') or die;

/**
 * Appends the current trail conditions to Tallpine trail articles.
 */
final class TallpineConditions extends CMSPlugin implements SubscriberInterface
{
    use DatabaseAwareTrait;
    use UserFactoryAwareTrait;

    public static function getSubscribedEvents(): array
    {
        return [
            'onContentPrepare' => 'onContentPrepare',
        ];
    }

    public function onContentPrepare(ContentPrepareEvent $event): void
    {
        // The application, injected by the provider, replaces the old $app property.
        if (!$this->getApplication()->isClient('site')) {
            return;
        }

        $item = $event->getItem();

        if ($event->getContext() !== 'com_content.article' || !isset($item->id)) {
            return;
        }

        $db    = $this->getDatabase();
        $query = $db->createQuery()
            ->select($db->quoteName('status'))
            ->from($db->quoteName('#__tallpine_conditions'))
            ->where($db->quoteName('article_id') . ' = :id')
            ->bind(':id', $item->id, \Joomla\Database\ParameterType::INTEGER);

        $status = $db->setQuery($query)->loadResult();

        if ($status !== null) {
            $item->text .= '<p class="trail-conditions">' . htmlspecialchars((string) $status, ENT_QUOTES, 'UTF-8') . '</p>';
        }
    }
}

getApplication() is a protected method of CMSPlugin that returns what the provider injected with setApplication(); the database comes from DatabaseAwareTrait, the user factory from UserFactoryAwareTrait. The event and its getters (getItem(), getContext(), getParams()) are covered on Plugin development and events. A plugin class without any of this, just a tallpine_conditions.php file and a PlgContentTallpine_conditions class, still loads through the legacy fallback, but it is the shape that Joomla 7 stops supporting.

Why Factory::get*() static calls are discouraged

The static helpers of Joomla\CMS\Factory were the standard way to reach the framework in Joomla 3. In Joomla 4.3 most of them were deprecated, and they are scheduled for removal in 7.0. Each emits an E_USER_DEPRECATED notice that names the replacement, so a site running with error reporting set to maximum shows them in the log:

Static call Deprecated Replacement

Factory::getDbo()

4.3, removal 7.0

DatabaseInterface from the container, DatabaseAwareTrait, or the helper/model that already has it

Factory::getUser()

4.3, removal 7.0

$app->getIdentity() for the current user, UserFactoryInterface::loadUserById() for another one

Factory::getLanguage()

4.3, removal 7.0

$app->getLanguage(), LanguageFactoryInterface, or the language aware trait of the class

Factory::getSession()

4.3, removal 7.0

$app->getSession() or SessionInterface from the container

Factory::getDocument()

4.3, removal 7.0

$app->getDocument()

Factory::getConfig()

4.3, removal 7.0

$app->get('key') or the config service

Factory::getCache()

4.3, removal 7.0

CacheControllerFactoryInterface

Factory::getMailer()

4.4, removal 7.0

MailerFactoryInterface

Factory::getApplication()

not deprecated

use at the edge (provider, script), prefer getApplication() of a class that was given it

Factory::getContainer()

not deprecated

use at the edge, to build objects; do not scatter it through business logic

The reasons are practical rather than stylistic:

  • Testability. A class that calls Factory::getDbo() inside a method cannot be unit-tested without a real database and a booted application. A class that receives a DatabaseInterface can be given an in-memory fake.

  • Honest dependencies. The constructor and setters of an injected class list what it needs. A static call hides the dependency inside the method body, so you discover it by reading every line.

  • Multiple applications. In a CLI task, in the Web Services API, in a scheduled task run through cron and in the administrator, "the application" and "the user" differ. A static call returns whatever global state was last set up, which is how a scheduled task ends up reading the language of a different request. An injected object is the one you were given.

  • Lifecycle. A static is bound to the current request and initialised at a point you do not control. Called too early (for example from a constructor that runs before the application has started) it throws "Failed to start application".

  • Removal. They disappear in 7.0, and the legacy J aliases that old code uses to reach them are off by default in 6. Fixing them later is the same work as fixing them now, with a deadline.

Getting the application, the database, the user and the language

The replacements depend on where the code lives. The principle: get the object once, at the edge, and pass it in.

Where the code is How to obtain the objects

Plugin class

getApplication() (set by the provider with setApplication()), getDatabase() from DatabaseAwareTrait, getUserFactory() from UserFactoryAwareTrait. The language comes from $this->getApplication()->getLanguage(); the language aware traits on CMSPlugin itself are deprecated, with removal in 7.0.

Module dispatcher or component dispatcher

$this->app (also getApplication()), $this->input, and the helper factory through HelperFactoryAwareTrait. The dispatcher receives the application in its constructor.

Module helper

DatabaseAwareInterface plus DatabaseAwareTrait; the HelperFactory calls setDatabase(). The application is passed as a method argument (see getNextHikes() above).

Component controller

$this->app, $this->input, and the objects MVCFactory injected because the controller implements their Aware interface (UserFactoryAwareTrait, FormFactoryAwareTrait, …​).

Component model

DatabaseAwareTrait is built in: $this->getDatabase(). The MVCFactory injects the database, the user factory, the mailer factory and so on, when the model declares the matching interface.

Table class

the database is a required constructor argument (Table::__construct($table, $key, DatabaseInterface $db)), so the factory or the code that creates the table passes it in.

Layout (tmpl file)

compute the value in the view (which has getCurrentUser() and the other view-level getters) and hand it to the layout; Factory::getApplication() in a layout is a last resort for old-style code.

Console command or scheduled task

the command or task plugin receives the objects through its Aware traits; see Console command plugins and Scheduled task plugins.

Script file with no class

Factory::getContainer()->get(DatabaseInterface::class) and Factory::getApplication(), once, at the top.

The language object deserves a note. Joomla\CMS\Language\LanguageAwareTrait gives a class setLanguage() and getLanguage() (which throws UnexpectedValueException if none was set). Text::_() is still a static call, but it is the one that Joomla keeps, because it reads the application’s language itself; for any other language use the LanguageFactoryInterface:

<?php

use Joomla\CMS\Factory;
use Joomla\CMS\Language\LanguageFactoryInterface;

\defined('_JEXEC') or die;

// A German Language object, independent of the language of the current request.
$german = Factory::getContainer()
    ->get(LanguageFactoryInterface::class)
    ->createLanguage('de-DE', false);

$german->load('com_tallpine', JPATH_ADMINISTRATOR . '/components/com_tallpine');
$label  = $german->_('COM_TALLPINE_TRAIL_DIFFICULTY');

The same factory is how the Tallpine mail templates render the German version of an email without switching the whole request’s language; see Language files and internationalisation.

The Aware traits and interfaces

An Aware pair is a very small convention: an interface that says "I accept this service" and a trait that implements it with a private property and a setter. The factory that builds the object (a MVCFactory, a HelperFactory, a plugin provider) checks instanceof and calls the setter. The traits you will use most:

Trait Setter and getter Typical user

Joomla\Database\DatabaseAwareTrait

setDatabase(), getDatabase()

models, helpers, plugins, task and console classes

Joomla\CMS\User\UserFactoryAwareTrait

setUserFactory(), getUserFactory()

plugins, controllers, models

Joomla\CMS\Mail\MailerFactoryAwareTrait

setMailerFactory(), getMailerFactory()

plugins that send email

Joomla\CMS\Language\LanguageFactoryAwareTrait

setLanguageFactory(), getLanguageFactory()

plugins that render in another language

Joomla\CMS\Language\LanguageAwareTrait

setLanguage(), getLanguage()

classes that need the current language

Joomla\CMS\Form\FormFactoryAwareTrait

setFormFactory(), getFormFactory()

models and plugins that build forms

Joomla\CMS\Helper\HelperFactoryAwareTrait

setHelperFactory(), getHelperFactory()

module dispatchers

Joomla\CMS\MVC\Factory\MVCFactoryAwareTrait

setMVCFactory(), getMVCFactory()

dispatchers and classes that create models

The getters are protected, so a class uses them internally; external code calls the public setter only. When you write your own service, copy the pattern: define a small interface, a trait with a private property, and set it from your provider.

Registering your own service

Nothing limits the container to core keys. A component that talks to the park authority API of the Tallpine scheduled task can register a client under its own interface, in the same provider as the component, and any class booted from the same child container can then fetch it:

<?php

\defined('_JEXEC') or die;

use Joomla\CMS\Http\HttpFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Tallpine\Component\Tallpine\Administrator\Service\ParkAuthorityClient;
use Tallpine\Component\Tallpine\Administrator\Service\ParkAuthorityClientInterface;

return new class () implements ServiceProviderInterface {
    public function register(Container $container): void
    {
        $container->share(
            ParkAuthorityClientInterface::class,
            static function (Container $container): ParkAuthorityClientInterface {
                // The endpoint and key come from the environment, never from source control.
                $endpoint = getenv('TALLPINE_PARK_API_URL') ?: 'https://park-authority.example/api';
                $apiKey   = getenv('TALLPINE_PARK_API_KEY') ?: 'CHANGE_ME';

                return new ParkAuthorityClient((new HttpFactory())->getHttp(), $endpoint, $apiKey);
            }
        );
    }
};

Registered in the child container, the key is visible to that extension only. A service shared between extensions belongs to a library extension or a system plugin that registers it in the root container (the event onBeforeExtensionBoot, which carries the child container, is the hook for registering into a child from outside). Avoid overriding a core key: some core entries, config for one, are protected, and an attempt to replace a protected key raises a ProtectedKeyException.

Testing with the container

The payoff of all this is that a unit test needs no booted Joomla. The test builds a plain container, registers a fake under the interface and hands it to the class under test:

<?php

namespace Tallpine\Tests\Unit\Module;

use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use PHPUnit\Framework\TestCase;
use Tallpine\Module\NextHikes\Site\Helper\NextHikesHelper;

final class NextHikesHelperTest extends TestCase
{
    public function testHelperUsesTheInjectedDatabase(): void
    {
        $db = $this->createMock(DatabaseInterface::class);

        // The root container holds the (fake) database; a child container behaves like an extension's.
        $root  = new Container();
        $root->set(DatabaseInterface::class, $db);
        $child = $root->createChild();

        // A key missing from the child is resolved from the parent, as in a booted extension.
        $this->assertSame($db, $child->get(DatabaseInterface::class));

        // The helper receives the fake exactly as HelperFactory would inject the real driver.
        $helper = new NextHikesHelper();
        $helper->setDatabase($child->get(DatabaseInterface::class));

        $this->assertInstanceOf(NextHikesHelper::class, $helper);
    }
}

The full test set-up for extensions is on Testing extensions.

Known limits of the container in extensions

The Manual keeps a list of places where core itself still uses static access and the DI route does not yet work smoothly, and it is useful to know them so you do not fight the framework:

  • Toolbar. Toolbar::getInstance('toolbar') is still the working way to reach the toolbar that the toolbar module renders; a toolbar created from the factory in the container is a different object and its buttons are not displayed.

  • Tables outside the model. The MVCFactory creates tables only for its own component. To use a table of another component, boot that component and use its factory: Factory::getApplication()->bootComponent('content')->getMVCFactory()->createTable($name, $prefix, $config).

  • Categories in models. The CategoryFactory is available to the extension class but is not passed on to models; a common workaround is to keep the factory reachable from the component’s extension class.

Security considerations

  • Every file starts with the _JEXEC guard. Provider files and class files are web-reachable if the server runs PHP in the extension folder; the guard stops direct execution. defined('_JEXEC') or die; is correct for 6.x; do not copy a JPATH_PLATFORM guard.

  • The map is code. autoload_psr4.php is executed on every request. Keep administrator/cache writable by the web server user only, never world-writable, and never serve it: an attacker who can write a prefix into the file can make Joomla load a class of their choosing.

  • Namespace squatting. An unmaintained extension still declares its namespace and still gets mapped. Uninstall extensions you do not use; the uninstall also drops the prefix from the map.

  • Do not put secrets in providers. Providers are source code. Read API keys and passwords from environment variables or from configuration.php (through the config service), and use obvious placeholders (CHANGE_ME) in samples.

  • Plugin and module code runs with the privileges of the site. A service you register is available to the whole child container; keep factories narrow and do not expose a "run any SQL" service.

  • Events can change booting. A system plugin that listens to onBeforeExtensionBoot can add or replace services of any extension. That is a feature for test doubles and a risk for a malicious plugin, which is one more reason to install extensions only from trusted sources.

Common errors

Symptom Likely cause and fix

"Class …​ not found" for your own class

The prefix is missing from autoload_psr4.php (stale map or no namespace element: delete the file and check the installed manifest), or the path or case differs from the namespace (Linux).

"Class JFactory not found" (or JText, JHtml)

Legacy aliases are off in Joomla 6. Replace them with Joomla\CMS\Factory, Joomla\CMS\Language\Text, Joomla\CMS\HTML\HTMLHelper.

Resource 'Joomla\...\MVCFactoryInterface' has not been registered with the container

The provider did not call registerServiceProvider(new MVCFactory(...)), or it was registered in another extension’s child container.

KeyNotFoundException for a service of yours

You registered it in a different child container, or the provider returned before reaching that line.

ProtectedKeyException

set() on a key that was registered as protected, typically a core service.

The component loads but ignores a Dispatcher override

The class is not at <Application>\Dispatcher\Dispatcher; the factory silently used the generic dispatcher.

Database not set in ... (DatabaseNotFoundException)

The class uses DatabaseAwareTrait but nobody called setDatabase(): it was built with new instead of through the factory or provider.

UserFactory not set in ...

Same for UserFactoryAwareTrait: add setUserFactory() to the provider closure.

Deprecation notices flood the log

Static Factory::get*() calls, a plugin constructor receiving a dispatcher, or a $app/$db plugin property. See the tables above.

A new extension works on one site and not on another

The Namespace Map plugin is disabled, administrator/cache is not writable, or a deployment kept a stale map.

A plugin file with a services/provider.php is never used

The provider did not bind PluginInterface::class, so Joomla fell back to the legacy file or a dummy plugin.

Version notes and corrections of older material

  • Joomla 4.0: namespaces, the child-container extension loader, services/provider.php, MVCFactory, ComponentDispatcherFactory, RouterFactory, CategoryFactory, ModuleDispatcherFactory and HelperFactory all start here (each class is marked @since 4.0.0).

  • Joomla 4.3 and 4.4: most static Factory helpers are deprecated (4.3, with getMailer() in 4.4).

  • Joomla 5.0: concrete event classes for plugins, and SubscriberInterface as the normal plugin base.

  • Joomla 5.x: a CMSPlugin constructor that receives a dispatcher, and the language and dispatcher traits on CMSPlugin, are deprecated (the notices appear from the 5.2/5.3 releases), with removal in 7.0.

  • Joomla 6.0: the legacy J* aliases, the JPATH_PLATFORM constant, \Joomla\CMS\Input, \Joomla\CMS\Filesystem and the CMS BaseApplication and CliApplication classes are available only through the Behaviour - Backward Compatibility 6 plugin.

  • Joomla 6.2: the MVCFactory service provider is split into createMVCFactory() and injectServicesIntoFactory() so a subclass can change what is created or injected; the API application receives the ApiMVCFactory.

  • Joomla 7.0 (planned): the static Factory helpers listed above are removed, and plugins no longer get $app and $db properties detected automatically; a CMSPlugin will not implement DispatcherAwareInterface. Code that uses getApplication(), getDatabase() and a provider is ready.

Developing Extensions for Joomla! 5 (Packt, 2023), the best development reference among the older books, teaches the namespaced MVC structure, services/provider.php and DatabaseAwareTrait correctly, and its explanation of the MVCFactory is still accurate. Its plugin chapters predate the following changes. A webservices plugin that extends CMSPlugin without implementing SubscriberInterface and reaches the database or the application through $db and $app properties is no longer the pattern: use a provider, SubscriberInterface and getApplication()/getDatabase() (property detection is removed in 7.0). Examples that call Factory::getLanguage() or fetch the driver with Factory::getContainer()->get('DatabaseDriver') should use the application’s language and the interface key DatabaseInterface::class; the old string alias still resolves but is no more than a compatibility name. Event handlers that read $event->getArgument('...') should use the concrete event classes. The same book’s description of the query builder as translating queries for every database engine should not be relied on for installer SQL: the builder does not make raw SQL portable, and MySQL and PostgreSQL need their own install files.

Joomla! 4 Masterclass (Packt, 2023) is a "use" book, so it says little about namespaces and the container; where it mentions extension code it targets Joomla 4, which reached end of life in October 2025, and the Joomla 3 J class names it still shows for compatibility are off by default in 6.

Checklist

  • The manifest has <namespace path="src">Vendor\Type\Name</namespace> and the installed manifest sits where the mapper expects it.

  • Directory and file names match the namespace case exactly, tested on Linux.

  • The Controller, Model, View, Table, Dispatcher, Service folders follow the names the factories use.

  • There is a services/provider.php returning a ServiceProviderInterface; components register MVCFactory, ComponentDispatcherFactory and an extension class, modules register ModuleDispatcherFactory, HelperFactory and Module, plugins bind PluginInterface (preferably with lazy()).

  • Classes get the application, database, user and language through the Aware traits or the provider, not through Factory::get*().

  • administrator/cache/autoload_psr4.php is writable, not committed, and deleted after manual file changes.

  • The Namespace Map plugin is enabled.

  • A unit test builds a plain Joomla\DI\Container and runs the class without a booted site.