View on GitHub

yii2-cookbook-chinese

Yii Application Development Cookbook(Third Edition)中文翻译

依赖注入容器

在提取清晰抽象子系统的帮助下,依赖注入容器(DIP)建议我们创建模块化低耦合代码。

例如,如果你想简化一个大类,你可以将它分割成许多块的程序代码,并将每一个块提取成一个新的简单的独立类。

原则上,你的低级块应该实现一个充足清晰的抽象,并且高级代码应该只与这个抽象工作,不能与低级实现工作。

当我们将一个大的多任务类分割成小的专门类,我们会遇到创建依赖对象并将它们注入到对方中的问题。

之前如果我们创建了一个实例:

$service = new MyGiantSuperService();

分割以后我们将会创建或者获取所有的依赖项,并建立我们的服务:

$service = new MyService(
    new Repository(new PDO('dsn', 'username', 'password')),
    new Session(),
    new Mailer(new SmtpMailerTransport('username', 'password', host')),
    new Cache(new FileSystem('/tmp/cache')),
);

依赖注入容器是一个工厂,它能让我们不关心创建自己的对象。在Yii2中,我们可以一次性配置一个容器,然后就可以通过如下方式获取我们的服务:

$service = Yii::$container->get('app\services\MyService')

我们也可以使用这个:

$service = Yii::createObject('app\services\MyService')

或者在构造其它服务是,我们让容器作为一个依赖注入它:

use app\services\MyService;
class OtherService
{
    public function __construct(MyService $myService) {  }
}

当我们获取OtherService实例时:

$otherService = Yii::createObject('app\services\OtherService')

在所有情况下,容器将会解析所有的依赖,并为每个注入依赖对象。

在本节中,我们创建了带有存储子系统的购物手推车,并将手推车自动注入到控制器中。

准备

按照官方向导http://www.yiiframework.com/doc-2.0/guide-startinstallation.html中的描述,使用Composer包管理器创建一个新应用。

如何做…

执行如下步骤:

  1. 创建一个购物手推车(shopping cart)类:
<?php
namespace app\cart;
use app\cart\storage\StorageInterface;
class ShoppingCart
{
    private $storage;
    private $_items = [];
    public function __construct(StorageInterface $storage)
    {
        $this->storage = $storage;
    }
    public function add($id, $amount)
    {
        $this->loadItems();
        if (array_key_exists($id, $this->_items)) {
            $this->_items[$id]['amount'] += $amount;
        } else {
            $this->_items[$id] = [
                'id' => $id,
                'amount' => $amount,
            ];
        }
        $this->saveItems();
    }
    public function remove($id)
    {
        $this->loadItems();
        $this->_items = array_diff_key($this->_items, [$id => []]);
        $this->saveItems();
    }
    public function clear()
    {
        $this->_items = [];
        $this->saveItems();
    }
    public function getItems()
    {
        $this->loadItems();
        return $this->_items;
    }
    private function loadItems()
    {
        $this->_items = $this->storage->load();
    }
    private function saveItems()
    {
        $this->storage->save($this->_items);
    }
}
  1. 它将只会和它自己的项工作。并不是内置地将项目存放在session,它将这个任务委派给了任意的外部存储类,这些类需要实现StorageInterface接口。

  2. 这个购物车类只是在它自己的构造器中获取了存储对象,将它保存在私有的$storage字段里,并通过load()和save()方法来调用。

  3. 使用必需的方法定义一个常用的手推车存储接口:

<?php
namespace app\cart\storage;
interface StorageInterface
{
    /**
     * @return array of cart items
     */
    public function load();
    /**
     * @param array $items from cart
     */
    public function save(array $items);
}
  1. 创建一个简单的存储实现。它将会在一个服务器session存储选择的项:
<?php
namespace app\cart\storage;
use yii\web\Session;
class SessionStorage implements StorageInterface
{
    private $session;
    private $key;
    public function __construct(Session $session, $key)
    {
        $this->key = $key;
        $this->session = $session;
    }
    public function load()
    {
        return $this->session->get($this->key, []);
    }
    public function save(array $items)
    {
        $this->session->set($this->key, $items);
    }
}
  1. 这个存储可以在它的构造器中获取任意框架session实例,然后使用它来获取和存储项目。

  2. 在config/web.php文件中配置ShoppingCart类和它的依赖:

<?php
use app\cart\storage\SessionStorage;
Yii::$container->setSingleton('app\cart\ShoppingCart');
Yii::$container->set('app\cart\storage\StorageInterface',
    function() {
        return new SessionStorage(Yii::$app->session,
            'primary-cart');
    });
$params = require(__DIR__ . '/params.php');
//…
  1. 基于一个扩展的构造器创建cart控制器:
<?php
namespace app\controllers;
use app\cart\ShoppingCart;
use app\models\CartAddForm;
use Yii;
use yii\data\ArrayDataProvider;
use yii\filters\VerbFilter;
use yii\web\Controller;
class CartController extends Controller
{
    private $cart;
    public function __construct($id, $module, ShoppingCart $cart, $config = [])
    {
        $this->cart = $cart;
        parent::__construct($id, $module, $config);
    }
    public function behaviors()
    {
        return [
            'verbs' => [
                'class' => VerbFilter::className(),
                'actions' => [
                    'delete' => ['post'],
                ],
            ],
        ];
    }

    public function actionIndex()
    {
        $dataProvider = new ArrayDataProvider([
            'allModels' => $this->cart->getItems(),
        ]);
        return $this->render('index', [
            'dataProvider' => $dataProvider,
        ]);
    }
    public function actionAdd()
    {
        $form = new CartAddForm();
        if ($form->load(Yii::$app->request->post()) && $form->validate()) {
            $this->cart->add($form->productId, $form->amount);
            return $this->redirect(['index']);
        }
        return $this->render('add', [
            'model' => $form,
        ]);
    }
    public function actionDelete($id)
    {
        $this->cart->remove($id);
        return $this->redirect(['index']);
    }
}
  1. 创建一个form:
<?php
namespace app\models;
use yii\base\Model;
class CartAddForm extends Model
{
    public $productId;
    public $amount;
    public function rules()
    {
        return [
            [['productId', 'amount'], 'required'],
            [['amount'], 'integer', 'min' => 1],
        ];
    }
}
  1. 创建视图文件views/cart/index.php:
<?php
use yii\grid\ActionColumn;
use yii\grid\GridView;
use yii\grid\SerialColumn;
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $dataProvider yii\data\ArrayDataProvider */
$this->title = 'Cart';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cart-index">
    <h1><?= Html::encode($this->title) ?></h1>
    <p><?= Html::a('Add Item', ['add'], ['class' => 'btn btn-success']) ?></p>
    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'columns' => [
            ['class' => SerialColumn::className()],
            'id:text:Product ID',
            'amount:text:Amount',
            [
                'class' => ActionColumn::className(),
                'template' => '{delete}',
            ]
        ],
    ]) ?>
