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