Update 2017-04-07: note that coming in Laravel 5.5,
intersect
will be removed andonly
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?
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/12{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.