Book Image

Yii2 Application Development Cookbook - Third Edition

By : Andrew Bogdanov, Dmitry Eliseev
Book Image

Yii2 Application Development Cookbook - Third Edition

By: Andrew Bogdanov, Dmitry Eliseev

Overview of this book

Yii is a free, open source web application development framework written in PHP5 that promotes clean DRY design and encourages rapid development. It works to streamline your application development time and helps to ensure an extremely efficient, extensible, and maintainable end product. Being extremely performance optimized, Yii is a perfect choice for any size project. However, it has been built with sophisticated, enterprise applications in mind. You have full control over the configuration from head-to-toe (presentation-to-persistence) to conform to your enterprise development guidelines. It comes packaged with tools to help test and debug your application, and has clear and comprehensive documentation. This book is a collection of Yii2 recipes. Each recipe is represented as a full and independent item, which showcases solutions from real web-applications. So you can easily reproduce them in your environment and learn Yii2 fast and without tears. All recipes are explained with step-by-step code examples and clear screenshots. Yii2 is like a suit that looks great off the rack, but is also very easy to tailor to fit your needs. Virtually every component of the framework is extensible. This book will show how to use official extensions, extend any component, or write a new one. This book will help you create modern web applications quickly, and make sure they perform well using examples and business logic from real life. You will deal with the Yii command line, migrations, and assets. You will learn about role-based access, security, and deployment. We’ll show you how to easily get started, configure your environment, and be ready to write web applications efficiently and quickly.
Table of Contents (19 chapters)
Yii2 Application Development Cookbook Third Edition
Credits
About the Authors
About the Reviewer
www.PacktPub.com
Preface
Index

Dependency injection container


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.

Getting ready

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.

How to do it…

Carry out the following steps:

  1. Create a shopping cart class:

    <?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);
        }
    }
  2. It will work only with own items. Instead of built-in storing items to session it will delegate this responsibility to any external storage class, which will implement the StorageInterface interface.

  3. The cart class just gets the storage object in its own constructor, saves it instance into private $storage field and calls its load() and save() methods.

  4. Define a common cart storage interface with the required 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);
    }
  5. Create a simple storage implementation. It will store selected items in a server 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);
        }
    }
  6. The storage gets any framework session instance in the constructor and uses it later for retrieving and storing items.

  7. Configure the 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');
    
    //…
  8. Create the cart controller with an extended constructor:

    <?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']);
        }
    }
  9. Create a 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],
            ];
        }
    }
  10. Create the 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>
  11. Create the 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>
  12. Add link items into the main menu:

    ['label' => 'Home', 'url' => ['/site/index']],
    ['label' => 'Cart', 'url' => ['/cart/index']],
    ['label' => 'About', 'url' => ['/site/about']],
    // …
  13. Open the cart page and try to add rows:

How it works…

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']);
}

See also