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. 明确定义的容器 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(),并使用上述设置来解决其余的依赖关系似乎是最好的选择。
详细信息请参阅此处。

最初尝试使用$container->set()方法。
广告
将在 10 秒后关闭
bannerAds