Michael Dyrynda
Home Blog Podcasts
 
Partial model updates in Laravel March 27th, 2017

Update 2017-04-07: note that coming in Laravel 5.5, intersect will be removed and only will behave similarly to what is described below. See this PR for more information.

Background

Many Laravel developers would be familiar with the helpful only method found on the request object, which allows you to specify keys to pluck from the request:

1public function create()
2{
3 $post = Post::create(request()->only('title', 'slug', 'excerpt', 'body', 'published_at'));
4 
5 return redirect()->route('posts.show', $post->id);
6}

Not only does this simplify your workflow, it works quite nicely when completely unguarding your models by setting protected $guarded = [];. How many times have you been in a situation where you've added a new field to your model and been met with a MassAssignmentException because you forgot to update the $fillable property?

I, too, like to live dangerously

For newcomers to Laravel, you might find this suggestion dangerous, but using only means you will only pass the desired input to your model irrespective of what was passed via the request itself. Explicit safety, right in your controllers. I prefer this practice personally, to the point I usually make it the default in my applications by declaring a base model and having my models extend from that.

1<?php
2 
3namespace App;
4 
5use Illuminate\Database\Eloquent\Model as BaseModel;
6 
7class Model extends BaseModel
8{
9 protected $guarded = [];
10}

Remember, if you're going to take this approach, never use request()->all() when passing data into your model's create or update methods!

Using intersect() to simplify partial updates

Adam Wathan tweeted about an approach he uncovered whilst helping somebody out when approaching partial model updates. Consider this approach in the following, fairly standard controller update method:

1public function update(Post $post)
2{
3 if (request()->has('title')) {
4 $post->title = request('title');
5 }
6 
7 if (request()->has('slug')) {
8 $post->slug = request('slug');
9 }
10 
11 if (request()->has('excerpt')) {
12 $post->excerpt = request('excerpt');
13 }
14 
15 if (request()->has('body')) {
16 $post->body = request('body');
17 }
18 
19 if (request()->has('published_at')) {
20 $post->published_at = request('published_at');
21 }
22 
23 $post->save();
24 
25 return redirect()->route('posts.show', $post->id);
26}

Instead of littering your controller method with multiple request()->has('field') checks, you can employ the request object's intersect method. The intersect method will return a new array containing only the keys that are present in both the specified list and the request itself.

Using intersect allows you to easily handle a PATCH request - one where you partially update a resource's data, rather than all of it as with a PUT - in a much more concise manner:

1public function update(Post $post)
2{
3 $post->update(request()->intersect('title', 'slug', 'excerpt', 'body', 'published_at'));
4 
5 return redirect()->route('posts.show', $post->id);
6}

Now, when you submit a PATCH request to the update route with only the fields that you are wanting to change, when the request reaches the controller, the intersect method will compare your request input against the array of accepted keys (title, slug, excerpt, body, published_at) and return an array containing values that exist in both the request and this list. In this example, that will be title and slug.

1PATCH /posts/1
2{
3 "title": "This is the new post title",
4 "slug": "this-is-the-new-post-title"
5}

Differences between only() and intersect()

Whilst only() is a useful method, it will return any key that doesn't exist in the request input with a null value, which combined with your now unguarded model, could lead you into issues.

1// PATCH /posts/1
2request()->only(
3 'title', // = 'title'
4 'slug', // = 'slug'
5 'excerpt', // = 'excerpt'
6 'body', // = 'body'
7 'published_at', // = null
8 'non_existent' // not in request
9);
10 
11// [
12// "title" => "title",
13// "slug" => "slug",
14// "excerpt" => "excerpt",
15// "body" => "body",
16// "published_at" => null,
17// "non_existent" => null,
18// ]

Meanwhile, intersect() will only return non-empty values that are present in the request input and the list of keys passed into the method.

1// PATCH /posts/1
2request()->intersect(
3 'title', // = 'title'
4 'slug', // = 'slug'
5 'excerpt', // = 'excerpt'
6 'body', // = 'body'
7 'published_at', // = null
8 'non_existent' // not in request
9);
10 
11// [
12// "title" => "title",
13// "slug" => "slug",
14// "excerpt" => "excerpt",
15// "body" => "body",
16// ]

A caveat to using intersect()

If you want to preserve keys that are posted with empty values - for example, if you wanted to make the published_at value null - you'll currently (5.4.16 at the time of writing) have to consider a manual approach.

Something along the following lines will do the trick:

1array_only(request()->all(), [
2 'title', /* = 'title' */
3 'body', /* = 'body */
4 'non_existent',
5 'published_at', /* = null */
6]);
7 
8// [
9// "title" => "title",
10// "body" => "body",
11// "published_at" => null,
12// ]

Using Laravel's macro functionality, we can define the following in the boot method of your AppServiceProvider, for example:

1\Illuminate\Support\Facades\Request::macro('onlyIncludingEmpty', function ($keys) {
2 return array_only($this->all(), $keys);
3});

This will allow you to use the onlyIncludingEmpty method on the request object directly:

1public function update(Post $post)
2{
3 $post->update(request()->onlyIncludingEmpty(['title', 'slug', 'excerpt', 'body', 'published_at']));
4 
5 return redirect()->route('posts.show', $post->id);
6}

Conclusion

Whilst intersect is nothing new or particularly special in Laravel-land, it is a concise way of handling a fairly common approach to updating application data. It removes a lot of superfluous code and conditional statements by leveraging framework tools on top of native language functions.

Be mindful of the differences between only and intersect, and how intersect won't return keys that have empty values.

I'm a real developer ™
Michael Dyrynda

@michaeldyrynda

I am a software developer specialising in PHP and the Laravel Framework, and a freelancer, blogger, and podcaster by night.

Proudly hosted with Vultr

Syntax highlighting by Torchlight