Skip to main content

Dependency Injection in Joomla: The DI Container Explained

15 August 2026

Somewhere between Joomla 3 and Joomla 4, extension code changed shape. The familiar JFactory::getDbo() calls disappeared, every extension grew a services/provider.php file, and tutorials started talking about containers and interfaces. Behind all of it stands one idea - dependency injection - and it is simpler than its reputation suggests.

This article explains dependency injection (DI) in Joomla from the ground up. It covers the idea in plain language for everyone who wants to understand why modern Joomla extensions look the way they do, the container and service providers for developers making the jump from Joomla 3 patterns, and the practical recipes - registering, consuming, swapping, and testing services - for daily extension work. It builds on the file structure article (which located services/provider.php) and the authentication article (whose plugins we now finally open up).

Dependency injection means a class asks for what it needs instead of grabbing it. Everything else is machinery around that one sentence.

The goal is simple: make the modern Joomla wiring feel obvious, so you can read, write, and debug DI-based extensions with confidence.

Written for Joomla 6.1. Notes mark where Joomla 4 and 5 differ.

1. The Basics

1.1 The Problem: Classes That Grab

Old-style Joomla code built or fetched its own tools: a model called JFactory::getDbo() for the database, JFactory::getUser() for the user, wherever it needed them. Convenient - and rigid. The class secretly depends on those global calls: you cannot give it a different database (say, a test database), you cannot see from the outside what it needs, and when the global changes shape, every class that grabbed it breaks at once. The Joomla 3 to 4 migration made that cost visible for an entire ecosystem.

1.2 The Idea: Classes That Ask

Dependency injection turns it around: a class declares what it needs (in its constructor or a setter), and someone outside hands it in. The class no longer knows or cares where the database comes from - it just receives one. Think of a chef who lists ingredients and lets the kitchen deliver them, instead of leaving the stove mid-recipe to shop.

1.3 Why Non-Developers Should Care

Even if you never write PHP, DI explains things you see: why Joomla 4+ extensions are structured so differently from Joomla 3 ones, why well-built modern extensions survive Joomla upgrades better (they depend on stable interfaces, not on internals), and why "this extension still uses deprecated Factory calls" in a review is a real warning. Many of these static shortcuts were deprecated during the Joomla 4.x cycle and are tagged for removal in Joomla 7. They should no longer be the foundation of modern extension code. DI is one of the reasons modern Joomla extensions can be more maintainable and resilient across upgrades.

Back to top

2. Dependency Injection in One Example

2.1 Before: Grabbing

class ArticleCounter
{
    public function count(): int
    {
        $db = \Joomla\CMS\Factory::getDbo();   // grabs a global (deprecated)

        return (int) $db->setQuery(
            'SELECT COUNT(*) FROM #__content'
        )->loadResult();
    }
}

2.2 After: Asking

use Joomla\Database\DatabaseInterface;

class ArticleCounter
{
    public function __construct(private DatabaseInterface $db)
    {
    }

    public function count(): int
    {
        return (int) $this->db->setQuery(
            'SELECT COUNT(*) FROM #__content'
        )->loadResult();
    }
}

2.3 What Just Improved

  • Honesty: the constructor is the class's ingredient list - dependencies are visible, not hidden in method bodies.
  • Flexibility: hand in any DatabaseInterface - the live database, a different connection, a test double.
  • Stability: the class depends on an interface that Joomla promises to keep, not on a static call scheduled for removal.

The pattern has a formal name: the Dependency Inversion Principle, the "D" in SOLID - depend on abstractions, not on implementations. If you have followed the example this far, you already understand it.

The open question - who hands the database in - is what the rest of this article answers.

Back to top

3. The Container

A DI container is not dependency injection itself. ArticleCounter already uses DI simply because the database is passed into it. The container is Joomla's mechanism for constructing and connecting larger graphs of such objects automatically.

3.1 A Registry of Recipes

The dependency injection container (DIC) is the kitchen from the analogy: a registry that knows how to build every service. You register a recipe (a closure) under a name - by convention the interface name - and the container runs the recipe when someone asks:

use Joomla\DI\Container;
use Joomla\Database\DatabaseInterface;

$container->share(
    DatabaseInterface::class,
    function (Container $container) {
        // build and return the database driver
    }
);

$db = $container->get(DatabaseInterface::class);

3.2 The API That Matters

