Dependency injection
1Overview
1.1Principle
Dependency injection allows decoupling logic between objects by relying on the principle of inversion of control.
Temma automatically creates a component (called the “loader”) to manage dependency injection for business objects.
This component is the backbone on which all objects in your application can rely to access one another.
It is available in controllers through the private attribute $_loader.
The loader can automatically instantiate the objects requested from it, but it can also contain values (scalars or objects) that are explicitly provided to it.
The nominal behavior of the loader is to always provide the same instance for a given object type.
When an object is requested from the loader:
- If the loader does not yet know this object, it instantiates it, stores the instance in its cache, and returns it.
- If the loader already knows this object (either because it was provided or because the loader already instantiated it), the loader returns the instance.
1.2Usage
The loader can be used in two different ways: as a service locator, or through autowiring.
Service locator
This is the typical usage in controllers. The loader is used to request the necessary objects. In your controllers, you use the loader to retrieve the instances of the objects you need.
Usage example:
// in a controller => service locator
class MyController extends \Temma\Web\Controller {
public function __invoke() {
// using a business object through the loader
$this['data'] = $this->_loader->MyObject->process();
}
}
- Line 5: The loader is used to retrieve an instance of the MyObject object, on which the process() method is called. If the loader already has this instance cached, it returns it; otherwise, it instantiates the object before returning it.
Autowiring
Your objects (excluding controllers) receive their dependencies as parameters of their constructors. If necessary, the loader will instantiate the expected objects before providing them to the constructor.
// in a business object => autowiring
class MyObject {
// Dependency injection via autowiring.
// The loader first instantiates the objects that must be passed to the constructor.
public function __construct(
private UserDao $userDao,
private BucketService $bucketService
) {
}
// using dependencies in other methods
public function process() : array {
$users = $this->userDao->getList();
$buckets = $this->bucketService->getFromUsers($users);
return $buckets;
}
}
2Available data
By default, the component contains the following elements:
- $loader->session : Session management object (see sessions).
- $loader->config : Configuration management object, which gives access to all configuration directives.
- $loader->request : Incoming request management object, which allows manipulation of the framework execution flow.
- $loader->response : Object used by the framework to manage the response sent to the client.
- $loader->controller : Instance of the plugin or controller currently in use.
Connections to data sources are accessible in two different ways:
-
$loader->dataSources is a Registry that contains the connection objects
(see the documentation for controllers).
For example, a MySQL connection named db will be accessible using $loader->dataSources->db. -
If the name of the data source is not already present in the component (such as session, config, etc.),
it is directly accessible from the component.
For example, a MySQL connection named db will be accessible using $loader->db.
3Alternative access
In the previous example, the loader (in service locator mode) was used assuming that the objects managed by the component are placed in the root namespace (in other words, they are not in an explicit namespace), and that their source files are placed in the lib/ directory of the project.
But sometimes, you will want to use objects that are located in deep namespaces. In this case, objects must be accessed using an associative array syntax, rather than an object-oriented one.
For example, if you want to use the add() method of the \Math\Base\Calculator object, you must write:
$res = $this->_loader['\Math\Base\Calculator']->add(3, 4);
It is also possible to use the get() method:
$res = $this->_loader->get('\Math\Base\Calculator')->add(3, 4);
You will see below how to simplify this syntax by using aliases and prefixes.
4Storage
The data stored in the loader is identified by a key (most of the time, this is the object type). If a backslash character (\) is present at the beginning of the key, it is removed.
This processing exists so that the following three notations are equivalent:
$res = $loader['\Math\Base\Calculator']->add(3, 4);
$res = $loader['Math\Base\Calculator']->add(3, 4);
$res = $loader[\Math\Base\Calculator::class]->add(3, 4);
5Loader configuration
5.1Preloading
It is possible to configure the loader in order to provide it with values that it will not have to instantiate itself (or that it would not be able to instantiate).
Thus, in the etc/temma.php file, you can define fixed values that will be accessible throughout the application:
<?php
return [
'x-loader' => [
'preload' => [
// adds a ZipArchive instance
'zipManager' => new ZipArchive(),
// adds a string retrieved from the environment
'appPassword' => getenv('APP_PASSWORD'),
// adds a number that will be globally accessible
'maxArticles' => 100,
]
]
];
These values will be directly usable by the loader.
Thus, it will be possible to write:
print("Maximum number of articles: " . $this->_loader->maxArticles);
A value registered in this way will also be usable by the loader in the context of autowiring.
You will find more information about explicitly adding data in the dedicated section.
5.2Aliases
It is possible to define naming aliases, which will be used when an object is called through the loader.
For example, with the following etc/temma.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserService' => '\MyApp\User\UserService',
'TµEmail' => '\Temma\Utils\Email',
]
]
];
You will be able to write the following code:
$list = $this->_loader->UserService->getList();
// is equivalent to
$list = $this->_loader['\MyApp\User\UserService']->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);
// is equivalent to
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
You will find more information about aliases in the dedicated section.
5.3Prefixes
It is also possible to define naming prefixes, which summarize namespaces (or parts of namespaces). These prefixes can then be used at the beginning of the name of an object managed by the loader.
Example of an etc/temma.php file:
<?php
return [
'x-loader' => [
'prefixes' => [
'global' => '\MyApp',
'App' => '\OtherApp\Extension\OtherApp\\',
'€x' => '\Europa\Source\Service\\',
]
]
];
You will then be able to write the following code:
$object = $this->_loader->globalArticles;
// equivalent to
$object = $this->_loader['\MyAppArticles'];
$object = $this->_loader->AppWidget;
// equivalent to
$object = $this->_loader['\OtherApp\Extension\OtherApp\Widget'];
$object = $this->_loader->€xUser;
// equivalent to
$object = $this->_loader['\Europa\Source\Service\User'];
You will find more information about prefixes in the dedicated section below.
6Autowiring: decoupling
6.1Autowiring principle
Autowiring is the ability of the loader to detect the dependencies that an object expects in its constructor.
Thus, any object can be called through the loader. On the first call, the loader will instantiate the object, passing its dependencies as parameters. If the loader does not already contain instances of these dependencies, it will create them on the fly.
Here is an example of two objects, one being a dependency of the other:
/**
* Object used to compute hashes from identifiers.
*/
class Hasher {
/**
* Method that returns a hash from a string.
* @param string $text Input text.
* @return string Computed hash.
*/
public function hash(string $text) : string {
return hash('sha256', $text);
}
}
/**
* Object managing users in the database.
*/
class UserDao {
/**
* Constructor.
* @param \Temma\Datasources\Redis $ndb Database connection.
* @param Hasher $hasher Hashing object.
*/
public function __construct(
private \Temma\Datasources\Redis $ndb,
private Hasher $hasher,
) {
}
/**
* Returns information about a user.
* @param int $id User identifier.
* @return array Associative array.
*/
public function get(int $id) : array {
$user = $this->ndb["user-$id"];
$user['hash'] = $this->hasher->hash($user['email']);
return $user;
}
}
- Lines 4 to 13: Utility object Hasher, which has no dependencies.
-
Lines 18 to 40: UserDao object.
- Lines 23 to 27: Constructor, which expects two dependencies.
- Line 35: Use of the Redis database connection.
- Line 36: Use of the Hasher object.
And here is the controller code that calls the UserDao object:
class Account extends \Temma\Web\Controller {
public function show(int $id) {
$this['user'] = $this->_loader->UserDao->get($id);
}
}
- Line 3: The loader is used to call the UserDao object. An instance is created on the fly, and to create it, the loader uses the existing Redis database connection and creates an instance of the Hasher object.
6.2Dependency management
When the loader automatically instantiates an object, it iterates over the parameters expected by the constructor.
For a given parameter (for example \App\UserManager $userManager), there are three possible cases:
- If the loader already contains an entry with the type name (for example \App\UserManager), it is used.
- If there is an entry with the parameter name (for example $userManager), it is used.
- If the parameter type (for example \App\UserManager) is instantiable, the loader creates an instance and uses it.
A dependency does not necessarily have to be an instantiable object. For example, if the loader contains a string name, and a constructor contains a parameter string $name, the loader will use this value.
7Service locator: performance optimization
While autowiring is very simple and convenient to use, it has the drawback of being costly in terms of performance.
For an application where performance is critical, you can favor the service locator approach.
In this case, an object will not receive its dependencies as constructor parameters. Instead, it will receive the loader,
which it will then use directly to access the objects it needs.
To do so, it must implement the \Temma\Base\Loadable interface, which requires that its constructor take only a single parameter, of type \Temma\Base\Loader.
Here is an example of an object that uses the loader to access its dependencies:
class SpecialLogger implements \Temma\Base\Loadable {
/** Constructor. */
public function __construct(private \Temma\Base\Loader $loader) {
}
/**
* Method that appends lines to the file 'var/list.txt'.
* @param string $text Text to write.
*/
public function write(string $text) : void {
$dest = $this->loader->config->varPath . '/list.txt';
file_put_contents($dest, "$text\n", FILE_APPEND);
}
/**
* Method that uses the OtherObject object.
*/
public function process() : void {
$value = $this->loader->OtherObject->doCalculation();
$this->write($value);
}
}
- Line 1: The object implements the \Temma\Base\Loadable interface.
- Line 3: Object constructor, which receives an instance of the dependency injection component as a parameter. It is stored as a private property.
- Line 11: In the write() method, $this->loader->config is used to access the configuration object (and its varPath property).
-
Line 19: In the process() method, $this->loader->OtherObject is called.
If the loader already had an instance of OtherObject, it is used; otherwise, a new instance
is created and returned.
OtherObject may use autowiring or implement the \Temma\Base\Loadable interface.
For this object to be loadable by the loader, it must be accessible through the project include paths. By default, this means it must be registered in a file named SpecialLogger.php, placed in the lib/ directory of the project.
From that point on, the object is directly available as if it were a property of the loader. Only one instance of the object will be created (on the first call).
Here is an example of a controller that uses the SpecialLogger object through the loader:
class Homepage extends \Temma\Web\Controller {
/** Root action. */
public function __invoke() {
// write to the file
$this->_loader->SpecialLogger->write('It works');
}
}
- Line 5: The loader is used to access the SpecialLogger object, then its write() method.
Now imagine that we create another business object, loadable through the loader, which uses the SpecialLogger object.
class Calculator implements \Temma\Base\Loadable {
/** Constructor. */
public function __construct(private \Temma\Base\Loader $loader) {
}
/**
* Function that performs an addition.
* @param int $i First operand.
* @param int $j Second operand.
* @return int Result of the addition.
*/
public function add(int $i, int $j) : int {
$result = $i + $j;
$this->_loader->SpecialLogger->write("Computed: $result");
return ($result);
}
}
- Line 3: Object constructor, which stores the loader as a private property.
- Line 14: The loader is used to call the SpecialLogger object.
This illustrates how business objects can call each other, with the component managing access and instantiation.
8Adding data to the loader
8.1Explicit additions
You can manually create an object and add it to the loader by specifying the name you want to give it:
// create the object instance
$calculator = new \Math\Base\Calculator($this->_loader);
// add the instance to the component
// (the three syntaxes are equivalent)
// - object-oriented syntax
$this->_loader->calculator = $calculator;
// - associative array syntax
$this->_loader['calculator'] = $calculator;
// - using the set() method
$this->_loader->set('calculator', $calculator);
It is then possible to retrieve this object from the loader (used as a service locator):
// now that the instance is registered in the component,
// it can be used wherever the component is accessible
$res = $this->_loader->calculator->add(3, 4);
This also works with autowiring (which is based here on the name calculator rather than the type):
class MyObject {
// constructor: receives the dependency previously added to the loader
public function __construct(private \Math\Base\Calculator $calculator) {
}
public function process(int $i, int $j) : int {
// use the private object
return $this->calculator->add($i, $j);
}
}
You can add any kind of element to the loader (not only objects):
$this->_loader->anInteger = 3;
$this->_loader->anArray = ['a', 'b', 'c'];
$this->_loader->anObject = new \Toto();
8.2Callback additions
It is also possible to assign an anonymous function. This function will be executed the first time the loader is accessed; the value it returns will then be stored by the loader, and the anonymous function will never be called again.
The anonymous function receives the loader instance as a parameter. It must return the value that will replace the anonymous function in the loader, and which will be returned by the loader for each subsequent call for the same key.
It is possible to pass a function name, an anonymous function, a static call string (for example 'MyObject::myMethod'), or an array callback on an instantiated object (for example [$object, 'myMethod']).
Here is an example controller:
class Homepage extends \Temma\Web\Controller {
// initialization
public function __wakeup() {
// add the 'calc' key associated with an anonymous function
$this->_loader->calc = function($loader) {
if ($this['param'] == 'base')
return (new \Math\Base\Calculator($loader));
return (new \Math\Other\Calculator($loader));
};
}
// action
public function compute(string $type) {
$this['param'] = $type;
// the anonymous function is executed to generate the 'calc' key
$this['res'] = $this->_loader->calc->add(3, 4);
// the value of 'calc' is directly retrieved
$this['zzz'] = $this->_loader->calc->add(5, 6);
}
// other action
public function compute2() {
$this['res'] = $this->_loader->calc->add(3, 4);
}
}
- Line 3: The __wakeup() method is used to initialize the controller.
- Line 5: The calc key is created in the loader and assigned an anonymous function.
- Lines 6 to 8: The $this variable refers to the controller itself. The template variable param is used to determine which implementation is returned.
- Line 14: The template variable param is assigned the value received in the $type parameter.
- Line 17: The loader is used without caring which implementation is selected.
- Line 19: The loader is used to retrieve the same instance as in the previous call.
- Line 24: The loader is used. Here, the \Math\Other\Calculator object will always be used.
8.3Dynamic callback additions
With callback additions (seen in the previous section), the anonymous function is executed only once, and the value it returns is stored by the loader to replace the anonymous function.
But sometimes, you want the anonymous function to be executed dynamically each time the loader is accessed,
without the returned value being stored.
In that case, you should use the dynamic() method of the loader,
providing it with the key and an anonymous function:
// add a dynamic value to the loader
$loader->dynamic('randomValue', function() {
return mt_rand(0, 255);
});
// display the value several times; it will be regenerated each time
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");
print($loader->randomValue . "\n");
8.4Builder additions
A builder is a function responsible for managing instantiations, and which is registered using
the loader’s setBuilder() method.
This function takes two parameters: the first is the loader instance; the second is the name of the object
that is being accessed.
Here is an example:
// builder definition
$this->_loader->setBuilder(function($loader, $key) {
// check whether the requested object name ends
// with "Service" or with "Dao"
if (str_ends_with($key, 'Service')) {
$className = substr($key, 0, -7);
return new \MyApp\Service\$className($loader);
} else if (str_ends_with($key, 'Dao')) {
$className = substr($key, 0, -3);
return new \MyApp\Dao\$className($loader);
}
});
// this call will use the \MyApp\Service\Mail object
$this->_loader->MailService->send();
// this call will use the \MyApp\Dao\User object
$this->_loader->UserDao->remove($userId);
8.5DAO additions
DAO objects can be instantiated in controllers using their _loadDao() method. Outside of controllers, the dependency injection component can be used to create instances of DAO objects that you have developed.
For example, if you have created a UserDao object (in the lib/UserDao.php file), you can use it as follows:
$user = $this->_loader->UserDao->getFromEmail($email);
9Alias management
9.1Alias definition
As seen above, it is possible to define naming aliases in the configuration file.
You can also declare aliases directly in the loader using the alias() method:
// alias definition
$this->_loader->alias('UserService', '\MyApp\User\UserService');
// alias usage
$list = $this->_loader->UserService->getList();
// is equivalent to
$list = $this->_loader['\MyApp\User\UserService']->getList();
You can also declare multiple aliases by passing an associative array:
// alias array definition
$this->_loader->alias([
'UserService' => '\MyApp\User\UserService',
'TµEmail' => '\Temma\Utils\Email',
]);
// alias usage
$list = $this->_loader->UserService->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);
// is equivalent to
$list = $this->_loader['\MyApp\User\UserService']->getList();
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
It is possible to remove a previously defined alias by providing the value null for the same alias name to the alias() method.
9.2Using aliases for tests
When running automated tests on a Temma application, you may need to replace an object with a mock, a “stub object” that simulates the behavior of the original object.
With naming aliases, it becomes very easy to create a etc/temma.test.php file (if the runtime environment is named test), which redefines the objects loaded for a given name.
Example of an etc/temma.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserService' => '\MyApp\User\UserService'
]
]
];
Example of an etc/temma.test.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserService' => '\MyMock\UserService',
'\Temma\Utils\Email' => '\MyMock\Email',
]
]
];
Your application code may contain:
// this line of code:
$list = $this->_loader->UserService->getList();
// under normal conditions, is equivalent to:
$list = $this->_loader['\MyApp\User\UserService']->getList();
// in the test environment, is equivalent to:
$list = $this->_loader['\MyMock\UserService']->getList();
// this line of code:
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
// in the test environment, is equivalent to:
$this->_loader['\MyMock\Email']->textMail($from, $to, $title, $message);
10Prefix management
10.1Prefix overview
In addition to aliases, it is also possible to define naming prefixes, which summarize namespaces. These prefixes can then be used at the beginning of the name of an object managed by the loader.
Warning: Avoid defining too many prefixes, as they are all checked each time an object is requested for the first time. This can have a performance impact.
10.2Prefix definition
You can declare a prefix using the prefix() method:
// prefix definition
$this->_loader->prefix('global', '\MyApp');
// prefix usage
$object = $this->_loader->globalArticles;
// is equivalent to
$object = $this->_loader['\MyApp\Articles'];
// second prefix
$this->_loader->prefix('App', '\OtherApp\Extension\OtherApp');
// prefix usage
$object = $this->_loader->AppWidget;
// is equivalent to
$object = $this->_loader['\OtherApp\Extension\OtherApp\Widget'];
// third prefix
$this->_loader->prefix('€x', '\Europa\Source\Service');
// prefix usage
$object = $this->_loader->€xUser;
// is equivalent to
$object = $this->_loader['\Europa\Source\Service\User'];
You can also declare multiple prefixes by passing an associative array:
// prefix array definition
$this->_loader->prefix([
'global' => '\MyApp',
'App' => '\OtherApp\Extension\OtherApp',
'€x' => '\Europa\Source\Service',
]);
It is possible to remove a previously defined prefix by providing the value null for the same prefix.
10.3Prefix management with callbacks
When defining a prefix (either via the prefix() method or in the configuration file), it is possible to assign it a value of type callable. In this case, when the element is requested from the loader, the referenced function is executed (with the loader instance and the requested object name stripped of its prefix passed as parameters), and its return value is used as the value associated with the requested name.
Example code using a function:
// function definition returning an object based on configuration
function sinclairManager(\Temma\Base\Loader $loader, string $name) {
if ($name == 'Spectrum')
return new \Sinclair\Computers\ZxSpectrum();
if ($name == '81')
return new \Sinclair\Computers\Zx81();
if ($name == 'QL')
return new \Sinclair\Computers\QuantumLeap();
return (null);
}
// prefix registration
$this->_loader->setPrefix('Zx', 'sinclairManager');
// usage
$computer = $this->_loader->ZxSpectrum;
// is equivalent to
$computer = $this->_loader['\Sinclair\Computers\ZxSpectrum'];
// other usage
$computer = $this->_loader->Zx81;
// is equivalent to
$computer = $this->_loader['\Sinclair\Computers\Zx81'];
// other usage
$computer = $this->_loader->ZxQL;
// is equivalent to
$computer = $this->_loader['\Sinclair\Computers\QuantumLeap'];
It is possible to pass a function name, an anonymous function, a static call string (for example 'MyObject::myMethod'), or an array callback on an instantiated object (for example [$object, 'myMethod']).
11Overriding the loader via configuration
Instead of explicitly calling the setBuilder() method, it is possible to specify a loader object in the etc/temma.php configuration file (see the configuration documentation). This object must extend the \Temma\Base\Loader class and contain a protected builder() method. This method must take the name of the object to instantiate as a parameter and return an instance of it.
Here is an example. First, the Temma configuration in the etc/temma.php file:
<?php
return [
'application' => [
'loader' => 'MyLoader'
]
];
Then, in the lib/MyLoader.php file:
class MyLoader extends \Temma\Base\Loader {
protected function builder(string $key) {
if ($key == 'User')
return new \MyApp\Dao\User($this);
else if ($key == 'HttpClient')
return new \Utils\Http\Client($this);
throw new Exception("Unknown object '$key'.");
}
}
It then becomes possible to write:
// uses \MyApp\Dao\User
$user = $this->_loader->User->get($userId);
// uses \Utils\Http\Client
$this->_loader->HttpClient->post($url, $data);