6

08. View models

 4 years ago
source link: https://stitcher.io/blog/laravel-beyond-crud-08-view-models
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this chapter of myLaravel beyond CRUD series, we take a deep dive in the application layer. A major trend throughout the series is to keep code clean, concise and manageable. This chapter won't be any different, as we'll look at how to keep controllers clean and to-the-point.

The pattern we'll use to help us is called the view model pattern. As its name suggests, these classes are models to your view files, they are responsible for providing data to a view, which would otherwise come directly from the controller or the domain model. They allow a better separation of concerns, and provide more flexibility for the developer.

In essence, view models are simple classes that take some data, and transform it into something usable for the view. In this chapter I'll show you the basic principles of the pattern, we'll take a look at how they integrate in Laravel projects, and finally I'll show you how we use the pattern in one of our projects.

Let's get started.

Say you have a form to create a blog post with a category. You'll need a way to fill the select box in the view with category options. The controller has to provide those.

public function create()
{
    return view('blog.form', [
        'categories' => Category::all(),
    ]);
}

The above example works for the create method, but let's not forget we should also be able to edit existing posts.

public function edit(Post $post)
{
    return view('blog.form', [
        'post' => $post,
        'categories' => Category::all(),
    ]);
}

Next there's a new business requirement: users should be restricted in which categories they are allowed to post in. In other words: the category selection should be restricted based on the user.

return view('blog.form', [
    'categories' => Category::allowedForUser(
        current_user()
    )->get(),
]);

This approach doesn't scale. You'll have to change code both in the create and edit method. Can you imagine what happens when you need to add tags to a post? Or if there's another special admin form for creating and editing posts?

The next solution is to have the post model itself provide the categories, like so:

class Post extends Model
{
    public static function allowedCategories(): Collection 
    {
        return Category::query()
            ->allowedForUser(current_user())
            ->get();
    }
}

There are numerous reasons why this is a bad idea, though it happens often in Laravel projects. Let's focus on the most relevant problem for our case: it still allows for duplication.

Say there's a new model News which also needs the same category selection. This again causes duplication, but on the model level instead of in the controllers.

Another option is to put the method on the User model. This makes the most sense, but also makes maintenance harder. Imagine we're using tags as mentioned before. They don't rely on the user. Now we need to get the categories from the user model, and tags from somewhere else.

I hope it's clear that using models as data providers for views also isn't the silver bullet.

In summary, wherever you try to get the categories from, there always seems to be some code duplication. This makes it harder to maintain and reason about the code.

This is where view models come into play. They encapsulate all this logic so that it can be reused in different places. They have one responsibility and one responsibility only: providing the view with the correct data.

class PostFormViewModel
{
    public function __construct(User $user, Post $post = null) 
    {
        $this->user = $user;
        $this->post = $post;
    }
    
    public function post(): Post
    {
        return $this->post ?? new Post();
    }
    
    public function categories(): Collection
    {
        return Category::allowedForUser($this->user)->get();
    }
}

Let's name a few key features of such a class:

post

This is what the controller looks like:

class PostsController
{
    public function create()
    {
        $viewModel = new PostFormViewModel(
            current_user()
        );
        
        return view('blog.form', compact('viewModel'));
    }
    
    public function edit(Post $post)
    {
        $viewModel = new PostFormViewModel(
            current_user(), 
            $post
        );
    
        return view('blog.form', compact('viewModel'));
    }
}

And finally, it can be used in the view like so:

<input value="{{ $viewModel->post()->title }}" />
<input value="{{ $viewModel->post()->body }}" />

<select>
    @foreach ($viewModel->categories() as $category)
        <option value="{{ $category->id }}">
            {{ $category->name }}
        </option>
    @endforeach
</select>

View models in Laravel

The previous example showed a simple class with some methods as our view model. This is sufficient to use the pattern, but within Laravel projects there are a few more niceties we can add.

For example, you can pass a view model directly to the view function if the view model implements Arrayable .

public function create()
{
    $viewModel = new PostFormViewModel(
        current_user()
    );
    
    return view('blog.form', $viewModel);
}

The view can now directly use the view model's properties like $post and $categories . The previous example now looks like this:

<input value="{{ $post->title }}" />
<input value="{{ $post->body }}" />

<select>
    @foreach ($categories as $category)
        <option value="{{ $category->id }}">
            {{ $category->name }}
        </option>
    @endforeach
</select>

You can also return the view model itself as JSON data, by implementing Responsable . This can be useful when you're saving the form via an AJAX call, and want to repopulate it with up-to-date data after the call is done.

public function update(Request $request, Post $post)
{
    // Update the post…

    return new PostFormViewModel(
        current_user(),
        $post
    );
}

You might see a similarity between view models and Laravel resources. Remember that resources map one-to-one on a model, whereas view models may provide whatever data they want.

In our projects, we're actually using resources and view models combined:

class PostViewModel
{
    // …
    
    public function values(): array
    {
        return PostResource::make(
            $this->post ?? new Post()
        )->resolve();
    }
}

Finally, in this project we're working with Vue form components, which require JSON data. We've made an abstraction that provides this JSON data instead of objects or arrays, when calling the magic getter:

abstract class ViewModel
{
    // …
    
    public function __get($name): ?string
    {
        $name = Str::camel($name);
    
        // Some validation…
    
        $values = $this->{$name}();
    
        if (! is_string($values)) {
            return json_encode($values);
        }
    
        return $values;
    }
}

Instead of calling the view model methods, we can call their properties and get JSON back.

<select-field
    label="{{ __('Post category') }}"
    name="post_category_id"
    :options="{{ $postViewModel->post_categories }}"
></select-field>

Wait, what about view composers?

You might be thinking there's some overlap with Laravel's view composers, but don't be mistaken. The Laravel documentation explains view composers like so:

View composers are callbacks or class methods that are called when a view is rendered. If you have data that you want to be bound to a view each time that view is rendered, a view composer can help you organize that logic into a single location.

View composers are registered like this (the example is taken from the Laravel docs ):

class ViewComposerServiceProvider extends ServiceProvider
{
    public function boot()
    {
        View::composer(
            'profile', ProfileComposer::class
        );

        View::composer('dashboard', function ($view) {
            // …
        });
    }
    
    // …
}

As you can see, you can both use a class and a closure which you can use to add variables to a view.

Here's how view composers are used in controllers.

class ProfileController
{
    public function index()
    {
        return new view('profile');
    }
}

Can you see them? Nope, of course not: view composers are registered somewhere in the global state, and you don't know which variables are available to the view, without that implicit knowledge.

Now I know that this isn't a problem in small projects. When you're the only developer and have 20 controllers and maybe 20 view composers, it'll all fit in your head.

But what about the kind of projects we're writing about in this series? When you're working with several developers, in a codebase that counts thousands upon thousands lines of code. It won't fit in your head anymore, not on that scale; let alone your colleagues also having the same knowledge. That's why the view model pattern is the preferred approach. It makes clear from the controller itself what variables are available to the view. On top of that, you can re-use the same view model for multiple contexts.

One last benefit — one you might not have thought about —

is that we can pass data into the view model explicitly. If you want to use a route argument or bound model to determine data passed to the view, it is done explicitly.

In conclusion: managing global state is a pain in large applications, especially when you're working with multiple developers on the same project. Also remember that just because two means have the same end result, that doesn't mean that they are the same!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK