DEV Community

Edmilson Rocha
Edmilson Rocha

Posted on

3 simple tips to improve your Laravel code today

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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();
    });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

@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
Enter fullscreen mode Exit fullscreen mode

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 :)

Discussion (11)

Collapse
yokim profile image
Yokim Pillay

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'.

Collapse
wotta profile image
Wouter van Marrum

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? :)

Collapse
edmilsonrobson profile image
Edmilson Rocha Author

You got it! :)

Collapse
chrisrhymes profile image
C.S. Rhymes

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?

Collapse
omarajmi profile image
Omar Ajmi

You get me nice thumbs up

Collapse
imanghafoori1 profile image
Iman

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...

Collapse
moatazabdalmageed profile image
Moataz Mohammady

To be checked thanks

Collapse
jamonjamon profile image
Jaimie Carter

Wow. Thanks for this. Very helpful.

Collapse
moatazabdalmageed profile image
Moataz Mohammady

You refreshed my memory I will try to follow your suggestions

Collapse
danrichards profile image
Dan Richards

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. 😃

Collapse
danersharifi profile image
Daner-Sharifi

This article is one of the best, quick and shortened articles that I read.
THANKS :))