20 Eloquent tricks - more
Recently I stumbled upon interesting article by Povilas Korop on laravel-news 20 eloquent tips and tricks. First of all thanks for sharing your knowledge Povilas – it helps many people out there, keep it up!
As you may know, I’ve worked with Eloquent for long and love its features, at the same time I’ve become very sensitive about some dangers lurking for inexperienced developers that come with all the magic of Eloquent :) Let’s use this opportunity to look at the tips from different perspective and learn even more by going through some of them in more details!
In order to make it valuable for you I’m keeping it as concise as possible:
-
in/decrement
: GOOD -
findOrFail
: GOOD, but there’s a catch infindOrFail
. Laravel internally handlesModelNotFoundException
here and it means that any call that fails here, will trigger a404 NOT FOUND
response from your app. This works fine when called for example for something like this:Route::get('products/{id}', function ($id) { return Product::findOrFail($id); });
Laravel uses the same pattern for implicit route model binding described here
However, if you’re calling your query deeper in the code of your application, you do not want to automatically trigger
404
for any not-found record, as it might have nothing to do with the routing or even HTTP layer at all.
That being said,findOrFail
is cool for top, HTTP layer. Other than that I recommend something between the lines of (yes, oldskool, even archaic, no magic involved… you get the idea):$model = $someQuery->find($id); if (is_null($model)) { // handle logic when record is not found }
-
boot
GOOD -
default ordering on relationships
– definitely no (unless the relationship itself explicitly states that):// Organization public function approvedUsers() { return $this->hasMany('App\\User')->where('approved', 1)->orderBy('email'); } // then somewhere you need this $organization->approvedUsers()->latest()->get();
Now you’re scratching your head and trying to figure out why you don’t get
latest
users. After a while you notice that they are ordered by email, so you’re tracking it down – relationship is found, so the question arises – can I change the relationship? Not really, it would break some functionality, probably untested, so it is not the way to go. Eventually it seems that the only way is a NEW relationhsip. Not good at all.You could make it better, if you really need default ordering, by creating custom and explicit relation in the first place – just like here
-
props
: GOOD. There’s one exception – I recommend not using$appends
EVER as this one very easily leads to huge problems if abused. Imagine using accessor that does some heavy logic (which it should not), like querying relation and returning its value:public function getOrganizationNameAttribute() { return $this->organization->name; }
this is evil as it is, trust me. Now, imagine you have an API endpoint that serves your model directly, and some time later your colleague needs to add
organization_name
to the JSON result. Easy does it, right? Just added to$appends
and it is automagically in the JSON!Only now you realize that your endpoint serves data 20 times slower now and you have no idea why. Here it is – magic has its cost. You endpoint used, say, 5 queries before, but now it uses 5 + N queries, because accessor runs the query under the hood. Finding it out is not trivial and even noticing it might take long, which can hurt the business. Don’t go this path, be like new Leo and save yourself some headache 😉
-
findMany
: GOOD. I’d only add note about different return valuesfind(int|string 1) : ?Model
whilefind(array|Arrayable $ids) : Collection
. Which means the first returnsModel
ornull
, but the latter returnsCollection
always (even if empty). -
dynamic wheres
: NEVER do this. It is illogical, confusing, impossible to understand for anyone without decent knowledge about Eloquent.
Next thing is this: if you ever need to restructure your DB or just investigate usage and find all queries against a column, you’d find easilywhere('some_column')
,orderBy('some_column')
etc by simply grepping against'some_column'
, but you won’t findwhereSomeColumn($value)
.
Another catch is this: you know already that Eloquent provides those magic calls (dynamic wheres), so when you seewhereDate
you can think it is the same. But wait, there’s nodate
column in the table, so WTF? Oh, sure, this is actually real methodwhereDate($column, $value)
. You get the idea, trust me, you neighbor and their dog will hate you if you use it even once. -
order by relation
: Custom relation definitely GOOD, but ordering nope. While it is valid, it’s dangerous as well. You should NEVER depend on the collection ordering if you don’t know how many items collection has (and it must be rather small collection). Imagine this code when your page grows from 20Topics
to 20 thousandsTopics
– then every single pageload is looong seconds, sometimes memory exhausted errors occur etc. Databases are pretty good with ordering, use them. -
when
: I totally agree with the comment: It may not feel shorter or more elegant – True, it is not shorter, nor more elegant, nor better in any way for me. Here’s what I recommend:if ($request->has('role')) { // I know the intent already $query->where('role_d', $request->get('role')); // then following action } // vs // Reading this I am not sure what false means, no idea what $role will be // in the closure, I have to spend a few seconds analyzing it every time. // It adds up in big codebase and I love to avoid unnecessary overload $query->when(request('role', false), function ($q, $role) { return $q->where('role_d', $role); });
One may argue that you HAVE TO avoid
if
statements, but here’s the thing – removing them doesn’t make you functional programmer, usingfunction () {}
rather thanif
doesn’t make you functional programmer, it doesn’t add any value to the business either. -
withDefault
: OKAY in general. I prefer explicit code, so don’t use it myself, but it’s matter of preference. However using it strictly for presentation purpose as in the example – no like. -
accessor ordering
: Smart – as in No. 8 be careful in cases, where you cannot be sure bout collection size. -
global scope
: OKAY. Personally I wouldn’t do that in a global scope, as global scopes are good for only so many cases, and only to apply somewhere
constraints (SoftDeletes
is a good example,Active
might be applicable in many cases etc). -
raw
: GOOD -
replicate
: GOOD -
chunk
: VERY GOOD. I would add that you NEVER want to callModel::all()
unless you have a dictionary-like model, say, a list of countries. A model that you can be sure will never grow (there won’t be more than 300 countries in our lifetime I guess). -
generators
: GOOD -
updated_at
: GOOD -
update
return value: GOOD -
orWhere
: This one is actually incorrect, both in the code and logically. I believe it comes from the bad example in the Laravel’s own docs. I corrected the example and added some notes to the docs recently, so feel free to look it up now docs -
orWhere
again: Same as No. 19.