Service locator
Instead of manually creating instances of different shared services (application components) we can get them from a special global object, which contains configurations and instances of all components.
A service locator is a global object that contains a list of components or definitions, uniquely identified by an ID, and allow us to retrieve any needed instance by its ID. The locator creates a single instance of the component on-the-fly at the first call and returns a previous instance at the subsequent calls.
In this recipe, we will create a shopping cart component and will write a cart controller for working with it.
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-start-installation.html.
How to do it…
Carry out the following steps to create a shopping cart component:
- Create a shopping cart component. It will store selected items in a user session:
<?php namespace app\components; use Yii; use yii\base\Component; class ShoppingCart extends Component { public $sessionKey = 'cart'; private $_items = []; 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 = Yii::$app->session->get($this->sessionKey, []); } private function saveItems() { Yii::$app->session->set($this->sessionKey, $this->_items); } }
- Register the
ShoppingCart
in service locator as an application component in theconfig/web.php
file:'components' => [ … 'cart => [ 'class' => 'app\components\ShoppingCart', 'sessionKey' => 'primary-cart', ], ]
- Create a cart controller:
<?php namespace app\controllers; use app\models\CartAddForm; use Yii; use yii\data\ArrayDataProvider; use yii\filters\VerbFilter; use yii\web\Controller; class CartController extends Controller { public function behaviors() { return [ 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'delete' => ['post'], ], ], ]; } public function actionIndex() { $dataProvider = new ArrayDataProvider([ 'allModels' => Yii::$app->cart->getItems(), ]); return $this->render('index', [ 'dataProvider' => $dataProvider, ]); } public function actionAdd() { $form = new CartAddForm(); if ($form->load(Yii::$app->request->post()) && $form->validate()) { Yii::$app->cart->add($form->productId, $form->amount); return $this->redirect(['index']); } return $this->render('add', [ 'model' => $form, ]); } public function actionDelete($id) { Yii::$app->cart->remove($id); return $this->redirect(['index']); } }
- 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], ]; } }
- 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="site-contact"> <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>
- 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="site-contact"> <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>
- Add a link item into the main menu:
['label' => 'Home', 'url' => ['/site/index']], ['label' => 'Cart', 'url' => ['/cart/index']], ['label' => 'About', 'url' => ['/site/about']], // …
- Open the cart page and try to add rows:
How it works…
First of all we created our own class with a public sessionKey
option:
<?php namespace app\components; use yii\base\Component; class ShoppingCart extends Component { public $sessionKey = 'cart'; // … }
Secondly, we added the component definition into the components
section of the configuration file:
'components' => [ … 'cart => [ 'class' => 'app\components\ShoppingCart', 'sessionKey' => 'primary-cart', ], ]
Right now we can retrieve the component instance in two ways:
$cart = Yii::$app->cart; $cart = Yii::$app->get('cart');
And we can use this object in our own controllers, widgets, and other places.
When we call any component such as cart
:
Yii::$app->cart
We call the virtual property of the Application
class instance in the Yii::$app
static variable. But the yii\base\Application
class extends the yii\base\Module
class, which extends the yii\di\ServiceLocator
class with the __get
magic method. This magic method just calls the get()
method of the yii\di\ServiceLocator
class:
namespace yii\di; class ServiceLocator extends Component { private $_components = []; private $_definitions = []; public function __get($name) { if ($this->has($name)) { return $this->get($name); } else { return parent::__get($name); } } // … }
As a result it is an alternative to directly calling the service via the get
method:
Yii::$app->get('cart);
When we get a component from the get
method of service locator, the locator finds needed definition in its _definitions
list and if successful it creates a new object by the definition on the fly, registers it in its own list of complete instances _components
and returns the object.
If we get some component, multiplying the locator will always return the previous saved instance again and again:
$cart1 = Yii::$app->cart; $cart2 = Yii::$app->cart; var_dump($cart1 === $cart2); // bool(true)
It allows us to use the shared single cart instance Yii::$app->cart
or single database connection Yii::$app->db
instead of creating one large set from scratch again and again.
See also
- For more information about the service locator and about core framework components refer to http://www.yiiframework.com/doc-2.0/guide-concept-service-locator.html
- The Configuring components recipe
- The Creating components recipe in Chapter 8, Extending Yii