Dependency Injection
Dependency injection is an optional feature in webman that is disabled by default. If you need dependency injection, it is recommended to use php-di. The following is an example of how to use php-di
with webman.
Installation
composer require psr/container ^1.1.1 php-di/php-di ^6 doctrine/annotations ^1.14
Modify the configuration config/container.php
, and the final content should be as follows:
$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();
config/container.php
should return an instance of a container that complies with thePSR-11
specification. If you don't want to usephp-di
, you can create and return another container instance that complies with thePSR-11
specification here.
Constructor Injection
Create a new file app/service/Mailer.php
with the following content:
<?php
namespace app\service;
class Mailer
{
public function mail($email, $content)
{
// Send mail code 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');
}
}
Normally, the following code is needed to instantiate app\controller\UserController
:
$mailer = new Mailer;
$user = new UserController($mailer);
When using php-di
, you don't need to manually instantiate Mailer
in the controller. Webman will automatically handle it for you. If there are dependencies in the instantiation process of Mailer
, webman will also automatically instantiate and inject them. You don't need any initialization work.
Note
Only instances created by the framework orphp-di
can complete dependency injection. Instances created manually with thenew
keyword cannot complete dependency injection. If you want to inject these instances, you need to use thesupport\Container
interface instead of thenew
statement, for example:
use app\service\UserService;
use app\service\LogService;
use support\Container;
// Dependency injection is not supported for instances created with the new keyword
$user_service = new UserService;
// Dependency injection is not supported for instances created with the new keyword
$log_service = new LogService($path, $name);
// Dependency injection is supported for instances created with Container
$user_service = Container::get(UserService::class);
// Dependency injection is supported for instances created with Container
$log_service = Container::make(LogService::class, [$path, $name]);
Annotation Injection
In addition to constructor injection, we can also use annotation injection. Continuing with the previous example, app\controller\UserController
is changed as follows:
<?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');
}
}
In this example, injection is performed using the @Inject
annotation, and the object type is declared using the @var
annotation. This example 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 when 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 a class, but rather strings, numbers, arrays, and other data. For example, the Mailer
constructor requires 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)
{
// Send mail code omitted
}
}
In this case, constructor auto-injection cannot be used directly because php-di
cannot determine the values of $smtp_host
and $smtp_port
. In this case, you can try custom injection.
Add the following code to config/dependence.php
(create the file if it does not exist):
return [
// ... Other configurations here
app\service\Mailer::class => new app\service\Mailer('192.168.1.11', 25)
];
This way, when the dependency injection needs to get an instance of app\service\Mailer
, it will automatically use the app\service\Mailer
instance created in this configuration.
We can see that config/dependence.php
uses new
to instantiate the Mailer
class, which is not a problem in this example. But imagine if the Mailer
class has dependencies on other classes or uses annotation injection internally, using new
initialization will not perform dependency injection. The solution is to use custom interface injection and initialize the class using the Container::get(class name)
or Container::make(class name, [constructor parameters])
methods instead of the new
keyword.
Custom Interface Injection
In real-world projects, it is preferable to program against interfaces rather than concrete classes. For example, 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 an 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)
{
// Send mail code 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');
}
}
Define the implementation of the MailerInterface
interface in config/dependence.php
.
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
interface, it will automatically use the Mailer
implementation.
The benefit of programming against interfaces is that when we need to replace a component, we don't need to change the business code, we only need to change the concrete implementation in
config/dependence.php
. This is also very useful for unit testing.Other custom injections
In addition to defining class dependencies,
config/dependence.php
can also define other values such as strings, numbers, arrays, etc.
For example, if config/dependence.php
is defined as follows:
return [
'smtp_host' => '192.168.1.11',
'smtp_port' => 25
];
We can use @Inject
to inject smtp_host
and smtp_port
into class properties.
<?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)
{
// Send mail code omitted
echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Will output 192.168.1.11:25
}
}
Note: The double quotes are used inside
@Inject("key")
.
For more information
Please refer to the php-di documentation