fetzi.dev

Getting rid of view models

4 minutes

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.

The View Model approach

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.

Example of View Model usage

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 😉.

A model should represent itself

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()
        );
    }
}

Conclusion

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.

Resources

This might be also interesting

Do you enjoy reading my blog?

sponsor me at ko-fi