Dependency injection
1Presentation
Dependency injection allows logic to be decoupled between objects by using the inversion of control principle.
Temma automatically creates a component to handle dependency injection into business objects. This component is the backbone
that all objects in your application can rely on to access each other.
It is available in controllers under the private attribute $_loader.
By default, the component contains the following:
- $loader->dataSources: Registry used to access the data source connection objects (see the controllers documentation).
- $loader->session: Session management object (see sessions).
- $loader->config: Configuration management object, which gives access to all configuration directives.
- $loader->request: Management object of the incoming request, which allows to handle the execution flow of the framework.
- $loader->response: Object used by the framework to manage the response to the client.
- $loader->controller: Instance of the plugin or the controller in use.
2Usage with your DAOs
DAO objects can be instantiated in controllers using their _loadDao() method. Outside controllers, the dependency injection component can be used to create instances of DAO objects you have developed.
For example, if you have created a UserDao object (in lib/UserDao.php), you can use it as follows:
$user = $this->_loader->UserDao->getFromEmail($email);
3Usage with your business objects
An object that implements the \Temma\Base\Loadable interface can be loaded by the component. Its constructor must then take a single parameter, of type \Temma\Base\Loader.
Here is an example of an object that writes data to a var/list.txt file (in the project tree):
class List implements \Temma\Base\Loadable {
/** Path to the file to write in. */
private string $_path = null;
/**
* Constructor.
* @param \Temma\Base\Loader $loader Component.
*/
public function __construct(\Temma\Base\Loader $loader) {
$this->_path = $loader->config->varPath . '/liste.txt';
}
/**
* Method which writes into the file.
* @param string $text The text to write.
*/
public function write(string $text) {
file_put_contents($this->_path, "$text\n", FILE_APPEND);
}
}
- Line 1: The object implements the \Temma\Base\Loadable interface.
- Line 3: A private attribute will contain the full path to the file in which we will write later.
-
Line 9: Constructor of the object, which receives as a parameter an instance of the dependency injection component.
- Line 10: We build the path to the file, using the configuration object, whose varPath attribute gives the path to the var/ directory of the project.
- Line 17: The write() method can be used to write to the file.
For this object to be loadable by the framework, it must be accessible in the inclusion paths of the project. By default, this means that we will save it in a file named List.php, placed in the lib/ directory of the project.
From that point on, the object is available directly as if it were an attribute of the component. Only one instance of the object will be created (the first time the object is called).
Here is an example of a controller that uses the List object thanks to the component:
class Homepage extends \Temma\Web\Controller {
/** Root action. */
public function index() {
// write in the file
$this->_loader->List->write('Ça marche');
}
}
- Line 5: We go through the component to access the List object, then to its write() method.
Now imagine that we create another business object, loadable via the component, which uses the List object.
class Compute implements \Temma\Base\Loadable {
/** Instance of the dependency injection component. */
private $_loader = null;
/**
* Constructor.
* @param \Temma\Base\Loader $loader Composant.
*/
public function __construct(\Temma\Base\Loader $loader) {
$this->_loader = $loader;
}
/**
* Method that performs an addition.
* @param int $i Number.
* @param int $j Number.
* @return int Result of the addition.
*/
public function add(int $i, int $j) : int {
$result = $i + $j;
$this->_loader->List->ecrit("Computed: $result");
return ($result);
}
}
- Line 3: Unlike the List object, this object will keep the instance of the dependency injection component in a private attribute.
- Line 10: In the constructor, we copy the instance of the component (received as a parameter) into the private attribute.
- Line 21: We use the component to call the List object.
This illustrates how business objects can call each other, with the component that handles accesses and instantiations.
4Alternative access
The examples seen previously assume 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 codes are in files placed in the lib/ directory of the project.
But sometimes you're going to want to use objects that are in deep namespaces. In this case, you will have to access the objects using an associative array type writing, and no longer object oriented.
For example, if we want to use the add() method of the \Math\Base\Compute object, we will have to write:
$res = $this->_loader['\Math\Base\Calcul']->add(3, 4);
It is also possible to use the get() method:
$res = $this->_loader->get('\Math\Base\Calcul')->addition(3, 4);
5Explicit additions in the component
You also have the option of creating the object yourself and adding it to the component by specifying the name you want to give it:
// we create the instance of the object
$compute = new \Math\Base\Compute($this->_loader);
// we add the instance in the component
// (the three writings are equivalent))
// - with an object-oriented writing
$this->_loader->calculator = $compute;
// - with an associative array writing
$this->_loader['calculator'] = $compute;
// - or by using the set() method
$this->_loader->set('calculator', $compute);
// now that the instance is registered in the component,
// we can use it wherever the component is accessible
$res = $this->_loader->calculator->add(3, 4);
You can add any element to the component (not just objects that implement the \Temma\Base\Loadable interface):
$this->_loader->anInteger = 3;
$this->_loader->anArray = ['a', 'b', 'c'];
$this->_loader->anObject = new \Foo();
6Additions by callback
It is also possible to assign an anonymous function. This function will be executed during the first call via the component; it must return the object which will then be returned by the component.
Here is an example of a controller:
class Homepage extends \Temma\Web\Controller {
// method called before the action
public function __wakeup() {
$this->_loader->calc = function($loader) {
if ($this['param'] == 'base')
return (new \Math\Base\Compute($loader);
return (new \Math\Other\Compute($loader));
};
}
// action
public function compute(string $type) {
$this['param'] = $type:
$this['res'] = $this->_loader->calc->add(3, 4);
}
// another action
public function compute2() {
$this['res'] = $this->_loader->calc->add(3, 4);
}
}
- Line 3: We use the __wakeup() method to initialize the controller.
- Line 4: We create the calc key in the component, by assigning an anonymous function to it. This function takes a single parameter, which will receive the component instance. The function must return the object which will be returned later.
- Lines 5 to 7: The variable $this refers to the controller itself. We look at the value contained by the param template variable to know which implementation will be returned.
- Line 13: We assign to the param template variable the value received in the $type parameter.
- Line 14: We use the component without having to worry about which implementation is used.
- Line 19: We use the component. Here it will always be the \Math\Other\Compute object that is used, but that could change without needing to modify the action.
7Additions by builder
A builder is a function that takes care of managing instantiations, and that we register with the
setBuilder() method of the component.
This function takes two parameters: the first is an instance of the component; the second is the name of the object we are trying to access.
Here is an example:
// definition of the builder
$this->_loader->setBuilder(function($loader, $key) {
// we see if the name of the requested object ends with
// "BO" or "DAO"
if (str_ends_with($key, 'Bo')) {
$classname = substr($key, 0, -2);
return new \MyApp\Bo\$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\Bo\Mail object
$this->_loader->MailBo->send();
// this call will use the \MyApp\Dao\User object
$this->_loader->UserDao->remove($userId);
8Alias management
8.1Defining aliases
It's possible to define naming aliases, which will be used when an object (necessarily implementing the \Temma\Base\Loadable interface) is called via the loader.
You can declare an alias using the setAlias() method:
// alias definition
$this->_loader->setAlias('UserBo', '\MyApp\User\UserBo');
// use alias
$list = $this->_loader->UserBo->getList();
// is equivalent to
$list = $this->_loader['\MyApp\User\UserBo']->getList();
You can also declare multiple aliases by passing an associative array to the setAliases() method:
// define an array of aliases
$this->_loader->setAliases([
'UserBo' => '\MyApp\User\UserBo',
'TµEmail' => '\Temma\Utils\Email',
]);
// use aliases
$list = $this->_loader->UserBo->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);
// equivalent to
$list = $this->_loader['\MyApp\User\UserBo']->getList();
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
It's possible to delete a previously defined alias, by supplying the value null for the same alias name, either with the setAlias() method or with the setAliases() method.
8.2Configuring aliases
Rather than defining aliases with the setAlias() and setAliases() methods, it's easier to list them in the project configuration.
For example, with the following etc/temma.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserBo' => '\MyApp\User\UserBo',
'TµEmail' => '\Temma\Utils\Email',
]
]
];
You can write the following code:
$list = $this->_loader->UserBo->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);
8.3Callback alias management
When an alias is defined (either by the setAlias() and setAliases() methods or in the configuration file), it is possible to define a callable value for it. In this case, when the element is requested from the loader, the pointed function is executed (passing the loader instance as a parameter), and its return is used as the value associated with the requested name.
Example of code with a function:
// definition of a function that returns an object according to the configuration
function createUser(\Temma\Base\Loader $loader) {
if ($loader->config->xtra('userType', 'internal', false))
return new \MyApp\User($loader);
return new \OtherApp\User($loader);
}
// add alias
$this->_loader->setAlias('User', 'createUser');
Example with an anonymous function:
// add alias, with definition of anonymous function
$this->_loader->setAlias('WalletDao', function(\Temma\Base\Loader $loader) {
return new \MyApp\Wallet\WalletDao($loader->dataSources->db);
});
Example with an object:
// definition of an object containing a method that returns an object
class CategoryManager {
public function generate(\Temma\Base\Loader $loader) {
return new \MyApp\CategoryDao($loader);
}
}
// add alias
$this->_loader->setAlias('Cat', ['CategoryManager', 'generate']);
Example with a static object:
namespace MyApp {
// define an object with a static method
class ArticleBuilder {
static public function build(\Temma\Base\Loader $loader) {
$objectName = $loader->config->xtra('articleManager', 'objectName', '\MyApp\Article');
return new $objectName($loader);
}
}
}
namespace {
// add alias
$this->_loader->setAlias('Article', '\MyApp\ArticleBuilder::build');
// alternative writing
$this->_loader->setAlias('Article', ['\MyApp\ArticleBuilder', 'build']);
}
8.4Using aliases for testing
When running automated tests on a Temma application, you may need to replace an object with a mock, a “fake object” that simulates the behavior of the original object.
With naming aliases, it becomes very easy to create a file etc/temma.test.php (if the runtime environment is called test), which redefines the loaded objects for a given name.
Example of an etc/temma.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserBo' => '\MyApp\User\UserBo'
]
]
];
Example of an etc/temma.test.php file:
<?php
return [
'x-loader' => [
'aliases' => [
'UserBo' => '\MyMock\UserBo',
'\Temma\Utils\Email' => '\MyMock\Email',
]
]
];
Your application code may contain:
// this line of code:
$list = $this->_loader->UserBo->getList();
// under normal circumstances, will be equivalent to:
$list = $this->_loader['\MyApp\User\UserBo']->getList();
// in a test environment, will be equivalent to:
$list = $this->_loader['\MyMock\UserBo']->getList();
// this line of code:
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
// in a test environment, will be equivalent to:
$this->_loader['\MyMock\Email']->textMail($from, $to, $title, $message);
9Prefix management
9.1Introduction to prefixes
In addition to aliases, it's 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.
Caution: Avoid listing too many prefixes, as they are all reviewed each time an object is requested for the first time. This can have an impact on performance.
9.2Defining prefixes
You can declare a prefix using the setPrefix() method:
// defining a prefix
$this->_loader->setPrefix('global', '\MyApp');
// thie line of code:
$object = $this->_loader->globalArticles;
// is equivalent to:
$object = $this->_loader['\MyAppArticles'];
// second prefix
$this->_loader->setPrefix('App', '\OtherApp\Extension\OtherApp\\');
// this line of code:
$object = $this->_loader->AppBidule;
// is equivalent to:
$object = $this->_loader['\OtherApp\Extension\OtherApp\Bidule'];
// third prefix
$this->_loader->setPrefix('€x', '\Europa\Source\Bo');
// thie line of code:
$object = $this->_loader->€xUser;
// is equivalent to:
$object = $this->_loader['\Europa\Source\BoUser'];
You can also declare multiple prefixes by passing an associative array to the setPrefixes() method:
// define an array of prefixes
$this->_loader->setPrefixes([
'global' => '\MyApp',
'App' => '\OtherApp\Extension\OtherApp\\',
'€x' => '\Europa\Source\Bo',
]);
It's possible to delete a previously defined prefix, by supplying the value null for the same prefix, either with the setPrefix() method or with the setPrefixes() method.
9.3Prefix configuration
Rather than defining prefixes with the setPrefix() and setPrefixes() methods, it's easier to list them in the project configuration.
Example of a etc/temma.php file:
<?php
return [
'x-loader' => [
'prefixes' => [
'global' => '\MyApp',
'App' => '\OtherApp\Extension\OtherApp\\',
'€x' => '\Europa\Source\Bo',
]
]
];
You can then write the following code:
// similar writings
$object = $this->_loader->globalArticles;
$object = $this->_loader['\MyAppArticles'];
// similar writings
$object = $this->_loader->AppBidule;
$object = $this->_loader['\OtherApp\Extension\OtherApp\Bidule'];
// similar writings
$object = $this->_loader->€xUser;
$object = $this->_loader['\Europa\Source\BoUser'];
9.4Callback prefix management
When a prefix is defined (either via the setPrefix() and setPrefixes() methods or in the configuration file), it is possible to define a callable value for it. In this case, when the element is requested from the loader, the pointed function is executed (passing the loader instance and the requested object name from which the prefix has been removed as parameters), and its return is used as the value associated with the requested name.
Code example with a function:
// define a function that returns an object based on the 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\QantumLeap();
return (null);
}
// add prefix
$this->_loader->setPrefix('Zx', 'sinclairManager');
// this line of code:
$computer = $this->_loader->ZxSpectrum;
// is equivalent to:
$computer = $this->_loader['\Sinclair\Computers\ZxSpectrum'];
// another line of code:
$computer = $this->_loader->Zx81;
// is equivalent to:
$computer = $this->_loader['\Sinclair\Computers\Zx81'];
// another line of code:
$computer = $this->_loader->ZxQL;
// is equivalent to:
$computer = $this->_loader['\Sinclair\Computers\QantumLeap'];
As with aliases, you can pass the name of a function, an anonymous function, a static call string (e.g. 'MyObject::myMethod') or an array of calls to an instantiated object (e.g. [$object, 'myMethod']).
10Redefinition of the loader in configuration
Rather than explicitly call the setBuilder() method, it is possible to specify a loader object in the etc/temma.php file (see the configuration documentation). This object must inherit from 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 the latter.
Here is an example. To start, 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'.");
}
}
Then it becomes possible to write:
// use \MyApp\Dao\User
$user = $this->_loader->User->get($userId);
// use \Utils\Http\Client
$this->_loader->HttpClient->post($url, $data);