If you used Yii 2 or even Yii 1, you have probably seen plenty of code that looks similar to the following:
# controllers/MovieController.php public function actionCreate() { // ... return $this->render('create', [ 'model' => $model, 'directors' => $directors, 'actors' => $actors, 'assignedDirectors' => $assignedDirectors, 'assignedActors' => $assignedActors, ]); }
# views/movie/create.php /** * @var models\Movie $model * @var models\Director[] $directors * @var models\Actor[] $actors * @var models\Director[] $assignedDirectors * @var models\Actor[] $assignedActors */ echo $this->render('_form', [ 'model' => $model, 'directors' => $directors, 'actors' => $actors, 'assignedDirectors' => $assignedDirectors, 'assignedActors' => $assignedActors, ]);
# views/movie/_form.php /** * @var models\Movie $model * @var models\Director[] $directors * @var models\Actor[] $actors * @var models\Director[] $assignedDirectors * @var models\Actor[] $assignedActors */ # views/movie/_form.php <?php //... echo $form->field($model, 'name')->textInput(); // ...
In our example, we have a controller method that passes five items to the “create” view, which in turn passes them to the “_form” view, which is a subview of both “create” and “edit” actions. On the surface, there is nothing wrong with the above code. After all, it is idiomatic Yii code, but if we take a closer look at it, we can notice the following issues:
- The names of the variables available inside the view are not easily discoverable. To find out what variables are available, you have to either look at the controller code or add docblocks to the top of the view file, which is a practice that is recommended by the authors of the framework. However, if your views pass more data to subviews, you have to duplicate the docblocks in subviews.
- The IDEs cannot offer refactoring support. Using the above example, assume that we want to clarify the purpose of “actors” and “directors” and rename them to “availableActors” and “availableDirectors”. Without IDE support, that would require manual changes to the controller and both views, but having to make a change in more than one place is a strike against maintainability.
Enter View Model
In the statically typed world of ASP.NET MVC, data is often passed from controllers to views using objects called view models. A view model is a class that represents the data needed by a view, and typically consists exclusively of public properties. Because the view model is a class, we can take advantage of autocompletion and, if we use docblocks for the view model’s properties, type hinting in views. As PHP matures and new features are influenced by strongly typed languages, I think it’s a good time to experiment with ideas borrowed from those languages. Let’s see how implementing view models can help us improve the maintainability of Yii applications.
First, let’s define a simple view model class:
<?php namespace app\viewmodels\hello; class HelloViewModel { /** * @var string */ public $message; }
Now, let’s instantiate the view model in a controller, assign a value to the $message property, and finally pass the view model instance to a view:
<?php namespace app\controllers; use app\viewmodels\hello\HelloViewModel; use yii\web\Controller; class HelloController extends Controller { public function actionHello() { $vm = new HelloViewModel(); $vm->message = 'Hello, World!'; return $this->render('hello', ['vm' => $vm]); } }
In our view, we need to add just one docblock to enable autocompletion in the IDE.
<?php /* @var \app\viewmodels\hello\HelloViewModel $vm */ use yii\helpers\Html; ?> <h1><?= Html::encode($vm->message) ?></h1>
Benefits
As you can see, we made the following improvements:
- We need only one additional docblock at the top of each view file, no matter how many properties are defined in the view model.
- We can enjoy fantastic IDE support for renaming properties within the view models. In addition, the data available in views is easily discoverable via autocompleting $vm->
- If we use subviews, all we need to pass to them via the render() method is the view model object.
Refactoring to View Models
Let’s return to the opening example. If we refactor it to use view models, the resulting code may look similar to the following:
# viewmodels/movie/MovieFormViewModel.php class MovieFormViewModel { /** * @var \models\Movie */ public $model; /** * @var \models\Director[] */ public $directors; /** * @var \models\Actor[] */ public $actors; /** * @var \models\Director[] */ public $assignedDirectors; /** * @var \models\Actor[] */ public $assignedActors; }
# controllers/MovieController.php public function actionCreate() { // ... $vm = new MovieFormViewModel(); $vm->model = $model, $vm->directors = $directors; $vm->actors = $actors; $vm->assignedDirectors = $assignedDirectors; $vm->assignedActors = $assignedActors; return $this->render('create', ['vm' => $vm]); }
# views/movie/create.php /** * @var \viewmodels\movie\MovieFormViewModel $vm */ echo $this->render('_form', ['vm' => $vm]);</pre> <pre class="font-size-enable:false lang:php decode:true "># views/movie/_form.php /** * @var \viewmodels\movie\MovieFormViewModel $vm */ //... echo $form->field($vm->model, 'name')->textInput(['maxlength' => true]); // ...
Now that we replaced the data array passed to a view with an object, not only did we eliminate a lot of duplicate code, but — with a little assistance from our IDE — we can very easily rename the data elements in the view. My IDE already saves me a lot of time, but to finally have full IDE support in view files is simply amazing.