-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating

Yii2 Application Development Cookbook
By :

Dependency Inversion Principle (DIP) suggests we create modular low-coupling code with the help of extracting clear abstraction subsystems.
For example, if you want to simplify a big class you can split it into many chunks of routine code and extract every chunk into a new simple separated class.
The principle says that your low-level chunks should implement an all-sufficient and clear abstraction, and high-level code should work only with this abstraction and not low-level implementation.
When we split a big multitask class into small specialized classes, we face the issue of creating dependent objects and injecting them into each other.
If we could create one instance before:
$service = new MyGiantSuperService();
And after splitting we will create or get all dependent items and build our service:
$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')), );
Dependency injection container is a factory that allows us to not care about building our objects. In Yii2 we can configure a container only once and use it for retrieving our service like this:
$service = Yii::$container->get('app\services\MyService')
We can also use this:
$service = Yii::createObject('app\services\MyService')
Or we ask the container to inject it as a dependency in the constructor of an other service:
use app\services\MyService; class OtherService { public function __construct(MyService $myService) { … } }
When we will get the OtherService
instance:
$otherService = Yii::createObject('app\services\OtherService')
In all cases the container will resolve all dependencies and inject dependent objects in each other.
In the recipe we create shopping cart with storage subsystem and inject the cart automatically into controller.
Create a new application by using the Composer package manager, as described in the official guide at http://www.yiiframework.com/doc-2.0/guide-startinstallation.html.
Carry out the following steps:
<?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); } }
StorageInterface
interface.$storage
field and calls its load()
and save()
methods.<?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); }
<?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); } }
ShoppingCart
class and its dependencies in the config/web.php
file:<?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'); //…
<?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']); } }
<?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], ]; } }
views/cart/index.php
view:<?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>
views/cart/add.php
view:<?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>
['label' => 'Home', 'url' => ['/site/index']], ['label' => 'Cart', 'url' => ['/cart/index']], ['label' => 'About', 'url' => ['/site/about']], // …
In this case we have the main ShoppingCart
class with a low-level dependency, defined by an abstraction interface:
class ShoppingCart { public function __construct(StorageInterface $storage) { … } } interface StorageInterface { public function load(); public function save(array $items); }
And we have some an implementation of the abstraction:
class SessionStorage implements StorageInterface { public function __construct(Session $session, $key) { … } }
Right now we can create an instance of the cart manually like this:
$storage = new SessionStorage(Yii::$app->session, 'primary-cart'); $cart = new ShoppingCart($storage)
It allows us to create a lot of different implementations such as SessionStorage
, CookieStorage
, or DbStorage
. And we can reuse the framework-independent ShoppingCart
class with StorageInterface
in different projects and different frameworks. We must only implement the storage class with the interface's methods for needed framework.
But instead of manually creating an instance with all dependencies, we can use a dependency injection container.
By default the container parses the constructors of all classes and recursively creates all the required instances. For example, if we have four classes:
class A { public function __construct(B $b, C $c) { … } } class B { ... } class C { public function __construct(D $d) { … } } class D { ... }
We can retrieve the instance of class A
in two ways:
$a = Yii::$container->get('app\services\A') // or $a = Yii::createObject('app\services\A')
And the container automatically creates instances of the B
, D
, C
, and A
classes and injects them into each other.
In our case we mark the cart instance as a singleton:
Yii::$container->setSingleton('app\cart\ShoppingCart');
This means that the container will return a single instance for every repeated call instead of creating the cart again and again.
Besides, our ShoppingCart
has the StorageInterface
type in its own constructor and the container does know what class it must instantiate for this type. We must manually bind the class to the interface like this:
Yii::$container->set('app\cart\storage\StorageInterface', 'app\cart\storage\CustomStorage',);
But our SessionStorage
class has non-standard constructor:
class SessionStorage implements StorageInterface { public function __construct(Session $session, $key) { … } }
Therefore we use an anonymous function to manually creatie the instance:
Yii::$container->set('app\cart\storage\StorageInterface', function() { return new SessionStorage(Yii::$app->session, 'primary-cart'); });
And after all we can retrieve the cart object from the container manually in our own controllers, widgets, and other places:
$cart = Yii::createObject('app\cart\ShoppingCart')
But every controller and other object will be created via the createObject
method inside the framework. And we can use injection of cart via the controller constructor:
class CartController extends Controller { private $cart; public function __construct($id, $module, ShoppingCart $cart, $config = []) { $this->cart = $cart; parent::__construct($id, $module, $config); } // ... }
Use this injected cart object:
public function actionDelete($id) { $this->cart->remove($id); return $this->redirect(['index']); }
Change the font size
Change margin width
Change background colour