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: Associative array containing the connection objects to the data sources (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);

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 (substr($key, -2) == 'Bo') {
        $classname = substr($key, 0, -2);
        return new \MyApp\Bo\$classname($loader);
    } else if (substr($key, -3) == '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);

8Redefinition 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:

[
    '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);