Advanced Laravel Eloquent usage

advanced-laravel-eloquent-usage-header.png

This tutorial explores how to use advanced Laravel Eloquent. It provides sample code to demonstrate each use case, and shows how using Eloquent can improve your code.

Introduction

In this tutorial, we are going to dig deep into the ORM (object-relational mapper) Laravel uses Eloquent. We will throw some light on some less used features of Laravel Eloquent and how it can make your development process even easier.

Laravel makes building PHP applications a breeze. It is designed to provide methods for handling the basics your application will need to run – database interaction, routing, sessions, caching and more. It has service providers that allow you to load custom configurations and extend Laravel’s capabilities to suit your needs.

Requirements

To follow along in this tutorial you must:

  • Have a working knowledge of PHP.
  • Have basic to intermediate knowledge of the Laravel framework.
  • Be familiar with Eloquent and its syntax.
  • A sample working Laravel project to play with is optional but recommended. You can find Pusher’s Laravel tutorials here.

What is Eloquent?

The Eloquent ORM included with Laravel provides a beautiful, simple ActiveRecord implementation (closely resembling that of Ruby on Rails) for working with your database. Each Eloquent model creates a wrapper around the database table associated with it. This makes every instance of the Eloquent model representation of a row on the associated table.

Eloquent provides accessor methods and properties corresponding to each table cell on the row. It also provides methods for establishing relationships with other models and enables you to access these models through that relationship.

Updating an Eloquent model instance updates the database record it is mapped to, and deleting it also deletes the record.

Eloquent also provides a lot of features for working with your database records. We will be exploring a few of them in this article like:

Using Eloquent accessors

Accessors allow you to format a value retrieved from the database in a certain way. For example, your app issues tracking codes for orders that all have the prefix – ‘Acme_’. You may create a string field and prepend your company initials to all generated codes before storing them. But that would make your database break the normalization code.

To ensure that when you return the tracking codes to your users it follows the same pattern, you can define an accessor for accessing these codes like this:

1<?php
2
3    namespace App;
4
5    use Illuminate\Database\Eloquent\Model;
6
7    class Order extends Model
8    {
9        protected $fillable = [
10            'product_id',
11            'user_id',
12            'tracking_code',
13            'total_cost',
14        ];
15
16        public function getTrackingCodeAttribute($value)
17        {
18            return "Acme_" . $value;
19        }
20
21        // [...]
22
23    }

Above, we defined an accessor for your tracking_codes. The format needed to do this getFooAttribute where Foo is the name of the model property you wish to access.

The names should always be in camel case. To use it, you do the following:

1$order = \App\Order::find(1);
2    $order->tracking_code;

Your accessor will be called anytime you try to retrieve a tracking code and it will prepend ‘Acme_’ to the code retrieved.

You can also use accessors on computed values like this:

1# Example Model
2    // [...]
3
4    public function getTotalCostAttribute()
5    {
6        return $this->quantity * $this->unit_price;
7    }
8
9    // [...]

And you retrieve the computed property as follows:

1$order = \App\Order::find(1);
2    $order->total_cost;

Using Eloquent mutators

Mutators are like accessors but just work the opposite way. They are used to mutate the data stored to the database as opposed to how it is fetched. Mutators are called when you assign a value to the model property you defined the mutator on.

You can define accessors in the model like this:

1// [...]
2
3    public function setTrackingCodeAttribute($value)
4    {
5        return $this->attributes['tracking_code'] = str_replace('Acme_', '', $value);
6    }
7
8    // [...]

This mutator will be called anytime you assign a value to tracking_code, like this:

1$order = \App\Order::find(1);
2    $order->total_cost = 24451;

? Like the accessors, snake cased properties will be converted to camel case and wrapped around. For example, total_cost becomes TotalCost and the mutator will be setTotalCostAttribute.

Eager loading relationships

When you are accessing an Eloquent model, the relationships for that model are not loaded by default. Laravel lazy loads the relationships when you try to access them. This is great since it saves memory used in storing all that data. However, what if you know you will need all of the relationships? Well, this is where eager loading comes in.

Another advantage of eager loading is that it avoids the N+1 problem. Consider this code sample:

1$orders = \App\Order::all();
2
3    foreach($orders as $order) {
4        echo $order->user->first_name;
5    }

In the code above, we are fetching all the orders from the data store, and then in the loop, we are trying to access the first_name property in the user relationship. While this code will work we will have a problem. Because the relationships are lazy-loaded, every time the loop runs, a new query will be fired to get the user relationship.

What happens when you have a hundred orders? Your application will perform 101 queries to get the names of all the users behind orders. The first to get all the orders and the next to get the users for each of the orders. You can see that your operation has a O(n) time complexity.

We reduce the time taken to get all the orders and users to O(1) by eager loading all the users like this:

1$orders = \App\Order::with('user')->get();
2
3    foreach($orders as $order){
4        echo $order->user->first_name;
5    }

The above code will execute two database queries. One to retrieve all the orders and the second to retrieve all the users tied to the orders. If you have 1000 order records, your application still executes just two queries.

