Using view models in Silverstripe to tidy up your view logic

What is a view model?

A view model is a class that is used to prepare data before it is handed to the template to be rendered. Each view model class generally corresponds to a specific template. This can be a good way of removing complexity from a controller, and encapsulating view logic in a class tied to the template where that data is rendered.

I think I first learnt about the idea of view models (many years ago) from FuelPHP, which has since renamed them to presenters. They probably weren’t the first to use view models, and many other frameworks have used the concept since.

Create a view model using ModelData

Although view models are not officially supported or documented by Silverstripe, a class that extends ModelData makes for a good view model.

namespace App\View;

use App\Course;
use App\Enrolment;
use SilverStripe\Model\ModelData;

class CourseOutline extends ModelData
{
    public function __construct(
        public Course $Course,
        public Enrolment $Enrolment
    ) {}

    protected function getStartDate()
    {
        $courseStartDate = $this->Course->dbObject('StartDate');

        return $courseStartDate->InFuture()
            ? $courseStartDate
            : $this->Enrolment->dbObject('StartDate');
    }

    ...
}

When a class extending ModelData is rendered, it will handle rendering the corresponding template and making the data available to the template.

…/templates/App/View/CourseOutline.ss

<section>
  <h2>$Course.Title</h2>
  <p>Start date: $StartDate.Short</p>
  ...
</section>

To use the view model, simply pass an instance to any template and output it.

class CoursePageController extends PageController
{
    public function index()
    {
        ...

        return [
            'CourseOutline' => new CourseOutline($course, $enrolment),
        ];
    }
}
<main>
  $CourseOutline
  ...
</main>

This is obviously a very simplistic example, but hopefully you can see that when your controller becomes cluttered with code preparing data for the template, a view model is a good way of extracting that logic into a class whose sole concern is the template.

Nested view models

Of course, we are not limited to only creating view models in the controller. A view model can be created anywhere, including inside another view model.

class CourseOutline extends ModelData
{
    ...

    protected function getTimeline()
    {
        return new CourseTimeline($this->Course, $this->Enrolment);
    }

    protected function getTopics()
    {
        $list = new ArrayList;

        foreach ($this->Course->Topics() as $topic) {
            $list->push(new CourseTopic($topic, $this->Enrolment));
        }

        return $list;
    }
}
$Timeline

<h2>Topics</h2>
<ul>
  <% loop $Topics %>
    <li>$Me</li>
  <% end_loop %>
</ul>

Casting

Casting in ModelData works the same way as it does in a DataObject or Page class.

private static $casting = [
    'Description' => 'HTMLFragment',
    'Events' => 'ArrayList',
];

protected function getDescription(): string
{
    ...
}

protected function getEvents(): array
{
    ...
}

That’s all there is to it

I hope this was a helpful tip.

Although view models are not often needed, when they are, they can be a good way of tidying up the code and keeping things clean and simple.