In my last article I mentioned that I have to use view models to transform my model data into an array representation that can be used by Handlebars. If you are using the standard Laravel models and blade as view engine you will be fine with just passing on the eloquent models directly into your view and access properties/methods as needed.
But if you are consuming multiple differrent data sources that should be presented by a different view engine you need to provide a way to prepare your models for the view.
In this article I sketch the idea of view models and also a (IMHO) better solution in combination with the Responsable
feature of Laravel 5.5.
To be able to prepare the data for frontend components we introduced a ViewModel interface.
<?php
interface ViewModel
{
public function prepare(array $data) : array;
}
This definition allows to pass multiple models into the view model and the prepare
method is responsible for transforming the data into an array representation that the frontend components can understand.
If I would like to display search suggestions on the website I have a model called Suggestion
that defines a slug and a label field.
<?php
class Suggestion
{
/**
* @var string the slug version of the suggestion
*/
public $slug;
/**
* @var string the label of the suggestion
*/
public $label;
}
In my controller I fetch the suggestions from a data source into a collection, pass the collection to the SuggestionListViewModel
and build a view instance with the prepared view data.
<?php
class DemoController
{
public function index()
{
$suggestions = $datasource->get();
$viewData = $suggestionListViewModel->prepare(
compact('suggestions')
);
return response()->view('demo.index', $viewData);
}
}
The SuggestionListViewModel
is responsible for converting the collection of Suggestion
objects into an array representation by using a sub view model called SuggestionViewModel
.
<?php
class SuggestionListViewModel implements ViewModel
{
private $suggestionViewModel;
public function __construct(SuggestionViewModel $suggestionViewModel)
{
$this->suggestionViewModel = $suggestionViewModel;
}
public function prepare(array $data) : array
{
/** @var Collection */
$suggestions = $data['suggestions'];
$data = $suggestions->map(function ($suggestion) {
return $this->suggestionViewModel->prepare(
compact('suggestion')
);
});
return [
'suggestionList' => $data->toArray(),
];
}
}
The SuggestionViewModel
converts the Suggestion
object into an array of its properties.
<?php
class SuggestionViewModel implements ViewModel
{
public function prepare(array $data) : array
{
$suggestion = $data['suggestion'];
return [
'slug' => $suggestion->slug,
'label' => $suggestion->label,
];
}
}
As you can see this solution adds an additional complexity layer to your application. View models feel somehow strange, especially for Laravel developers 😉.
When we look at the SuggestionViewModel
again we can see that the view model only outputs the object properties as array. This behavior can be implemented directly in the Suggestion
model. Laravel provides the Arrayable
interface that defines a toArray
method which should be perfectly fine for our use case.
<?php
class Suggestion implements Arrayable
{
/**
* @var string the slug version of the suggestion
*/
public $slug;
/**
* @var string the label of the suggestion
*/
public $label;
public function toArray()
{
return [
'slug' => $this->slug,
'label' => $this->label,
];
}
}
Yay we have removed our first view model. The world is looking much better right now 😆.
The SuggestionListViewModel
converts a Collection
of Suggestion
objects. To be able to handle this case we box the Collection into a model.
<?php
class SuggestionList implements Arrayable
{
/**
* @var Collection
*/
public $collection;
public function toArray()
{
return [
'suggestionList' => $this->collection
->map(function ($suggestion) {
return $suggestion->toArray();
})
->toArray(),
];
}
}
We need to define our generic HandlebarsResponse
class that is responsible for creating the view / json response with the prepared view data.
<?php
class HandlebarsResponse implements Responsable
{
/**
* @var string
*/
private $view;
/**
* @var Collection
*/
private $properties;
public function __construct($view, ...$properties)
{
$this->view = $view;
$this->properties = collect($properties);
}
public function toResponse()
{
$data = $this->properties->map(function ($property) {
if ($property instanceof Arrayable) {
return $property->toArray();
}
return $property;
});
if (request()->ajax()) {
return response()->json($data);
} else {
return response()->view($this->view, $data);
}
}
}
Finally lets have a look at our modified DemoController
. Assume that the $datasource->get()
call now returns an SuggestionList
object.
<?php
class DemoController
{
public function index()
{
return new HandlebarsResponse(
'demo.index',
$datasource->get()
);
}
}
Moving the view representation logic into the model seem perfectly fine for me. Especially when you have splitted your frontend into components called suggestionList
and suggestion
you have this name binding between your frontend components and your data models in PHP.
Of course we have introduced the SuggestionList
model but as already said we get this 1:1 mapping between our frontend component and the data model in the backend.
The addition of the generic HandlebarsResponse
class makes it easy to create and understand your controller logic.