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.
In this guide, 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 Eloquent and how it can make your development process even easier.
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.
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.
- Using Eloquent mutators.
- Eager loading relationships.
- Using collection with Eloquent results.
- Eloquent events.
- Eloquent query scopes.
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:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected $fillable = [
'product_id',
'user_id',
'tracking_code',
'total_cost',
];
public function getTrackingCodeAttribute($value)
{
return "Acme_" . $value;
}
// [...]
}
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:
$order = \App\Order::find(1);
$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:
# Example Model
// [...]
public function getTotalCostAttribute()
{
return $this->quantity * $this->unit_price;
}
// [...]
And you retrieve the computed property as follows:
$order = \App\Order::find(1);
$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:
// [...]
public function setTrackingCodeAttribute($value)
{
return $this->attributes['tracking_code'] = str_replace('Acme_', '', $value);
}
// [...]
This mutator will be called anytime you assign a value to tracking_code
, like this:
$order = \App\Order::find(1);
$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:
$orders = \App\Order::all();
foreach($orders as $order) {
echo $order->user->first_name;
}
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:
$orders = \App\Order::with('user')->get();
foreach($orders as $order){
echo $order->user->first_name;
}
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:
$orders = \App\Order::with('user:id,name')->get();
foreach($orders as $order){
echo $order->user->first_name;
}
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:
$orders = \App\Order::with('user.coupons')->get();
foreach($orders as $order){
echo $order->user->coupons;
}
We can also eager to load multiple relationships like this:
$orders = \App\Order::with(['user', 'product'])->get();
foreach($orders as $order){
echo $order->user->first_name;
}
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 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:
$orders = \App\Order::all()->reject(function ($order) {
return $order->is_delivered == false;
})
->map(function ($order) {
$order->status = "Fulfilled";
return $order;
});
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:
$orders = \App\Order::all()->map(function ($order) {
$order->status = $order->is_delivered ? "delivered" : "undelivered";
return $order;
})->mapToGroups(function ($item, $key) {
return [$item['status'] => $item];
});
If you want the product and price to be a key-value
pair, you can do something like this:
$products = \App\Product::all()->mapWithKeys(function ($item) {
return [$item['name'] => $item['price']];
});
foreach($products as $key => $value) {
echo "{$key}: {$value}";
}
What if you have a reusable div
container that holds only four items, you can return your data in chunks of four like this:
$products = \App\Product::all()->chunk(4);
And in your blade view template, you do this:
[...]
@foreach ($products as $chunk)
<div class="item-container">
@foreach ($chunk as $product)
<div class="col-xs-3">{{ $product->name }}: {{ $product->price }}</div>
@endforeach
</div>
@endforeach
[...]
What if you want to return all orders delivered on a particular day, you can do something like this:
public function orderOnDate(Request $request){
$orders = \App\Order::all()->filter(function ($order) use ($request) {
return $order->delivered_at == $request->date;
});
}
This will filter out all the orders that were not done on that particular day.
The 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.
Eloquent events
Eloquent models fire several events that 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 that 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:
<?php
namespace App\Observers;
use App\User;
use App\Invites;
use Illuminate\Support\Facades\Mail;
class UserObserver
{
public function creating(User $user)
{
$invite = Invites::where('email',$user->email)->first();
if ($invite) {
$user->referrer = $invite->user_id;
}
}
public function created(User $user)
{
Mail::raw("Some custom message here", function ($message){
$message->to($user->email)->subject("Please confirm your account");
});
}
public function deleting(User $user)
{
$user->orders->map(function($order) {
if ($order->is_delivered == false) {
abort(403,'You cannot delete your account yet');
}
});
}
public function deleted(User $user)
{
Mail::raw("Some custom message here", function ($message){
$message->to($user->email)->subject("We hate to see you go");
});
}
}
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:
<?php
use App\User;
class UserController extends Controller
// [...]
public function store(Request $request)
{
User::create($request->all());
return back();
}
public function delete(User $user)
{
$user->delete();
return back();
}
// [...]
}
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
:
<?php
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
User::observe(UserObserver::class);
}
// [...]
}
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 a 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:
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\{Scope, Model, Builder};
class ClosedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('is_closed', '=', false);
}
}
To apply it to your make-believe Ticket
model, you could do something like this:
<?php
namespace App;
use use App\Scopes\ClosedScope;
use Illuminate\Database\Eloquent\Builder;
class Ticket extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new ClosedScope);
}
// [...]
}
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:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class Ticket extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope('closed', function (Builder $builder) {
$builder->where('is_closed', '=', false);
});
}
// [...]
}
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:
# Shows all tickets that have not been closed
$tickets = \App\Ticket::all();
# Without Standalone ClosedScope
$tickets = \App\Ticket::withoutGlobalScope(\App\Scopes\ClosedScope::class)->get();
# Without dynamic scope defined on "is_closed"
$tickets = \App\Ticket::withoutGlobalScope('closed')->get();
# Without all scopes
$tickets = \App\Ticket::withoutGlobalScopes()->get();
# Without multiple specific scopes
$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:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class Ticket extends Model
{
public function scopeOpen($query)
{
return $query->where('is_closed', false);
}
public function scopeIsClosed($query, $state)
{
return $query->where('is_closed', $state);
}
// [...]
}
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:
# Get only open tickets
$tickets = \App\Ticket::open()->get();
# Get only open tickets
$tickets = \App\Ticket::isClosed('false')->get();
# Get only closed tickets
$tickets = \App\Ticket::isClosed('true')->get();
# Get all tickets
$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 guide, 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.