PHP-DI介绍
关于这篇文章
最近有很多PHP框架都默认配备了DI库,但由于我希望尽量不依赖框架的功能来进行DI,所以我尝试了通过外部库来实现DI。
我在研究PHP的依赖注入时发现了几种选项,但这次我想探索一下PHP-DI,并在这里记录其基本用法。我只记录了自己想了解的内容,如果你想了解更详细的使用方法,请参考官方网站。
环境
我正在一个只有 PHP 和 Composer 的最简环境下进行尝试。
$ php -v
PHP 7.3.0 (cli) (built: Dec 6 2018 02:17:00) ( ZTS MSVC15 (Visual C++ 2017) x86 )
$ composer -v
Composer version 1.8.0 2018-12-03 10:31:16
引入
首先,使用composer在PHP中安装PHP-DI。
composer require php-di/php-di
安装后的 composer.json 文件添加了类似以下内容的行:
{
"name": "hirodragon/testapp",
// ...
"require": {
"php-di/php-di": "^6.0" // ←追加されている
}
}
此外,在 [项目]/vendor/composer/autoload_psr4.php 文件中还添加了自动加载的设置。
<?php
return array(
// ...省略
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
);
一旦安装完成后,由于已经完成了自动加载设置等操作,因此您可以立即在php源代码中使用DI容器实例。
让我们尝试创建一个index.php文件,并执行以下代码。
<?php
require_once 'vendor/autoload.php';
$container = new DI\Container();
var_dump($container);
运行结果
$ php index.php
object(DI\Container)#2 (8) {
# ...省略
}
可以试试看
我打算先不使用設定來嘗試使用並測試其操作。
构成
我为这次的样本创建了一个名为sample的包。在想象一个干净架构的软件下,我创建了四个目录。我打算在这里添加各种文件并试试看。
(以下是当前目录结构)
project
│ composer.json
│ composer.lock
│ index.php
│───package
│ └───sample # このsampleパッケージに色々追加していきます
│ ├───app
│ ├───usecase
│ ├───domain
│ └───infra
└───vendor
让我们首先将添加的目录添加到自动加载中。
{
...
"autoload": {
"psr-4": {
"package\\" : "package/" // 追加
}
},
"require": {
"php-di/php-di": "^6.0"
}
}
自动加载更新
$ composer dumpautoload
样本包
接下来创建SampleController.php和SampleUseCase.php,并修改先前创建的index.php。
假设按照index.php -> SampleController.php -> SampleUseCase.php的顺序进行调用。
<?php
namespace package\sample\app;
use package\sample\usecase\SampleUseCase;
class SampleController
{
/**
* @var SampleUseCase
*/
private $usecase;
public function __construct(SampleUseCase $usecase)
{
$this->usecase = $usecase;
var_dump(get_class($this->usecase));
}
}
<?php
namespace package\sample\usecase;
class SampleUseCase
{
public function __construct()
{
}
}
<?php
require_once 'vendor/autoload.php';
use package\sample\app\SampleController;
use package\sample\usecase\SampleUseCase;
$container = new DI\Container();
$controller = new SampleController(new SampleUseCase());
执行结果 (shí jié guǒ)
$ php index.php
string(36) "package\sample\usecase\SampleUseCase"
在创建此SampleController时,作为参数传递的SampleUseCase是本次依赖注入的目标。
首先,我们尝试使用 $container->set() 来设置并使用依赖关系。
请注意,set() 的官方文档建议使用定义文件,所以请注意。
我将编辑index.php文件,并尝试使用set()函数进行定义。
<?php
require_once 'vendor/autoload.php';
use package\sample\app\SampleController;
use package\sample\usecase\SampleUseCase;
$container = new DI\Container();
$container->set('SampleUseCase', new SampleUseCase());
$controller = new SampleController($container->get('SampleUseCase'));
执行结果
$ php index.php
string(36) "package\sample\usecase\SampleUseCase"
我确认它与之前的版本一样可以正常工作。然而,由于这种用法会使得代码充斥着对$container的描述,所以还是最好使用正确的定义。
定义
官方网站:定义
据说有三种方法可以为DI(依赖注入)进行定义。
-
- auto wiring
-
- annotations
- PHP definitions
这些可以同时使用,并在同时使用时按照以下优先顺序应用。
-
- 明确定义的容器 1
通过 PHP 文件进行定义(如果在多个 PHP 配置文件中设置,则使用最后一个设置)
通过注解进行定义(Annotation)
通过类型声明进行定义(自动装配)
暂时先来看看这三个基本用法吧。
自动布线
auto wiring默认为打开状态,因此可以直接使用。它似乎会自动检测并生成并注入与类型声明相同的类型。我们将更改index.php并尝试实际运行一下。
<?php
require_once 'vendor/autoload.php';
$container = new DI\Container();
$controller = $container->get('package\sample\app\SampleController');
var_dump(get_class($controller));
执行结果
$ php index.php
string(36) "package\sample\usecase\SampleUseCase"
string(35) "package\sample\app\SampleController"
看起来SampleUseCase对象已经成功注入到SampleController中了。
此外,通过该构造函数实现的自动装配功能能够递归地进行注入,因此在初始生成时也会对其子对象进行注入。
我想在infra层创建一个用于数据库的类,并修改代码以便将其注入UseCase,并进行测试。
<?php
namespace package\sample\infra;
class MySqlDB
{
public function save(int $number): void
{
// なんか保存処理
}
}
<?php
namespace package\sample\usecase;
use package\sample\infra\MySqlDB;
class SampleUseCase
{
public function __construct(MySqlDB $db) // 作成したMySqlDBクラスを受け取る用に修正
{
var_dump(get_class($db)); //
}
}
执行结果
$ php index.php
string(28) "package\sample\infra\MySqlDB"
string(36) "package\sample\usecase\SampleUseCase"
string(35) "package\sample\app\SampleController"
通过从容器中生成控制器,我们确认它还注入了UseCase类所需的其他类。
然而,由于Auto wiring 是根据类型声明来确定注入对象,所以无法解决以下情况:
实际上,我认为想要进行DI几乎是针对接口的,所以需要使用其他方法。
class Database
{
public function __construct($dbHost, $dbPort) // 型宣言がない
{
// ...
}
public function setLogger(LoggerInterface $logger) // インターフェース
{
// ...
}
}
在这种情况下,需要在PHP文件的定义中明确声明向DI\autowire()注入什么。
PHP 定义
接下来,我们将尝试在PHP中编写定义并定义依赖关系注入。
注册方法似乎有以下两种定义。
// addDefinitions()の引数に配列を渡す
$containerBuilder->addDefinitions([
// place your definitions here
]);
// addDefinitions()の引数に定義ファイル名を渡す
$containerBuilder->addDefinitions('config.php');
我也想立即尝试一下基本的使用方法。
示例2
我会创建一个名为Sample2的程序包,然后尝试上述两种方法(内容与之前的sample程序包相同,但是UseCase类的构造函数暂时设为无)。
将”addDefinitions”以关联数组的形式进行定义
<?php
require_once 'vendor/autoload.php';
$builder = new DI\ContainerBuilder();
$builder->addDefinitions([
'SampleUseCase' => function($c){
return $c->get('package\sample2\usecase\SampleUseCase');
},
'SampleController' => DI\create('package\sample2\app\SampleController')
->constructor(DI\get('SampleUseCase')),
]);
$container = $builder->build();
$controller = $container->get('SampleController');
var_dump(get_class($controller));
// 実行結果
$ php index.php
string(37) "package\sample2\usecase\SampleUseCase"
string(36) "package\sample2\app\SampleController"
创建一个定义文件并进行定义。
在index.php所在的目录下放置diconfig.php文件。
<?php
use Psr\Container\ContainerInterface;
use function DI\factory;
use package\sample2\usecase\SampleUseCase;
use package\sample2\app\SampleController;
return [
'SampleUseCase' => DI\factory(function (ContainerInterface $c) {
return new SampleUseCase();
}),
'SampleController' => DI\factory(function (ContainerInterface $c) {
return new SampleController($c->get('SampleUseCase'));
}),
];
<?php
require_once 'vendor/autoload.php';
$builder = new DI\ContainerBuilder();
$builder->addDefinitions('diconfig.php');
$container = $builder->build();
$controller = $container->get('SampleController');
var_dump(get_class($controller));
// 実行結果
$ php index.php
string(37) "package\sample2\usecase\SampleUseCase"
string(36) "package\sample2\app\SampleController"
DI\factory()可以在无法从类型声明中推断出的情况下使用。请参阅官方文档以获取更详细的信息。
使用这个配置文件似乎可以描述针对接口的DI设置,但是我想先看一下最后的注释定义。
Subsequent explanations or notes.
让我们来看一下使用标注的定义方法。
默认情况下,此方法被禁用,因此首先需要启用它。
# Annotationsライブラリをインストール
composer require doctrine/annotations
另外,在使用时需要添加有效化的描述。
$containerBuilder->useAnnotations(true);
可以通过在phpdoc中添加@inject注释来定义要注入的目标和注入内容,从而实现对PHP-DI的注入定义。
样本3包裹
我剛才跟之前一樣,切了一個叫做sample3的套餐,試著看了一下。
雖然內容基本上是一樣的,但有以下幾點不同。
<?php
require_once 'vendor/autoload.php';
$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->useAnnotations(true);
$container = $containerBuilder->build();
$controller = $container->get('package\sample3\app\SampleController');
var_dump(get_class($controller));
<?php
namespace package\sample3\app;
use package\sample3\usecase\SampleUseCase;
class SampleController
{
/**
* @inject
* @var SampleUseCase
*/
private $usecase;
public function __construct(SampleUseCase $usecase)
{
$this->usecase = $usecase;
var_dump(get_class($this->usecase));
}
}
# 実行結果
$ php index.php
string(37) "package\sample3\usecase\SampleUseCase"
string(36) "package\sample3\app\SampleController"
只需添加@inject注解,它真的成功注入了。
暫時以這個方法,我們已經成功地嘗試了以最簡單的方式進行三種不同的DI方式。
通过将这三种方法结合起来,似乎可以灵活地进行依赖注入。
这个问题
我已经浏览了基本的使用方法,所以打算将它们结合起来,尝试以更实用的方式使用。到目前为止,只在实际类中定义了这些,但我将尝试在最常使用的接口中交织依赖注入。
样本4包
新添加了一个副本包并对接口进行了修改,每个文件如下所示。
<?php
namespace package\sample4\app;
use package\sample4\usecase\SampleUseCase;
class SampleController
{
/**
* @var SampleUseCase
*/
private $usecase;
public function __construct(SampleUseCase $usecase)
{
$this->usecase = $usecase;
}
}
<?php
namespace package\sample4\usecase;
use package\sample4\domain\SampleDB;
class SampleUseCase
{
/**
* @var SampleDB
*/
private $db;
public function __construct(SampleDB $db) // interfaceによる型宣言
{
$this->db = $db;
}
}
<?php
namespace package\sample4\domain;
interface SampleDB
{
public function save(int $number): void;
}
<?php
namespace package\sample4\infra;
use package\sample4\domain\SampleDB;
/**
* SampleDBインターフェースの具象クラス1
*/
class InMemoryDB implements SampleDB
{
private $data = [];
public function save(int $number): void
{
$this->data[] = $number;
}
}
<?php
namespace package\sample4\infra;
use package\sample4\domain\SampleDB;
/**
* SampleDBインターフェースの具象クラス2
*/
class MySqlDB implements SampleDB
{
public function save(int $number): void
{
// なんか保存処理
}
}
客户代码
<?php
require_once 'vendor/autoload.php';
use package\sample4\app\SampleController;
use package\sample4\infra\InMemoryDB;
use package\sample4\infra\MySqlDB;
use package\sample4\usecase\SampleUseCase;
// 以下のコードをPHP-DIを使用して自動化したい
$db = new InMemoryDB();
$usecase = new SampleUseCase($db);
$controller = new SampleController($usecase);
var_dump($controller);
# 実行結果
$ php index.php
object(package\sample4\app\SampleController)#12 (1) {
["usecase":"package\sample4\app\SampleController":private]=>
object(package\sample4\usecase\SampleUseCase)#11 (1) {
["db":"package\sample4\usecase\SampleUseCase":private]=>
object(package\sample4\infra\InMemoryDB)#10 (1) {
["data":"package\sample4\infra\InMemoryDB":private]=>
array(0) {
}
}
}
}
由于使用了interface,我想通过先前尝试过的PHP文件中的定义来定义它的依赖关系,并希望使用注解来定义其他的依赖关系。
定义接口的依赖关系
在设定文件中定义映射如下所示。
<?php
use package\sample4\domain\SampleDB;
use package\sample4\infra\InMemoryDB;
return [
SampleDB::class => DI\autowire(InMemoryDB::class),
];
我将修改index.php文件,首先只尝试这个部分。
<?php
require_once 'vendor/autoload.php';
$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->addDefinitions('diconfig.php');
$container = $containerBuilder->build();
use package\sample4\usecase\SampleUseCase;
$usecase = $container->get(SampleUseCase::class);
var_dump($usecase);
# 実行結果
$ php index.php
object(package\sample4\usecase\SampleUseCase)#20 (1) {
["db":"package\sample4\usecase\SampleUseCase":private]=>
object(package\sample4\infra\InMemoryDB)#23 (1) {
["data":"package\sample4\infra\InMemoryDB":private]=>
array(0) {
}
}
}
InMemoryDB类的实现类被注入到SampleUseCase类的构造函数中指定的SampleDB接口。
由于SampleUseCase是SampleController的具体类,所以我希望使用注解进行设置。
<?php
namespace package\sample4\app;
use package\sample4\usecase\SampleUseCase;
class SampleController
{
/**
* @inject // アノテーションを追加
* @var SampleUseCase
*/
private $usecase;
public function __construct(SampleUseCase $usecase)
{
$this->usecase = $usecase;
}
}
客户代码
<?php
require_once 'vendor/autoload.php';
use package\sample4\app\SampleController;
$containerBuilder = new DI\ContainerBuilder();
$containerBuilder->useAnnotations(true);
$containerBuilder->addDefinitions('diconfig.php');
$container = $containerBuilder->build();
$controller = $container->get(SampleController::class);
var_dump($controller);
# 実行結果
$ php index.php
object(package\sample4\app\SampleController)#29 (1) {
["usecase":"package\sample4\app\SampleController":private]=>
object(package\sample4\usecase\SampleUseCase)#33 (1) {
["db":"package\sample4\usecase\SampleUseCase":private]=>
object(package\sample4\infra\InMemoryDB)#38 (1) {
["data":"package\sample4\infra\InMemoryDB":private]=>
array(0) {
}
}
}
}
通过在客户端代码中从容器中创建控制器,可以确认在注入之前已解决了所有依赖关系,并注入到所需的所有类中。
结束了 (Chinese: le)
基本操作已经在此记录,但到达这一步后,只需要参考文档并根据需要进行适当的设置,就可以使用了。
在本文中没有提到,但是在使用框架时,控制器应该已经在框架中生成,这种情况下,您可以使用injectOn()来对已经生成的对象进行依赖注入。
当然,由于已经生成,因此无法使用构造函数注入,而是需要使用属性注入来进行依赖注入。
对于生成的控制器,使用injectOn(),并使用上述设置来解决其余的依赖关系似乎是最好的选择。
详细信息请参阅此处。