I've been using Laravel for a little while now and, over the past few months, really focused on how to improve my code and project structure. So why not share the knowledge for Laravel padawans?
Let's examine together a few real life examples in a Laravel project and how we can refactor and learn from them. Ready to improve your code?
1. Refactor your collections, son
Imagine that you're developing a website where students participate in projects and are graded every week, and your job is to display to their mentors the current average score of all students in a given project, effectively grading the project's average student score, in order to track progression.
You may come up with a Project class like this:
<?php
class Project extends Model{
/** ... code omitted for brevity **/
public function studentsAverageScore() {
$participants = $this->participants;
$sum = 0;
$totalStudents = 0;
foreach($participants as $participant) {
if ($participant->isStudent()) {
$totalStudents++;
$sum += $participant->student->lastRating()->averageScore();
}
}
return $sum / $totalStudents;
}
}
Our method studentsAverageScore()
seems to work quite nicely. We loop through our participants, check if the participant is a student (some participants can be their tutors, for example) and we keep summing up their last ratings average score (the average of each criteria in a given rating).
The issue here is that if someone has to come back to this method later for a bugfix or a changed requirement, your teammate (or even yourself) is going to "compile" this foreach
in his head before doing anything. Loops are generic and, in the case of this one, we do multiple things in each pass: we check if they're a student and then add it to a sum that we only deal again in the return statement.
Of course, this is a relatively simple example, but imagine if we did more? What if we wanted to filter this to only some students or add different weights to each one? Maybe consider all their ratings, not only their last one? This could get out of hand quickly.
So how can we express these checks and calculations better? More semantically? Fortunately, we can use a bit of functional programming with the methods that Eloquent gives us.
Instead of checking manually if a given participant is a student, using the filter
method can return only the students for us:
<?php
public function studentsAverageScore() {
$participants = $this->participants;
$participants->filter(function ($participant) {
return $participant->isStudent();
});
}
Using the filter
function, we can just pass a function as an argument to return only the participants that fulfill our condition. In this case, this call will return a subset of $participants
: only the students.
Naturally, we also need to finish this by calculating their average score. Should we do a foreach
now? It would still be suboptimal. There's a built-in solution in another function, conveniently called average
, in our returned Eloquent collection. It follows rules similar to filter
, where we just return which value we want to average from the whole colllection. The final code looks like this:
<?php
public function studentsAverageScore() {
$participants = $this->participants;
return $participants->filter(function ($participant) {
return $participant->isStudent();
})->average(function ($participant) {
return $participant->student->lastRating()->averageScore();
});
}
Since average returns a number, this is exactly what we want. Pay attention how we chained our calls, and how much better the code looks. You can almost read it like a natural language: Get the participants filtered by who is a student, then average their last rating's score and return the value. The intention of our code is cleare and our code, cleaner and more semantic.
This applies not only to PHP or Eloquent, really - you can do similar things with javascript. It's out of the scope of this article, but if you never heard of filter, map and reduce in the context of javascript, go check it out.
2. Be aware of the N+1 Query problem
Let's do some piggybacking on our code from Tip #1. Note how we fetch the Student model for a given participant in the average
function. This is highly problematic, because we're doing an additional SQL query "behind the scenes" by loading many student models one at a time.
A better solution for this would be to eager load them on our first query. When we do that, we can reduce the number considerably, instead of having N+1 queries (hence the name of that dreaded issue).
It's easy to do it with eloquent with the with
method. Let's refactor the code above:
<?php
public function studentsAverageScore() {
$participants = $this->participants()->with('student')->get();
return $participants->filter(function ($participant) {
return $participant->isStudent();
})->average(function ($participant) {
return $participant->student->lastRating()->averageScore();
});
}
Now, whenever we call $participant->student
, the student model related to the participant was already cached during our first call ($this->participants()
).
(By the way, there's still one non-optimized call related to this tip in the code above - can you spot it? Leave it in the comments)
3. Improve your Blade files
I love Blade. It's a powerful templating engine that ships with Laravel and has amazing syntax. But are you using all of its potential?
We all have to use @foreach
in order to display some collection to the users, but what if the collection is empty? A simple answer would be to use a @if
statement before the @foreach
. Turns out there's a better way to write that:
<?php
@forelse ($participants as $participant)
<li>{{ $participant->email }}</li>
@empty
<p>No participants in this project :(</p>
@endforelse
@forelse
is very similar to @foreach
, but with the added @empty
section. Much cleaner than using an @if
, isn't it?
Speaking of @if
, blade also has another directive that I love: @unless
and @endunless
. It's the exact opposite of @if
and it reads much better than just an @if
with a negative condition. If you ever used Ruby, you know how it works.
There's also some shortcuts now in Laravel 5.5 for authentication: @auth
/@endauth
and @guest
/@endguest
. It's really similar to just using @if(Auth::check())
, but reads much better. Here's a quick example from Laravel 5.5 docs:
<?php
@auth
// The user is authenticated...
@endauth
@guest
// The user is not authenticated...
@endguest
There's much more in the official docs, and you can write your own directives too. I highly recommend it - makes your template files much easier to reason instead of a bunch of meaningless ifs.
Got more useful tips?
That's it for now. Leave a comment if you have a cool tip that you like to use in your projects! And remember: better code is less time spent refactoring and fixing bugs. Your team will appreciate :)
Top comments (10)
Thank you so much for this beautifully written article. I use these quite a lot in my day-to-day work at my job. I was taught this was a while ago and have never looked back. It's such a lovely way to write methods that need to be iterated over a collection.
If you're interested, Adam Wathan released a book called Refactoring to Collections which has helped me greatly when it came to never having to write another 'traditional loop'.
Will keep this simple, nice article and good explanations!
Aren't you having another n+1 query problem?
Feels like the 'lastRating' is the piece of code that also should be eager loaded.
If that's not the problem what is? :)
You got it! :)
I understand this is for demonstrating the power of collections, but wouldn’t it be better to use a where statement in the original query to only get students that are participants rather than filtering them out in a collection method later?
You get me nice thumbs up
Do you want to have more power to write clean code ?!
Do not miss the links below !
github.com/imanghafoori1/laravel-t...
github.com/imanghafoori1/laravel-w...
To be checked thanks
Wow. Thanks for this. Very helpful.
Thanks for demonstrating usage of collection. Now try it with just a raw query and an aggregate, you'll end up with less code, and your server's CPU and memory will thank you. 😃
You refreshed my memory I will try to follow your suggestions