Dependency Injection

In Webman, dependency injection is an optional feature, and it is turned off by default. If you need dependency injection, it is recommended to use php-di. Below is how to use Webman with php-di.

Installation

composer require psr/container ^1.1.1 php-di/php-di ^6.3 doctrine/annotations ^1.14

Modify the configuration in config/container.php, with the final content as follows:

$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();

The final return in config/container.php is a container instance that complies with the PSR-11 specification. If you do not want to use php-di, you can create and return another container instance that complies with the PSR-11 specification here.

Constructor Injection

Create app/service/Mailer.php (please create the directory if it does not exist) with the following content:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // Code to send email is omitted
    }
}

The content of app/controller/UserController.php is as follows:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Under normal circumstances, the following code is needed to instantiate app\controller\UserController:

$mailer = new Mailer;
$user = new UserController($mailer);

When using php-di, developers do not need to manually instantiate the Mailer in the controller; Webman will automatically do it for you. If there are other class dependencies during the instantiation of Mailer, Webman will also automatically instantiate and inject them. Developers do not need to perform any initialization work.

Note
Only instances created by the framework or php-di can complete dependency injection. Instances created with new cannot complete dependency injection. If injection is needed, use the support\Container interface to replace the new statement, for example:

use app\service\UserService;
use app\service\LogService;
use support\Container;

// Instances created with the new keyword cannot be dependency injected
$user_service = new UserService;
// Instances created with the new keyword cannot be dependency injected
$log_service = new LogService($path, $name);

// Instances created by Container can be dependency injected
$user_service = Container::get(UserService::class);
// Instances created by Container can be dependency injected
$log_service = Container::make(LogService::class, [$path, $name]);

Annotation Injection

In addition to constructor dependency injection, we can also use annotation injection. Continuing from the previous example, change app\controller\UserController to the following:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var Mailer
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

This example injects via the @Inject annotation, and the type of the object is declared with the @var annotation. This method has the same effect as constructor injection, but the code is more concise.

Note
Webman does not support controller parameter injection before version 1.4.6. For example, the following code is not supported in Webman <= 1.4.6:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    // Controller parameter injection is not supported before version 1.4.6
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

Custom Constructor Injection

Sometimes, the parameters passed to the constructor may not be instances of classes but rather strings, numbers, arrays, and other data types. For example, the Mailer constructor may need to pass in the SMTP server IP and port:

<?php
namespace app\service;

class Mailer
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Code to send email is omitted
    }
}

In this case, the automatic constructor injection introduced earlier cannot be directly used because php-di cannot determine the values of $smtp_host and $smtp_port. In this situation, you can try custom injection.

Add the following code to config/dependence.php (please create the file if it does not exist):

return [
    // ... Other configurations are omitted

    app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25);
];

Thus, when dependency injection needs to obtain an instance of app\service\Mailer, it will automatically use the app\service\Mailer instance created in this configuration.

We notice that new is used to instantiate the Mailer class in config/dependence.php. This is fine for this example, but imagine if the Mailer class depended on other classes or used annotation injection internally; initializing with new will not allow for dependency injection. The solution is to utilize custom interface injection by initializing the class using Container::get(类名) or Container::make(类名, [构造函数参数]).

Custom Interface Injection

In real projects, we prefer to program against interfaces rather than specific classes. For instance, app\controller\UserController should import app\service\MailerInterface instead of app\service\Mailer.

Define the MailerInterface interface.

<?php
namespace app\service;

interface MailerInterface
{
    public function mail($email, $content);
}

Define the implementation of the MailerInterface interface.

<?php
namespace app\service;

class Mailer implements MailerInterface
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Code to send email is omitted
    }
}

Import MailerInterface instead of the concrete implementation.

<?php
namespace app\controller;

use support\Request;
use app\service\MailerInterface;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var MailerInterface
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

In config/dependence.php, define the MailerInterface interface as follows:

use Psr\Container\ContainerInterface;
return [
    app\service\MailerInterface::class => function(ContainerInterface $container) {
        return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
    }
];

This way, when the business needs to use the MailerInterface, it will automatically use the implementation of Mailer.

The benefit of programming against interfaces is that when we need to replace a component, we do not need to change the business code; we only need to change the specific implementation in config/dependence.php. This is also very useful for unit testing.

Other Custom Injections

In config/dependence.php, besides defining class dependencies, you can also define other values, such as strings, numbers, arrays, etc.

For example, define config/dependence.php as follows:

return [
    'smtp_host' => '192.168.1.11',
    'smtp_port' => 25
];

At this point, we can inject smtp_host and smtp_port into the class properties using @Inject.

<?php
namespace app\service;

use DI\Annotation\Inject;

class Mailer
{
    /**
     * @Inject("smtp_host")
     */
    private $smtpHost;

    /**
     * @Inject("smtp_port")
     */
    private $smtpPort;

    public function mail($email, $content)
    {
        // Code to send email is omitted
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Will output 192.168.1.11:25
    }
}

Note: The @Inject("key") inside uses double quotes.

More Content

Please refer to the php-di documentation