Understanding ViewModels in PHP

Published Apr 13, 20214 min read

You may have come across the term ViewModels in a tutorial but never fully understood how or why to use them? If that's the case or you are first hearing about it, we'll try to explain it. As with many other things in software, there are different names for the same thing, and ViewModels are no exception. Some might call it ModelView Presenter or just a Presenter.

So what is a ViewModel? It is a layer that transforms your data and prepares it for being used in a View. This abstraction keeps you from repeating the code and pushing data from controllers directly to views. To put simply, ViewModels are thin classes that take data and arrange it, so it's useful for your Views.

Let's see a simple use case where ViewModels might be helpful.

At the very top of this post, you'll see that there is a reading time estimate. We'll use this as an example of how ViewModels can be used in your Laravel application. Note that while the example is based on Laravel, it by no means prevents you from using it in any other MVC framework.

To show a post, you might have a PostController with a show method. It would look something like this:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }
}

This will pass the $post model to a view. The $post model, contains a $title and $content, but you don't have any other stats about the post like, character_count , word_count and read_time. This kind of data is a great candidate to be extracted to a ViewModel.

One of the approaches to solve this kind of problem would be calculating those stats directly inside the controllers show method. Maybe something like this:

    public function show(Post $post)
    {
        // Calculate stats
        $character_count = strlen($post->content);
        $word_count      = str_word_count($post->content);
        $read_time       = ceil($word_count / 200);

        return view('posts.show', compact('post', 'character_count', 'word_count', 'read_time');
    }

The problem with this approach is that you cannot reuse this code, and if you had some other kinds of processing, the controller could quickly grow big.

You also might be tempted to put this into the model itself

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function characterCount(): int
    {
        return strlen(this->content);
    }

    public function wordCount(): int
    {
        return str_word_count($this->content);
    }

    public function readTime(): int
    {
        return ceil($word_count / 200);
    }
}

But a new requirement comes in, and they want you to add the same to Article model and to the Tutorial model. You keep duplicating your code. If they decide that the avarage read time per minute is 220 words, you'll have to change it in 3 places.

ViewModels to the rescue

This kind of problem is best solved with a ViewModel. It will keep the controller lean, and the data will be reusable.

We can put all of our ViewModels inside a folder called ViewModels in app/ViewModels. For the above example, we will name the file ContentStats, so the full path would be app/ViewModels/ContentStats

<?php

namespace App\ViewModels;

class ContentStats
{
    public function __invoke($content)
    {
        // Calculate stats
        $character_count = strlen($content);
        $word_count      = str_word_count($content);
        $read_time       = ceil($word_count / 200);

        return [
            'character_count' => $character_count,
            'word_count'      => $word_count,
            'read_time'       => $read_time
        ];
    }
}

You may notice that it is an invocable class. The magic method __invoke will be run when a script tries to call an object as a function. This will make our code inside the controller cleaner.

<?php

namespace App\Http\Controllers;

use App\ViewModels\ContentStats;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function show(Post $post)
    {
        $contentStats = new ContentStats();
        $stats        = $contentStats($post->content);

        return view('posts.show', compact('post', 'stats'));
    }
}

This approach makes the code reusable and controllers lean.

Conclusion

We saw how ViewModels could help us out in organizing our code better. There are a couple of packages available for ViewModels, but I think it's unnecessary to include additional dependency if you have simple use cases like the above one.

I hope you've learned something and might use ViewModels in your code. If you have any comments or suggestions or want to discuss the topic, you can do it here