</div>
  1. 创建视图文件views/cart/add.php:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
/* @var $this yii\web\View */
/* @var $form yii\bootstrap\ActiveForm */
/* @var $model app\models\CartAddForm */
$this->title = 'Add item';
$this->params['breadcrumbs'][] = ['label' => 'Cart', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="cart-add">
    <h1><?= Html::encode($this->title) ?></h1>
    <?php $form = ActiveForm::begin(['id' => 'contact-form']);
    ?>
    <?= $form->field($model, 'productId') ?>
    <?= $form->field($model, 'amount') ?>
    <div class="form-group">
        <?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
    </div>
    <?php ActiveForm::end(); ?>
</div>
  1. 添加链接项目到主菜单:
['label' => 'Home', 'url' => ['/site/index']],
['label' => 'Cart', 'url' => ['/cart/index']],
['label' => 'About', 'url' => ['/site/about']],
// …
  1. 打开cart页并尝试添加几行:

工作原理…

在这个例子中,通过一个抽象接口,我们定义了一个依赖较少的主类ShoppingCart:

class ShoppingCart
{
    public function __construct(StorageInterface $storage) {  }
}
interface StorageInterface
{
    public function load();
    public function save(array $items);
}

然后我们实现了这个抽象类:

class SessionStorage implements StorageInterface
{
    public function __construct(Session $session, $key) {  }
}

然后我们可以按如下方式手动创建一个cart的实例:

$storage = new SessionStorage(Yii::$app->session, 'primary-cart');
$cart = new ShoppingCart($storage)

它允许我们创建许多不同的实现,例如SessionStorage、CookieStorage或者DbStorage。并且我们可以在不同的项目和不同的框架中复用不依赖框架的基于StorageInterface的ShoppingCart类。我们只需为需要的框架使用接口的方法实现这个存储类。

并不需要手动创建一个带有所有依赖的实例,我们可以使用一个依赖注入容器。

默认情况下容器解析所有类的构造函数,并递归创建所有需要的实例。例如,如果我们有四个类:

class A {
    public function __construct(B $b, C $c) {  }
}
class B {
...
}
class C {
    public function __construct(D $d) {  }
}
class D {
...
}

我们可以用两种方法获取A类的实例:

$a = Yii::$container->get('app\services\A')
// or
$a = Yii::createObject('app\services\A')

并且容器自动创建B、D、C和A的实例,并将他们注入到对象中。

在我们的例子中,我们将cart实例标记为一个单件模式(singleton):

Yii::$container->setSingleton('app\cart\ShoppingCart');

这意味着容器将会为每一个重复的请求返回一个单例,而不是一次又一次的创建。

此外,我们的ShoppingCart在它自己的构造器中有StorageInterface类型,并且容器知道需要为这个类型实例化哪些类。我们必须按如下方式为接口手动绑定这个类:

Yii::$container->set('app\cart\storage\StorageInterface', 'app\cart\storage\CustomStorage',);

但是我们的SessionStorage有一个非标准构造器:

class SessionStorage implements StorageInterface
{
    public function __construct(Session $session, $key) {  }
}

因此我们使用一个匿名函数来手动创建这个实例:

Yii::$container->set('app\cart\storage\StorageInterface', function()
{
    return new SessionStorage(Yii::$app->session, 'primary-cart');
});

毕竟在我们自己的控制器、控件等其它地方,我们可以从容器中手动获取cart对象,

$cart = Yii::createObject('app\cart\ShoppingCart')

但是,在框架内部中,每一个控制器和其它对象将会通过createObject方法创建。并且我们可以通过控制器构造器来注入cart:

class CartController extends Controller
{
    private $cart;
    public function __construct($id, $module, ShoppingCart $cart,
                                $config = [])
    {
        $this->cart = $cart;
        parent::__construct($id, $module, $config);
    }
    // ...
}

使用被注入的cart对象:

public function actionDelete($id)
{
    $this->cart->remove($id);
    return $this->redirect(['index']);
}

参考