Since we need only the user’s first name, we can choose to eager load it like this:

1$orders = \App\Order::with('user:id,name')->get();
2
3    foreach($orders as $order){
4        echo $order->user->first_name;
5    }

⚠️ A little caveat in eager loading properties of a relationship is that you have to include the id column.

Assuming your application has a Coupon model and you would like to retrieve a user’s coupons along with the user information, you do the following:

1$orders = \App\Order::with('user.coupons')->get();
2
3    foreach($orders as $order){
4        echo $order->user->coupons;
5    }

We can also eager load multiple relationships like this:

1$orders = \App\Order::with(['user', 'product'])->get();
2
3    foreach($orders as $order){
4        echo $order->user->first_name;
5    }

Eager loading is very useful if you have an API and need to return data through your API endpoint. It will present you with the entire dataset you would need, saving you from making multiple API calls to get data.

Collection methods

Whenever you run a model query to return many results, you receive an Eloquent Collection object. This collection object extends the Laravel base collection. This means it inherits dozens of methods used to fluently work with the underlying array of Eloquent models.

? Eloquent collections are immutable so every operation you perform on them returns a new Eloquent collection instance. For pluck, keys, zip, collapse, flatten and flip, a base collection instance is returned. Always assign the output to a variable and use that variable.

For example, assume you want to retrieve only orders that have been delivered and assign a delivery status text to them, you can do the following:

1$orders = \App\Order::all()->reject(function ($order) {
2        return $order->is_delivered == false;
3    })
4    ->map(function ($order) {
5        $order->status = "Fulfilled";
6        return $order;
7    });

The $orders variable will hold only orders that have been marked as delivered.

Say you want to return both delivered and undelivered orders, and group them like delivered = [], undelivered = [], you can do something like:

1$orders = \App\Order::all()->map(function ($order) {
2        $order->status = $order->is_delivered ? "delivered" : "undelivered";
3
4        return $order;
5    })->mapToGroups(function ($item, $key) {
6        return [$item['status'] => $item];
7    });

If you want the product and price to be a key-value pair, you can do something like this:

1$products = \App\Product::all()->mapWithKeys(function ($item) {
2        return [$item['name'] => $item['price']];
3    });
4
5    foreach($products as $key => $value) {
6        echo "{$key}: {$value}";
7    }

What if you have a reusable div container that holds only four items, you can return your data in chunks of four like this:

1$products = \App\Product::all()->chunk(4);

And in your blade view template, you do this:

1[...]
2
3    @foreach ($products as $chunk)
4        <div class="item-container">
5            @foreach ($chunk as $product)
6                <div class="col-xs-3">{{ $product->name }}: {{ $product->price }}</div>
7            @endforeach
8        </div>
9    @endforeach
10
11    [...]

What if you want to return all orders delivered on a particular day, you can do something like this:

1public function orderOnDate(Request $request){
2        $orders = \App\Order::all()->filter(function ($order) use ($request) {
3            return $order->delivered_at == $request->date;
4        });
5    }

This will filter out all the orders that were not done on that particular day.

? Collection is easily one of the most robust features of Laravel and you should look into it. You can learn more about collections here and see more useful methods for your application.

Laravel Eloquent events

Eloquent models fire several events which allow you to hook into different parts of a model’s lifecycle. The events are: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, and restored. Every time each event occurs, you can execute code or perform an action.

To track and react to events, you can use an observer class for the model you want to capture events on. Observers classes have method names which reflect the Eloquent events you wish to listen for. Each of these methods receives the model as their only argument.

To explore how observers work, let’s write an observer class for a make-believe User model. We will check if a user has an invite when creating the account and tag the referrer. We will also check if there is a pending order before deleting the user’s account.

First, create a directory app/observers and in there make a new PHP file UserObserver.php. In the file paste the following code:

1<?php
2
3    namespace App\Observers;
4
5    use App\User;
6    use App\Invites;
7    use Illuminate\Support\Facades\Mail;
8
9    class UserObserver
10    {
11        public function creating(User $user)
12        {
13            $invite = Invites::where('email',$user->email)->first();
14
15            if ($invite) {
16                $user->referrer = $invite->user_id;
17            }
18        }
19
20        public function created(User $user)
21        {
22            Mail::raw("Some custom message here", function ($message){
23                $message->to($user->email)->subject("Please confirm your account");
24            });
25        }
26
27        public function deleting(User $user)
28        {
29            $user->orders->map(function($order) {
30                if ($order->is_delivered == false) {
31                    abort(403,'You cannot delete your account yet');
32                }
33            });
34        }
35
36        public function deleted(User $user)
37        {
38            Mail::raw("Some custom message here", function ($message){
39                $message->to($user->email)->subject("We hate to see you go");
40            });
41        }
42    }

In the observer above, we have defined operations we want to run when certain events are fired. So now we can create our controller and never have to worry about making the controller unnecessarily burdened with the logic that does not concern it.

Here is what the controller will look like:

