DEV Community

Cover image for Avoid the Mess: Structuring Laravel View Data with ViewModels

Avoid the Mess: Structuring Laravel View Data with ViewModels

Raheel Shan on August 28, 2025

When working with .NET Framework MVC applications, I used to use bound view models, which let me use classes on the Razor templates very easily wit...
Collapse
 
xwero profile image
david duymelinck

I think the ViewModel concept is very tricky to get right, because it is easy to cause SRP violations or separation of concerns violations.
In an ideal situation to create a view, there is a storage query combiner to reduce the number of queries and a data transformer to prepare the data from the storage that is still in a raw form. These mechanisms should be configurable to make them as maintainable as possible.

Moving from an array to a class has not much added value.
If you want a root for the view data just wrap the keys, return view('index', ['page' => $data]);.
The reason an array is the most common data structure for views is because views are prone to change. Having a set schema makes it harder to make changes.
If you want more control over the array keys you could use enums.

For me the ViewModel is an abstraction that will hurt you in the long run. If you look at the Spatie controller example I already see problems when the create and edit pages will not contain the same fields.

Collapse
 
raheelshan profile image
Raheel Shan • Edited

You may be right but I disagree. I am going to post a 5-post series on this topic next. This approach will also give us autocompletion in blade views which is not possible with arrays. Moreover, I am going to introduce fully-typed blade views and the blade views will always expect specific type of data. If not provided or provided different data, they will complain. The main purpose is to follow strict structure and set a standard for the whole team to follow.

Collapse
 
xwero profile image
david duymelinck

While I agree creating a DTO like object makes it easier to autocomplete and allows to check the type. I would go all out with the DTO concept instead of having a mix of storage retrieval and a DTO.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

Yes DTO is an excellent option. I have thought it through. ViewModels can have properties have DTO type classes. Viewmodels in my views are about defining a strict type shape the views can depend on. Moreover if you have used graphql that is initiator for the required data, here viewmodels will act same and ask for data required by view.

Thread Thread
 
xwero profile image
david duymelinck

The main reason I don't like the ViewModel is the data retrieval, because you're adding the whole class to the view. Nothing is stopping you to do the data retrieval in the view, and that could be a source of problems.
if you look at the Spatie example they also have no data retrieval. The model, Post is added during instantiation.

About the shape, you can do that with DTOs too.

I think graphql is a bad comparison because it is a an independent layer, while the ViewModel is a part of the view.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

I should clarify that in my approach, the ViewModel isn’t responsible for fetching data you can do it anyhow. Retrieval stays in services, repositories, or wherever your domain logic lives. The ViewModel’s job begins after the data exists — it simply provides a typed, predictable contract for the view. That’s the same reason the Spatie example avoids retrieval inside the ViewModel, and I agree with that boundary.
On shape, DTOs can do this as well, but I find that tying the contract directly to the view makes the intent clearer for teams. DTOs are more general-purpose, while ViewModels are focused on ensuring the Blade side always receives exactly what it expects.
And about GraphQL, I only bring it up as a conceptual analogy. Both enforce a strict definition of "this is the data a view will get". The implementation context is different, but the discipline of shaping data is the same.

Thread Thread
 
xwero profile image
david duymelinck

In the handle function you call data retrieval methods, so you are contradicting your own boundary.

A DTO is what you want it to be, it can be general purpose but it can also be as specific as you want it to be. The first examples of the ViewModel classes are DTOs.
A way to structure it to make the DTOs focused is with a ViewData suffix for the classes or group them in a ViewData directory.

You are just changing the graphql concept to fit your narrative. In the first comment you mention it asks for the data, and with the latest comment you mention it is to shape the data.
This makes you look like you didn't really think it through.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

The handle() in my examples isn’t about querying storage — it just assembles data that’s already been fetched into a strict shape for the view. I’ll make that clearer going forward.

On DTOs, the distinction I’m drawing is intent. A PostViewModel communicates, this is exactly what the post Blade template will always receive. It’s a narrower, view-focused contract, even if it looks similar to a DTO.

As of GraphQL, it both initiates requests and specifies only the fields needed. That’s the mindset I want ViewModels to bring to Blade, clear, minimal and predictable data.

Thread Thread
 
xwero profile image
david duymelinck

The section of the post where the handle example is named; filling the viewmodel with data. And you are using your design pattern that is just a layer that wraps the builder methods from Laravels Model class. That for me is a tight coupling with the data storage.
So in this post you are showing how to abuse the pattern, to show it the right way in further posts? That seems a bad way to introduce a pattern.

I also think the focus on Blade templates receiving the correct information is the wrong one. It is the job of the backend to send the right data to the templates. It should not matter what template engine you are using.

I think the reason for the ViewModel is to have the two mechanisms I mentioned in my first comment or at least the data transformer. But because they are not separated I see potential problems in the long run.

Thread Thread
 
raheelshan profile image
Raheel Shan • Edited

Let me simplify what I mean by a ViewModel.

class ProductsViewModel {
    public $products;
    public $categories;
    public $brands;
}
Enter fullscreen mode Exit fullscreen mode

That’s the core idea. Forget the handle method if it’s distracting — fetch the data however you want. The point isn’t how you retrieve it, but that the view always gets a clearly defined, consistent contract.

class ProductViewModel {
        public $product;
}
Enter fullscreen mode Exit fullscreen mode

or alternately

class Product extends Eloquent{

}
Enter fullscreen mode Exit fullscreen mode

or

class ProductDTO{
    public $name;
    public $price;
}
Enter fullscreen mode Exit fullscreen mode

You could call this a DTO if you want, but when it’s bound directly to a view I find “ViewModel” communicates intent better. Yes, the backend is responsible for providing data, but the ViewModel draws the line: this is exactly how much and what kind of data the view needs. Also in my first example $products can be of type ProductDTO.

Collapse
 
developerkwame profile image
Oteng Kwame

I don't understand what's going on here

class ProductsViewModel {
    public $products;
    public $categories;
    public $brands;

    public function handle($params)
    {
        $this->brands = GetAllBrands::handle();
        $this->categories = GetAllCategories::handle();
        $this->products = GetAllProducts::handle();
        return $this;
    }
}

Enter fullscreen mode Exit fullscreen mode

How does the $params get passed to the GetAllBrands::handle etc. And where are they defined?

Collapse
 
raheelshan profile image
Raheel Shan

They are supposed to be like this from controller.

class ProductController extends Controller
{
    public function index(GetAllProductsRequest $request)
    {
        $params = $request->all();
        $productsViewModel = new ProductsViewModel();
        $response = $productsViewModel->handle($params);
        return ResponseHelper::handle('index', [ 'model' => $response ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Once done, pass down the params.

class ProductsViewModel {
    public $products;
    public $categories;
    public $brands;

    public function handle($params)
    {
        $this->brands = GetAllBrands::handle($params);
        $this->categories = GetAllCategories::handle($params);
        $this->products = GetAllProducts::handle($params);
        return $this;
    }
}
Enter fullscreen mode Exit fullscreen mode