MethodMeaning
set(id, recipe) Register a service, non-shared by default.
share(id, recipe) Register a shared service, built once and reused.
get(id) / has(id) Fetch a service / check if a recipe exists.
alias(alias, id) A second name for the same recipe - how legacy names keep working.
protect(id, recipe) Like set, but the recipe cannot be overwritten.
extend(id, closure) Wrap an existing recipe - decorate a service without replacing it.
lazy(class, recipe) (6.1+) Defer construction until the service is first used. Real lazy proxies need PHP 8.4; on older PHP the object is built immediately.
createChild() A child container that inherits all recipes but can add or override its own - remember this one for section 5.

3.3 Where the Container Lives

Joomla builds the container during bootstrap, registering the core recipes, and exposes it via Factory::getContainer(). (The build step itself, Factory::createContainer(), is protected - Joomla's own bootstrap, not an API you call.) You will call getContainer() rarely and deliberately - section 7 explains why reaching for the container from everywhere would just recreate the old grabbing problem with extra steps.

Back to top

4. Service Providers

4.1 Recipes in Packages

Registering recipes one by one would drown the bootstrap in closures. A service provider bundles related recipes into one class with one method:

use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

class MyServiceProvider implements ServiceProviderInterface
{
    public function register(Container $container): void
    {
        // $container->share(...), $container->set(...), ...
    }
}

$container->registerServiceProvider(new MyServiceProvider());

4.2 The Core Providers

Everything you have used through this article series is wired exactly this way. libraries/src/Service/Provider/ holds the core's providers - a sample:

ProviderRegisters
Application The site, administrator, API, and console applications - the four doors from the file structure article.
Database The DatabaseInterface driver, built from configuration.php.
Session The session, with the handler chosen in the Global Configuration.
User The UserFactoryInterface from the authentication and ACL articles.
Mailer, Logger, Document, Router, Toolbar, … Each subsystem, one provider each - about two dozen in total.

Reading this folder is the fastest way to learn "how do I get service X properly": find its provider, see what id it registers, ask for that interface.

Back to top

5. How Extensions Boot

5.1 services/provider.php

The file structure article located it; now we know what it is: modern extensions ship a services/provider.php that returns an anonymous service provider. When Joomla boots the extension, it does something elegant (verified in ExtensionManagerTrait): it creates a child container, runs your provider in it, and pulls the extension instance out:

$container = $this->getContainer()->createChild();   // inherits all core recipes
require $path . '/services/provider.php';            // your provider registers into it
// Joomla then gets ComponentInterface / ModuleInterface / PluginInterface from it

The child container is why your extension can see every core service but can never accidentally overwrite another extension's wiring: each extension gets its own container scope, inheriting core services without modifying another extension's registrations.

5.2 A Real Plugin Provider, Line by Line

The Authentication - Joomla plugin from the authentication article boots like this (abridged from the actual file):

return new class () implements ServiceProviderInterface {
    public function register(Container $container)
    {
        $container->set(
            PluginInterface::class,
            $container->lazy(Joomla::class, function (Container $container) {
                $plugin = new Joomla(
                    (array) PluginHelper::getPlugin('authentication', 'joomla')
                );
                $plugin->setApplication(Factory::getApplication());
                $plugin->setUserFactory($container->get(UserFactoryInterface::class));

                return $plugin;
            })
        );
    }
};

Every piece now has a name: the recipe is registered under PluginInterface::class (what Joomla asks for), lazy() defers construction until the plugin is really needed, the plugin's configuration is injected through the constructor, and its dependencies - application, user factory - are injected from the container. When the authentication article said "the factory is injected through the plugin's service provider", this is that sentence, in code.

On Joomla 4 and 5 the same provider looks different: lazy() does not exist yet, and the dispatcher is the plugin's first constructor argument - new Joomla($container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin(...))
, registered with a plain set(). From Joomla 6.1 the constructor takes only the plugin configuration, and plugins that need the dispatcher receive it through setDispatcher().

Back to top

6. Inside a Component Provider

6.1 com_content, Abridged

Components wire more, but with ready-made building blocks. The real com_content provider:

$container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content'));
$container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content'));

$container->set(ComponentInterface::class, function (Container $container) {
    $component = new ContentComponent(
        $container->get(ComponentDispatcherFactoryInterface::class)
    );
    $component->setMVCFactory($container->get(MVCFactoryInterface::class));
    // ...category factory, router factory, HTML registry...
    return $component;
});

6.2 The Four Standard Factories

Factory providerGives the component
MVCFactory Builds its models, views, and controllers by namespace - the bootComponent('com_content')->getMVCFactory() you have seen throughout this series ends here.
ComponentDispatcherFactory Builds the dispatcher that runs a request through the component.
RouterFactory Builds the SEF router (the SEO article's URL machinery).
CategoryFactory Category handling for components that use categories.

For your own component, the provider is mostly these four lines with your namespace - the container does the rest. You add custom recipes only when you have custom services, which is exactly section 8.

Back to top

7. Consuming Services the Right Way

7.1 The Ladder of Preference

  1. Constructor injection: declare the dependency; the factory or your provider hands it in. The default for your own classes.
  2. Framework setters and traits: Joomla's base classes offer DatabaseAwareTrait ($this->getDatabase()), getApplication(), getUserFactory() in plugins - fed by the provider, consumed without ceremony.
  3. Factory::getContainer() directly: only at boundaries where injection cannot reach - a legacy integration point, a quick CLI script. Inside normal classes it is the old grabbing pattern wearing a new coat (the service locator anti-pattern).

7.2 In Practice, in a Model

use Joomla\Database\DatabaseInterface;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;

class MessagesModel extends BaseDatabaseModel
{
    public function getMessages(): array
    {
        $db = $this->getDatabase();   // injected by the MVCFactory, not grabbed

        // query as in the ACL and authentication articles: bind, don't concatenate
    }
}

7.3 The Migration Rule of Thumb

Modernising old code? Replace every Factory::getDbo() and Factory::getUser() inside classes with the injected equivalent (DatabaseInterface, UserFactoryInterface). These are the deprecated shortcuts: modern Joomla code should obtain the dependency through injection rather than reaching for it through Factory.

Factory::getApplication() is a different case. It carries no deprecation marker, and inside a service recipe it remains the normal way to reach the application - the core plugin providers do exactly that. Inside your own classes, still prefer the injected application (setApplication() from the provider, getApplication() from the aware trait).

Back to top

8. Real-World DI Recipes

8.1 Register Your Own Service

A component with an export helper other classes need? Register it once in your provider:

$container->share(
    ExportService::class,
    fn (Container $c) => new ExportService($c->get(DatabaseInterface::class))
);

Models then ask for ExportService::class - one construction recipe, no duplicated wiring.

8.2 Swap an Implementation

Code depends on ExportInterface::class; the provider decides which implementation backs it. Development registers a dummy PDF renderer, production the real one, and no consuming class changes - the payoff of asking instead of grabbing.

8.3 Wrap a Third-Party SDK

An external API client (payment, CRM, mail service) belongs in the container as a shared service: configuration read once, one instance, and every consumer receives it injected. When the SDK changes, one recipe changes.

8.4 Decorate an Existing Service

extend() wraps a recipe without replacing it - add logging around an existing service, or wrap a factory with caching - useful in integration scenarios where you own neither the service nor its consumers.

Back to top

9. Under the Hood (Developer View)

9.1 What a Recipe Really Is

Internally the container stores closures keyed by id, with two flags: shared (cache the first result) and protected (refuse overwrites). get() runs the closure - passing the container itself, so recipes can fetch their own dependencies - and alias() simply points a second key at the same entry. No magic: a map of names to build instructions.

9.2 buildObject: Reflection Wiring

For classes without a recipe, buildObject() can inspect a constructor via reflection and resolve each type-hinted parameter from the container - convenient for tools and tests, though core extensions prefer explicit recipes: explicit wiring is readable wiring.

9.3 The Container Hierarchy at Runtime

core container            (Factory::createContainer + core providers)
├─ child: com_content    (its services/provider.php)
├─ child: mod_menu       (its provider)
└─ child: plg_auth_joomla (its provider)

Lookups fall through child to parent - an extension sees everything the core registered, while its own registrations stay in its sandbox. If that pattern sounds familiar: it is the same parent-fallback idea as template inheritance in the template overrides article, applied to services.

9.4 Why the old Factory shortcuts are on the way out

Joomla historically exposed many services through static Factory::get*() methods. During the Joomla 4.x cycle, several of these shortcuts were deprecated. They are still there: the tags first named Joomla 6 as the removal release, that removal was deferred, and in the Joomla 6.1 source the same tags name Joomla 7.

This was not merely an API cleanup. Static service access hides a class's dependencies, makes substitution harder, and complicates isolated testing. Joomla's move toward interfaces, service providers and dependency injection makes those dependencies explicit.

In the Joomla 6.1 source the tags read @deprecated 4.3 will be removed in 7.0 (getMailer() deprecated since 4.4, createConfig() since 4.0). Note what that history means in practice: Joomla's backward compatibility policy allows the release leadership to defer a removal, and it has done so once already for these methods. Treat them as technical debt without a guaranteed expiry date rather than a fixed deadline - extensions that still grab will break whenever the removal does land; extensions that ask will not notice the release. When auditing an extension (the security hardening article's quality checks), a quick grep for Factory::get inside class methods is a remarkably honest quality signal.

Back to top

10. Testing: the Payoff

10.1 The Untestable Version

The section 2 "before" class is difficult to unit test in isolation: Factory::getDbo() couples the test to Joomla's global environment. Testing it means testing all of Joomla.

10.2 The Testable Version

public function testCountReturnsDatabaseResult(): void
{
    $db = $this->createMock(DatabaseInterface::class);
    $db->method('loadResult')->willReturn('42');
    $db->method('setQuery')->willReturnSelf();

    $counter = new ArticleCounter($db);

    $this->assertSame(42, $counter->count());
}

No Joomla, no database, milliseconds per test. This is not a nice-to-have: it is the difference between extensions whose logic is verified on every change and extensions whose logic is verified by customers finding bugs. DI is what makes the first kind possible.

Back to top

11. DI and the Web Services API

The API application is built from the same container as everything else - the Application service provider registers all four applications, and an API request boots components through the same child-container mechanism as a site request. That is why the Web Services API article could say "the same models, the same ACL, the same behaviour": it is literally the same wiring, asked for by a different door.

For extension developers this means API support costs no extra DI work: the api/components/ code from the file structure article consumes the same services your site code does, injected the same way. One provider, four applications - the architecture's whole promise in one sentence. And it reaches further than the web: console commands are registered as container services, and scheduler task plugins boot through the same provider mechanism - the CLI rescue kit from the troubleshooting article and the maintenance tasks from the scheduled tasks article run on this exact wiring.

Back to top

12. SEO and Metadata

Dependency injection has no direct SEO surface - no output, no URLs, no metadata. The connection is indirect but real, and it echoes the authentication article's honest framing: architecture quality becomes site quality. Extensions built on injected, interface-based wiring survive Joomla upgrades without emergency downtime (downtime is an SEO event, as the troubleshooting article showed), and their testability means fewer of the broken-output bugs that quietly damage crawling and rendering.

When you evaluate extensions for a project - the selection frameworks from the security and architecture articles - modern DI-based structure is one of the cheapest quality signals to check: open the package, look for services/provider.php and namespaced src/ classes, grep for deprecated static calls. Five minutes that predict years of upgrade behaviour.

Back to top

13. Common Mistakes and Pitfalls

13.1 The Container as a Global Grab-Bag

Symptom: Factory::getContainer()->get(...) scattered through models and helpers.

Fix: that is service location - the old pattern with more typing. Dependencies enter through constructors and providers; the container is touched at boundaries only.

13.2 Copying Joomla 3 Tutorials

Symptom: new code with JFactory::getDbo(), helper files, and no provider - from a tutorial that never mentions its Joomla version.

Fix: the file structure article's warning applies to code too: check the date and the patterns. services/provider.php plus namespaced src/ marks current material.

13.3 The Missing Provider

Symptom: a freshly built extension installs but never runs - no error, nothing.

Fix: without services/provider.php registering ComponentInterface/ModuleInterface/PluginInterface, Joomla has nothing to boot. The manifest must list the services folder too - the file structure article's manifest rule.

13.4 Everything Eager, Nothing Shared

Symptom: heavy objects built on every request, or - the opposite - a "fresh" service that turns out to be shared state.

Fix: choose deliberately: share() for connections and single instances, set() for per-use objects, lazy() so construction waits until first use - as the core plugin providers do.

13.5 Type-Hinting the Concrete Class

Symptom: a constructor demands MysqliDriver instead of DatabaseInterface - and breaks on sites using another driver.

Fix: ask for the interface; let the container decide the implementation. The interface is the promise; the class is a detail.

13.6 Testing Through the Full Stack Only

Symptom: an extension's "tests" all need a live site, so nobody runs them.

Fix: with injection in place, unit tests with mocks (section 10) run anywhere in milliseconds - keep full-stack tests for the few paths that genuinely need them.

13.7 The Circular Dependency

Symptom: service A's recipe needs service B, whose recipe needs service A - and construction chases its own tail.

Fix: a cycle is an architecture message, not a container limitation. Extract the part both services need into a third service they both depend on, and the circle opens into a chain.

Back to top

14. Best Practices

If you remember only a few things from this article, remember these:

  • Classes ask via constructors; providers answer via recipes; the container only connects the two.
  • Depend on interfaces (DatabaseInterface, UserFactoryInterface), never on concrete drivers or deprecated statics.
  • One services/provider.php per extension: the four standard factories for components, lazy() plugin construction as the core does it.
  • share() connections, set() throwaways, protect() what must not be overridden, extend() instead of replacing.
  • Treat remaining Factory::get*() calls as technical debt and migrate them toward injected services where appropriate.
  • Read libraries/src/Service/Provider/ and the core extension providers as your reference library - they are the patterns, verified.
  • Write at least a few unit tests with mocked dependencies - they are the proof your wiring is real DI and not decoration.
  • Watch constructor size: a class demanding half a dozen services has too many jobs - split it before wiring it.
Back to top

15. Quick Reference

IDEA         grab (JFactory::getDbo)  →  ask (constructor param)
             = Dependency Inversion Principle (the D in SOLID)
             deprecated 4.3, tagged for removal in Joomla 7

CONTAINER    set(id, fn)      fresh instance per get()
             share(id, fn)    build once, reuse (db, app)
             get / has        fetch / check
             alias(a, id)     second name, same recipe
             protect / extend no-overwrite / decorate
             createChild()    inheriting sandbox
             lazy(class, fn)  defer construction (6.1+, PHP 8.4)

PROVIDERS    core: libraries/src/Service/Provider/  (~2 dozen)
             extension: services/provider.php returns
             anonymous ServiceProviderInterface

BOOT         core container (Factory::createContainer)
             > per-extension createChild()
             > provider registers Component|Module|PluginInterface
             > Joomla pulls the instance out

COMPONENT    MVCFactory + ComponentDispatcherFactory
             + RouterFactory + CategoryFactory ('\\Vendor\\Name')
             + ComponentInterface closure

CONSUME      1. constructor injection      (your classes)
             2. traits/setters             (getDatabase, getUserFactory)
             3. Factory::getContainer()    (boundaries only!)

TESTS        mock the interface, inject, assert - no Joomla needed

AUDIT        open package: services/provider.php? src/?
             grep Factory::get inside classes = quality signal
Back to top

16. Summary

Dependency injection in Joomla is one idea and a small toolkit around it:

  • The idea: classes ask for dependencies through constructors instead of grabbing globals - visible needs, swappable implementations, testable logic.
  • The container stores build recipes under interface names: set, share, alias, protect, extend, lazy.
  • Service providers package recipes: about two dozen wire the core; every extension ships one in services/provider.php.
  • Extensions boot in child containers - inheriting every core service, sandboxed from each other, the same parent-fallback pattern as template inheritance.
  • Consumption has a ladder: constructor injection first, framework traits second, direct container access at boundaries only.
  • The debt is real: the static Factory shortcuts are deprecated and tagged for removal in Joomla 7 - a date that has already slipped once, so migrate on your own schedule instead of waiting for a deadline. Modern wiring is not a style preference, it is upgrade insurance.

Once you see the pattern - ask, register, inject - the modern Joomla codebase stops being intimidating and starts being consistent: every extension boots the same way, every service is found the same way, and every class tells you honestly what it needs.

And if you maintain extensions that still grab - your own, a predecessor's, or a business-critical one whose developer left - the migration to injected wiring is methodical work: inventory the static calls, introduce the provider, inject one dependency at a time, test as you go. Done early it is a planned project measured in days; done under upgrade pressure it is an emergency measured in downtime - and the tested, injected result is cheaper to maintain every year after that.

Back to top
Dependency Injection in Joomla: The DI Container Explained
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.