1<?php
2
3    use App\User;
4
5    class UserController extends Controller
6
7        // [...]
8
9        public function store(Request $request)
10        {
11            User::create($request->all());
12
13            return back();
14        }
15
16        public function delete(User $user)
17        {
18            $user->delete();
19
20            return back();
21        }
22
23        // [...]
24
25    }

As seen above, the controller remains small and manageable.

To register our UserObserver, use the observe method on the User model. You may register observers in the boot method of one of your service providers.

In this example, we are registering the observer in our AppServiceProvider:

1<?php
2
3    namespace App\Providers;
4
5    use App\User;
6    use App\Observers\UserObserver;
7    use Illuminate\Support\ServiceProvider;
8
9    class AppServiceProvider extends ServiceProvider
10    {
11        public function boot()
12        {
13            User::observe(UserObserver::class);
14        }
15
16        // [...]
17    }

And that’s it.

Eloquent query scopes

Scopes allow you to add constraints to all queries for a given model. Laravel has two types of scopes: global and local.

Global scopes are applied every time you call the model by default. Local scopes allow you to define common sets of constraints that you may re-use throughout your application. You use local scopes only when you need to and will not be applied every time you call the model.

An example of global scope is softDeletes, which filters your queries to remove records you had previously marked as deleted. A place you may wish to use a global scope is with a feature like tickets where you only want to retrieve tickets that have not been closed. Let’s examine how to define this global scope.

You can either write the scope as a standalone class or defined it as a dynamic scope in the boot method of your model.

Eloquent query scopes: standalone global scope

Here’s how we can implement a standalone global scope. We could create a directory app/Scopes in our Laravel application and in that directory create a scope class as seen below:

1<?php
2
3    namespace App\Scopes;
4
5    use Illuminate\Database\Eloquent\{Scope, Model, Builder};
6
7    class ClosedScope implements Scope
8    {
9        public function apply(Builder $builder, Model $model)
10        {
11            $builder->where('is_closed', '=', false);
12        }
13    }

To apply it to your make-believe Ticket model, you could do something like this:

1<?php
2
3    namespace App;
4
5    use use App\Scopes\ClosedScope;
6    use Illuminate\Database\Eloquent\Builder;
7
8    class Ticket extends Model
9    {
10        protected static function boot()
11        {
12            parent::boot();
13
14            static::addGlobalScope(new ClosedScope);
15        }
16
17        // [...]
18
19    }

Eloquent query scopes: dynamic global scopes

To create a dynamic global scope you could create just write the logic directly in the boot method of your example Ticket model as seen below:

1<?php
2
3    namespace App;
4
5    use Illuminate\Database\Eloquent\Model;
6    use Illuminate\Database\Eloquent\Builder;
7
8    class Ticket extends Model
9    {
10        protected static function boot()
11        {
12            parent::boot();
13
14            static::addGlobalScope('closed', function (Builder $builder) {
15                $builder->where('is_closed', '=', false);
16            });
17        }
18
19        // [...]
20
21    }

Both scope definitions will achieve the same result. They would return only tickets that have not been closed. To use them in your controller, you do the following:

1# Shows all tickets that have not been closed
2    $tickets = \App\Ticket::all(); 
3
4    # Without Standalone ClosedScope
5    $tickets = \App\Ticket::withoutGlobalScope(\App\Scopes\ClosedScope::class)->get(); 
6
7    # Without dynamic scope defined on "is_closed"
8    $tickets = \App\Ticket::withoutGlobalScope('closed')->get();
9
10    # Without all scopes
11    $tickets = \App\Ticket::withoutGlobalScopes()->get();
12
13    # Without multiple specific scopes
14    $tickets = \App\Ticket::withoutGlobalScopes([FirstScope::class, SecondScope::class])->get();

That’s it for global scopes.

If you want to define the same constraint to your queries as local scope, you can do the following inside the model:

1<?php
2
3    namespace App;
4
5    use Illuminate\Database\Eloquent\Model;
6    use Illuminate\Database\Eloquent\Builder;
7
8    class Ticket extends Model
9    {
10        public function scopeOpen($query)
11        {
12            return $query->where('is_closed', false);
13        }
14
15        public function scopeIsClosed($query, $state)
16        {
17            return $query->where('is_closed', $state);
18        }
19
20        // [...]
21
22    }

Above, we defined a local scope on the ticket model. The scopeOpen will only return tickets that are still open while the scopeIsClosed does the opposite. You can, however, pass an argument to scopeIsClosed to return tickets that are either open or closed.

To use it, you do the following:

1# Get only open tickets
2    $tickets = \App\Ticket::open()->get();
3
4    # Get only open tickets
5    $tickets = \App\Ticket::isClosed('false')->get();
6
7    # Get only closed tickets
8    $tickets = \App\Ticket::isClosed('true')->get();
9
10    # Get all tickets
11    $tickets = \App\Ticket::all();

Which scope you choose to define depends on the needs of your application. You can learn more here.

Conclusion

In this tutorial, we have considered how useful Laravel Eloquent is and how you can utilize to improve your code. They may seem confusing the first time you encounter them but once you put them into practice, it can save you a good amount of time and make your code more readable.

You can also check out our latest tutorials for Laravel!!