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 thePSR-11
specification. If you do not want to usephp-di
, you can create and return another container instance that complies with thePSR-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 orphp-di
can complete dependency injection. Instances created withnew
cannot complete dependency injection. If injection is needed, use thesupport\Container
interface to replace thenew
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