spatie/laravel-view-models
Move complex view-prep logic out of controllers into dedicated Laravel view model classes. Extend Spatie\ViewModels\ViewModel to transform data for views, expose computed properties, and keep templates clean and focused.
Installation:
composer require spatie/laravel-view-models
Publish the config (optional):
php artisan vendor:publish --provider="Spatie\ViewModels\ViewModelsServiceProvider"
First Use Case:
Create a view model class (e.g., app/ViewModels/PostViewModel.php):
namespace App\ViewModels;
use Spatie\ViewModels\ViewModel;
class PostViewModel extends ViewModel
{
public function __construct(
public string $title,
public string $content,
public int $commentsCount
) {}
}
Usage in Controller:
use App\ViewModels\PostViewModel;
public function show(Post $post)
{
return view('posts.show', [
'viewModel' => PostViewModel::createFrom($post)
->withCommentsCount($post->comments()->count())
]);
}
View Access:
<h1>{{ $viewModel->title }}</h1>
<p>{{ $viewModel->content }}</p>
<span>Comments: {{ $viewModel->commentsCount }}</span>
config/view-models.php (for customization)Model-to-ViewModel Transformation:
Use createFrom() to instantiate a view model from a model instance, then chain methods to enrich data:
PostViewModel::createFrom($post)
->withCommentsCount($post->comments()->count())
->withAuthor($post->author->name)
->withTags($post->tags->pluck('name'))
Dynamic Properties:
Add computed properties via with() or withDynamic():
public function withDynamic(Post $post)
{
return $this->with([
'isPopular' => $post->views > 1000,
'formattedDate' => $post->created_at->format('M d, Y'),
]);
}
View Model Factories:
For complex logic, use a factory class (e.g., PostViewModelFactory):
public function create(Post $post)
{
return new PostViewModel(
$post->title,
$post->content,
$post->comments()->count(),
$post->author->name,
);
}
Integration with Controllers:
Spatie\ViewModels\Traits\ResourceController for DRY code.return response()->json($viewModel);
Caching: Cache view models in controllers or factories:
$viewModel = Cache::remember("post.{$post->id}.viewmodel", now()->addHours(1), function () use ($post) {
return PostViewModel::createFrom($post)->withCommentsCount($post->comments()->count());
});
class PostViewModel extends ViewModel
{
public function __construct(
public AuthorViewModel $author,
public CommentViewModel $latestComment
) {}
}
$posts = Post::all();
$viewModels = PostViewModel::collection($posts)
->each(fn ($vm) => $vm->withCommentsCount($vm->post->comments()->count()));
public function withDynamic(Post $post)
{
return $this->with([
'excerpt' => $post->isPublished ? Str::limit($post->content, 200) : null,
]);
}
Over-Fetching Data:
with() sparingly and lazy-load relationships where possible:
$viewModel->withCommentsCount($post->comments()->count()); // Eager-load
$viewModel->withAuthor($post->author); // Load on demand in view model
Circular Dependencies:
PostViewModel → AuthorViewModel → PostViewModel).class AuthorViewModel extends ViewModel
{
public function __construct(
public string $name,
public ?int $postId = null // Store ID instead of model
) {}
}
Immutable Properties:
with() or withDynamic() to add new properties:
// ❌ Fails: $viewModel->newProperty = 'value';
// ✅ Works: $viewModel->with(['newProperty' => 'value']);
Performance with Large Collections:
each() with batch processing or chunking:
PostViewModel::collection($posts)
->each(fn ($vm) => $vm->withCommentsCount($vm->post->comments()->count()))
->chunk(100); // Process in batches
Testing Quirks:
$viewModel = Mockery::mock(PostViewModel::class)
->makePartial()
->shouldAllowMockingProtectedMethods();
dd() or dump() to inspect view model contents:
dd(PostViewModel::createFrom($post)->withCommentsCount($post->comments()->count()));
UndefinedProperty errors if a property isn’t defined. Use isset() in views:
@if (isset($viewModel->optionalProperty))
{{ $viewModel->optionalProperty }}
@endif
// ❌ Wrong: PostViewModel::create($post); // Missing 'from' if using model
// ✅ Correct: PostViewModel::createFrom($post);
Custom View Model Macros:
Add reusable methods to the base ViewModel class:
use Spatie\ViewModels\ViewModel;
ViewModel::macro('withMeta', function (array $meta) {
return $this->with($meta);
});
Usage:
PostViewModel::createFrom($post)->withMeta(['key' => 'value']);
View Model Events: Listen for view model creation/modification:
ViewModel::created(function (ViewModel $viewModel) {
Log::info("ViewModel created: " . get_class($viewModel));
});
Custom Validation: Validate view model data before passing to views:
public function withDynamic(Post $post)
{
$this->validate([
'title' => 'required|max:255',
'content' => 'required',
]);
return $this->with([
'isValid' => true,
]);
}
View Model Caching Strategies:
Override the default caching behavior in config/view-models.php:
'cache' => [
'driver' => 'redis',
'prefix' => 'view_models',
],
Integration with Form Requests: Use view models to validate and transform form data:
public function rules()
{
return [
'title' => 'required|max:255',
'content' => 'required',
];
}
public function withViewModel()
{
return PostViewModel::create(
$this->validated('title'),
$this->validated('content'),
0 // Default comments count
);
}
How can I help you explore Laravel packages today?