Skip to content

Commit 5046255

Browse files
authored
Merge pull request #162 from robbiejackson/dependency-injection
Dependency Injection done
2 parents 47fc084 + c8bd88a commit 5046255

File tree

10 files changed

+422
-0
lines changed

10 files changed

+422
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
title: The Dependency Injection Container
3+
sidebar_position: 2
4+
---
5+
# The Dependency Injection Container
6+
The Joomla Dependency Injection Container, DIC for short, is basically a repository of key-value pairs where:
7+
- the key is a string which is (usually) the fully qualified name of a class or interface
8+
- the value is an instance of the relevant class, or a function that returns an instance of that class.
9+
10+
It doesn't *really* matter what the key is set to - as long as it's intelligible and you use the same key for putting entries in and taking entries out. Of course, it has to be unique.
11+
12+
![DIC](_assets/dic.jpg "DIC")
13+
14+
You put things into the container using `set()` passing:
15+
- the class name or interface name
16+
- a function which returns an instance of the class (or the value can be just an instance of the class, without the enveloping function)
17+
- a boolean defining whether the class instance may be shared or not (ie if there is a second request to the DIC to supply that instance, does it return the same instance or a new one)
18+
- a boolean defining whether this entry into the DI container is protected or not (an error will be raised if you try to overwrite a protected entry by calling `set()` again using the same key).
19+
20+
The function `share()` is basically the same as `set()` with the shared boolean set to true.
21+
22+
You get things out of the container by calling `get()` passing the key of the resource you want. The DI functionality will
23+
- find the key in the container
24+
- if the value isn't already a class instance, then it will run the associated function to generate an instance of the class
25+
- if the resource is shared, it will store the class instance, so that on subsequent invocations of `get()` it can just return the instance
26+
- return the class instance to you
27+
28+
You can also define aliases for each key in the container, which means that you call `get()` passing either the key or an alias of the key.
33.8 KB
Loading
7.61 KB
Loading
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: Basic Concept
3+
sidebar_position: 1
4+
---
5+
# Basic Concept
6+
The basic idea behind Joomla dependency injection is that developers should no longer get access to key Joomla class instances by calling
7+
```php
8+
$obj = new JoomlaClass();
9+
or
10+
$obj = JoomlaClass::getInstance();
11+
```
12+
Instead they should make a request to the Dependency Injection Container (DIC):
13+
```php
14+
$obj = $container( – please give me an instance of JoomlaClass – );
15+
```
16+
or get an instance via the Application class or a Factory class which has been obtained from the DIC.
17+
18+
The main reason for this is to enable better testing by making it easier to mock classes. If the code has multiple calls to `JoomlaClass::getInstance()` then it's hard to mock that class for testing.
19+
20+
If instead the code always uses the DIC to get an instance of the class, then to mock it all we have to do is put the mocked class into the DIC instead of the real one.
21+
To make it less obvious that a specific class is being requested we can ask instead for a class which meets a particular interface:
22+
```php
23+
$obj = $container( – please give me an instance of a class that implements JoomlaInterface – );
24+
```
25+
The DIC will then return either the real class or the mocked class, depending upon how it has been configured. However, using a class name versus an interface name has no bearing on the functionality; as we shall see, the key used for putting things into and getting things out of the DIC is just a string, so we just have to ensure that we use the same key string for both operations.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
title: Dependency Injection Issues
3+
sidebar_position: 7
4+
---
5+
# Dependency Injection Issues
6+
Dependency Injection was introduced in Joomla 4, with the intention of removing direct access to most key classes via `getInstance()` calls, including via `Factory::` calls, and many of these API calls are deprecated in Joomla 4. However, not all `getInstance()` calls are deprecated; examples which are not deprecated include `Uri::getInstance()` and `Filter\InputFilter::getInstance()`.
7+
8+
## Toolbar::getInstance()
9+
The `Toolbar` class is used for managing the buttons on the forms within the Joomla administrator back-end. The [Toolbar API](https://api.joomla.org/cms-4/classes/Joomla-CMS-Toolbar-Toolbar.html) documentation seems to suggest that we should replace
10+
```php
11+
$bar = Toolbar::getInstance('toolbar');
12+
```
13+
with
14+
```php
15+
$bar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar');
16+
```
17+
However, this currently won't work. The problem is that all the other Joomla code uses `getInstance('toolbar')` to access the Toolbar class, including the code in administrator/modules/mod_toolbar/mod_toolbar.php which renders the toolbar. The `getInstance` method keeps track of the Toolbar instances in a local static variable `$instances`, and returns the same Toolbar instance on repeated invocations.
18+
19+
So if you use the second approach above you will get a Toolbar instance, but it won't be the same Toolbar instance as is stored in the `$instances` variable. It won't then be picked up by the Toolbar module, and so won't be rendered on the form.
20+
21+
## Table::getInstance()
22+
Usually a Table instance is created by the MVCFactory class by calling `getTable()` in your Model. However, there are other cases where you may want one:
23+
- in your Table class code you may want another instance of your own component table to verify if an `alias` field entered by a user already exists
24+
- you may wish to access the Table class of another component - eg of com_categories.
25+
26+
Neither of these is possible via your component's MVCFactory class as
27+
- it creates Table classes for your component alone, and,
28+
- in your Table code by default you've no pointer back to MVCFactory class instance
29+
30+
However, you can obtain instances by a more convoluted route, eg for com_content
31+
```php
32+
Factory::getApplication()->bootComponent('content')->getMVCFactory()->createTable($name, $prefix, $config);
33+
```
34+
You can also use this method to boot your own component, to get another instance of your own Table.
35+
36+
## Categories::getInstance()
37+
If you use Categories in your component then you may want to use the Categories API, for example in a custom router, or in a site CategoryModel. The old method of calling
38+
```php
39+
$categories = Categories::getInstance(...);
40+
```
41+
is deprecated.
42+
43+
You can get a CategoryFactory from your child DIC when you instantiate your Extension, but unfortunately it's not passed down via the MVCFactory class to your Model. One possible solution is to store the CategoryFactory reference as a static variable of your Extension class, as described in [Implementing Categories in your Component](../../using-core-functions/categories/implementing-categories-in-components.md).
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
title: Extensions and Child Containers
3+
sidebar_position: 4
4+
---
5+
# Extensions and Child Containers
6+
Whenever Joomla boots an extension it creates a child DIC, solely for use of that extension. It's shown diagrammatically below.
7+
8+
![Child DIC](_assets/child-dic.jpg "Child DIC")
9+
10+
The child DIC has a `parent` pointer to the main DIC, and acts similarly to the main DIC, but not exactly:
11+
- Whenever `set()` is called on this child DIC the key-value pair is inserted into the child container.
12+
- Whenever `get()` is called on it, the resource is obtained from the child container, but if it's not found there then the parent container is searched also.
13+
14+
From Joomla version 4 the Joomla developers are requesting that extension developers use dependency injection for their extensions by defining a services/provider.php file. The loading of an extension is then a two-step process, handled within the services/provider.php file::
15+
1. The extension class is entered into child DIC
16+
2. The extension class is retrieved from the child DIC, creating an instance of the class
17+
18+
Let's look at a minimal example of this for com_example, with namespace Mycompany\Component\Example.
19+
```php
20+
use Joomla\CMS\Extension\ComponentInterface;
21+
use Joomla\DI\Container;
22+
use Joomla\DI\ServiceProviderInterface;
23+
use Mycompany\Component\Example\Administrator\Extension\ExampleComponent;
24+
return new class implements ServiceProviderInterface {
25+
public function register(Container $container): void
26+
{
27+
$container->set(
28+
ComponentInterface::class,
29+
function (Container $container) {
30+
$component = new ExampleComponent();
31+
return $component;
32+
}
33+
);
34+
}
35+
};
36+
```
37+
We can see that when Joomla does a `require` of this php file, then it will return a class instance:
38+
```php
39+
$provider = require $path; // $path points to the relevant services/provider.php file
40+
```
41+
The variable `$provider` then points to an object which is an instance of this anonymous class. Also the class implements `Joomla\DI\ServiceProviderInterface`, which basically means that it has a method called `register` with the above signature.
42+
43+
When next Joomla does
44+
```php
45+
if ($provider instanceof ServiceProviderInterface) {
46+
$provider->register($container);
47+
```
48+
the `register` function will be called, which will put into the child DIC an entry with
49+
- key = ComponentInterface::class – this is just a PHP shorthand way of specifying the string 'Joomla\CMS\Extension\ComponentInterface'
50+
- value = a function which returns a new instance of ExampleComponent, which is the [Extension class](../extension-and-dispatcher/extension-component.md) of `com_example`
51+
52+
Then when Joomla executes the code:
53+
```php
54+
$extension = $container->get($type);
55+
```
56+
the function above will run and will return a new instance of `ExampleComponent`.
57+
58+
You can follow through the Joomla code yourself in libraries/src/Extension/ExtensionManagerTrait.php, and confirm that the above pattern also applies to modules and plugins.
59+
60+
Notice also the following line in that file:
61+
```php
62+
if ($extension instanceof BootableExtensionInterface) {
63+
$extension->boot($container);
64+
}
65+
```
66+
So if the Extension class implements `BootableExtensionInterface` then Joomla will immediately call the `boot` function of the Extension instance, as described in the [Extension documentation](../extension-and-dispatcher/extension-component.md).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: Dependency Injection
3+
sidebar_position: 3
4+
---
5+
Dependency Injection was introduced in Joomla 4 to try to enable better testing of the Joomla code. If you're not familiar with the concepts of dependency injection then you may find the following videos useful: [Dependency Injection in PHP](https://www.youtube.com/watch?v=igx3bIl1T_c) and [DI Container](https://www.youtube.com/watch?v=78Vpg97rQwE). The videos are not related to Joomla, but do present good background information.
6+
7+
There are also a couple of Joomla-related videos, covering [Joomla 4 Dependency Injection](https://youtu.be/Y2oe73C1Q54) and [services/provider.php](https://youtu.be/n2fdf2szI_A).
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: JConfig Example
3+
sidebar_position: 3
4+
---
5+
# JConfig Example
6+
Early in its initialisation phase Joomla initialises the dependency injection container (DIC) by calling `createContainer()` which is in library/src/Factory.php. This results in the `register()` function being called in each of the class files in libraries/src/Service/Provider. By looking through those files you can see what gets put into the DIC upon initialisation.
7+
8+
Let's look at library/src/Service/Provider/Config.php, which sets up the configuration class `JConfig`. Here's what gets executed when the `register()` function is called:
9+
```php
10+
$container->alias('config', 'JConfig')
11+
->share(
12+
'JConfig',
13+
function (Container $container) {
14+
if (!is_file(JPATH_CONFIGURATION . '/configuration.php')) {
15+
return new Registry();
16+
}
17+
18+
\JLoader::register('JConfig', JPATH_CONFIGURATION . '/configuration.php');
19+
20+
if (!class_exists('JConfig')) {
21+
throw new \RuntimeException('Configuration class does not exist.');
22+
}
23+
24+
return new Registry(new \JConfig());
25+
},
26+
true
27+
);
28+
```
29+
Here's an explanation of the code:
30+
```php
31+
$container->alias('config', 'JConfig')
32+
```
33+
This sets up an alias in the DIC so that you can call `$container->get('config')` as well as `$container->get('JConfig')`. Notice that the return value of this function is again the `$container` so that you can chain function calls.
34+
35+
Next there's a call to `share()` passing 3 parameters:
36+
1. the string 'JConfig'
37+
2. a function which returns something, which will look at shortly
38+
3. `true` – for the `protected` parameter, which means that if another call is made to try and put an entry with key 'JConfig' into the DIC then it will be rejected (rather than overwriting this entry).
39+
40+
Remember that `share()` is the same as `set()` with the `shared` parameter set to `true`, so that the same class instance is going to be returned for each `get('JConfig')` call.
41+
42+
When some part of the Joomla code calls
43+
```php
44+
$container->get('JConfig')
45+
```
46+
then the function passed as parameter 2 above will be executed.
47+
48+
The Joomla global configuration is held in the `JConfig` class in configuration.php in the top level folder of your Joomla instance, so that function does the following:
49+
1. Checks if configuration.php exists, and if it doesn't just return an empty set of configuration data
50+
2. Registers the 'JConfig' class in configuration.php, so that the autoloader knows where to find it
51+
3. Checks if the class exist – this will force PHP to call the Joomla autoloader function, which will then do a `require` of configuration.php. If the `JConfig` class still doesn't exist, then an exception will be raised.
52+
4. The `JConfig` class is instantiated, and passed into a Registry object which is then returned.
53+
54+
As this DIC entry is shared, the Registry instance will be stored in the DIC and will be returned on subsequent calls to `get('JConfig')`.
55+
56+
So you can obtain the global configuration parameters by
57+
```php
58+
use Joomla\CMS\Factory;
59+
$container = Factory::getContainer();
60+
$container->get('JConfig'); // or ...
61+
$container->get('config');
62+
```
63+
These calls go directly to the DIC to obtain the shared JConfig instance.
64+
65+
Or you can obtain them via the Application instance, which has already got them from the DIC:
66+
```php
67+
use Joomla\CMS\Factory;
68+
$application = Factory::getApplication();
69+
$application->getConfig(); // or, to get a particular parameter:
70+
$application->get($paramName, $defaultValue);
71+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
title: Modules and Plugins
3+
sidebar_position: 6
4+
---
5+
# Modules and Plugins
6+
The service provider files for modules and plugins are considerably more straightforward than those for components. If you look through your Joomla instance modules and plugins folders then you'll find several examples of services/provider.php files.
7+
8+
## Modules
9+
For example, for mod_breadcrumbs in Joomla 5 we have:
10+
```php
11+
use Joomla\CMS\Extension\Service\Provider\Module;
12+
13+
public function register(Container $container): void
14+
{
15+
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Breadcrumbs'));
16+
$container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Breadcrumbs\\Site\\Helper'));
17+
18+
$container->registerServiceProvider(new Module());
19+
}
20+
```
21+
This module uses the standard Joomla\CMS\Extension\Service\Provider\Module class to generate its extension class \Joomla\CMS\Extension\Module.
22+
23+
It has 2 dependencies:
24+
1. It uses a ModuleDispatcherFactory class to instantiate its own Dispatcher.php in src/Dispatcher/Dispatcher.php
25+
2. It uses a HelperFactory to find its helper file in src/Helper/BreadcrumbsHelper.php
26+
27+
## Plugins
28+
For example for the custom field color plugin in plugins/fields/color
29+
```php
30+
use Joomla\Plugin\Fields\Color\Extension\Color;
31+
32+
public function register(Container $container)
33+
{
34+
$container->set(
35+
PluginInterface::class,
36+
function (Container $container) {
37+
$plugin = new Color(
38+
$container->get(DispatcherInterface::class),
39+
(array) PluginHelper::getPlugin('fields', 'color')
40+
);
41+
$plugin->setApplication(Factory::getApplication());
42+
43+
return $plugin;
44+
}
45+
);
46+
}
47+
```
48+
The plugin's Extension class is Color, which gets instantiated with 2 parameters being passed into its constructor:
49+
- the EventDispatcher class – this is obtained from the parent DIC, having been originally put into it from libraries/src/Service/Provider/Dispatcher.php when Joomla initialised
50+
- the plugin stdClass object which Joomla has used to store plugin data (id, name, type and params).
51+
52+
This is a standard pattern that you can use for your own plugins. You can obviously get the Application instance by calling `Factory::getApplication` either in the services/provider.php file or in your standard plugin code.

0 commit comments

Comments
 (0)