Tips, Tricks, and Good Practices with Laravel's Eloquent
This is a talk I gave at TrianglePHP on Aug 16, 2018. We'll learn how Eloquent functions on the basic levels and continue through some more well-known methods and some possibly lesser-known ones. Then we'll finish with some more advanced ideas and techniques.
Tips, Tricks, and Good Practices
with
Laravel's Eloquent
Presented by Chris Gmyr
What is Laravel?
Laravel is a modern PHP framework that helps you create applications using simple, expressive syntax as well as offers powerful features like an ORM, routing, queues, events, notifications, simple authentication...
...and so much more!
What is Eloquent?
The Eloquent ORM included with Laravel provides a beautiful, simple ActiveRecord implementation for working with your database. Each database table has a corresponding "Model" which is used to interact with that table. Models allow you to query for data in your tables, as well as insert new records into the table.
The Basics
A Model
class Post extends Model
{
// look Ma, no code!
}
- id
- title
- created_at
- updated_at
$post = Post::find(1);
Artisan Goodies
php artisan make:model Product
Artisan Goodies
php artisan make:model Product -mcr
-m
will create a migration file -c
will create a controller -r
will indicate that controller should be resourceful
Cruddy
Creating
$user = new User();
$user->first_name = 'Chris';
$user->email = 'cmgmyr@gmail.com';
$user->save();
Creating
$user = User::create([
'first_name' => 'Chris',
'email' => 'cmgmyr@gmail.com',
]);
Note: $fillable
/$guarded
properties
Updating
$user = User::find(1);
$user->email = 'me@chrisgmyr.com';
$user->save();
Updating
$user = User::find(1);
$user->update([
'email' => 'me@chrisgmyr.com',
]);
Note: $fillable
/$guarded
properties
Updating
$user = User::find(1);
$user->fill([
'email' => 'me@chrisgmyr.com',
]);
$user->save();
Note: $fillable
/$guarded
properties
Deleting
$user = User::find(1);
$user->delete();
Deleting
User::destroy(1);
User::destroy([1, 2, 3]);
User::destroy(1, 2, 3);
"or" helper methods
User::findOrFail(1);
$user->saveOrFail(); // same as save(), but uses transaction
User::firstOrCreate([ /* attributes */]);
User::updateOrInsert([/* attributes to search */], [/* attributes to update */]);
Querying
Querying
$users = User::get(); // User::all()
$user = User::where('id', 1)->first();
$user = User::find(1);
$user = User::findOrFail(1);
$users = User::find([1, 2, 3]);
$users = User::whereIn('id', [1, 2, 3])->get();
$users = User::where('is_admin', true)
->where('id', '!=', Auth::id())
->take(10)
->orderBy('last_name', 'ASC')
->get();
Chunking
User::chunk(50, function ($users) {
foreach ($users as $user) {
//
}
});
Collections
For Eloquent methods like
all()
andget()
which retrieve multiple results, an instance ofIlluminate\Database\Eloquent\Collection
will be returned.
$admins = $users->filter(function ($user) {
return $user->is_admin;
});
Raw query methods
Product::whereRaw('price > IF(state = "NC", ?, 100)', [200])
->get();
Post::groupBy('category_id')
->havingRaw('COUNT(*) > 1')
->get();
Customer::where('created_at', '>', '2016-01-01')
->orderByRaw('(updated_at - created_at) desc')
->get();
Relationships
Relationships
hasOne() // User has one Address
belongsTo() // Address belongs to User
hasMany() // Post has many Comment
belongsToMany() // Role belongs to many User
hasManyThrough() // Country has many Post through User
// Use single table
morphTo() // Comment can be on Post, Video, Album
morphMany() // Post has many Comment
// Use pivot table
morphToMany() // Post has many Tag
morphedByMany() // Tag has many Post
Relationships
class Video extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
}
$video = Video::find(1);
foreach ($video->comments as $comment) {
// $comment->body
}
Relationships
class Video extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
}
$video = Video::find(1);
foreach ($video->comments()->where('approved', true)->get() as $comment) {
// $comment->body
}
Default conditions and ordering
class Video extends Model
{
public function comments()
{
return $this->hasMany(Comment::class)
->where('approved', true)
->latest();
}
}
Default conditions and ordering
class Video extends Model
{
public function comments()
{
return $this->hasMany(Comment::class);
}
public function publicComments()
{
return $this->comments()
->where('approved', true)
->latest();
}
}
Default Models
Default models can be used with belongsTo()
, hasOne()
, and morphOne()
relationships.
Default Models
{{ $post->author->name }} // error if author not found
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class);
}
}
Default Models
{{ $post->author->name ?? '' }} // meh
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class);
}
}
Default Models
{{ $post->author->name }} // better!
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class)->withDefault();
}
}
Default Models
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
}
Events
The
retrieved
event will fire when an existing model is retrieved from the database. When a new model is saved for the first time, thecreating
andcreated
events will fire. If a model already existed in the database and thesave()
method is called, theupdating
/updated
events will fire. However, in both cases, thesaving
/saved
events will fire.
https://laravel.com/docs/5.6/eloquent#events
Events
class User extends Model
{
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
Observers
php artisan make:observer UserObserver --model=User
class ModelObserverServiceProvider extends ServiceProvider
{
public function boot()
{
User::observe(UserObserver::class);
}
}
Observers
class UserObserver
{
public function created(User $user)
{
}
public function updated(User $user)
{
}
public function deleted(User $user)
{
}
}
boot()
method
class Post extends Model
{
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->uuid = (string) Uuid::generate();
});
}
}
Bootable Trait
class Post extends Model
{
use HasUuid;
}
trait HasUuid
{
public static function bootHasUuid()
{
self::creating(function ($model) {
$model->uuid = (string) Uuid::generate();
});
}
// more uuid related methods
}
Helper Methods
Increments and Decrements
$post = Post::find(1);
$post->stars++;
$post->save();
$post->stars--;
$post->save();
Increments and Decrements
$post = Post::find(1);
$post->increment('stars'); // add 1
$post->increment('stars', 15); // add 15
$post->decrement('stars'); // subtract 1
$post->decrement('stars', 15); // subtract 15
Aggregates
$count = Product::where('active', 1)->count();
$min = Product::where('active', 1)->min('price');
$max = Product::where('active', 1)->max('price');
$avg = Product::where('active', 1)->avg('price');
$sum = Product::where('active', 1)->sum('price');
Check if Records Exist
Instead of count()
, you could use...
User::where('username', 'cmgmyr')->exists();
User::where('username', 'cmgmyr')->doesntExist();
Model State
$model->isDirty($attributes = null);
$model->isClean($attributes = null);
$model->wasChanged($attributes = null);
$model->hasChanges($changes, $attributes = null);
$model->getDirty();
$model->getChanges();
//Indicates if the model exists.
$model->exists;
//Indicates if the model was inserted during the current request lifecycle.
$model->wasRecentlyCreated;
"Magic" where()
$users = User::where('approved', 1)->get();
$users = User::whereApproved(1)->get();
$user = User::where('username', 'cmgmyr')->get();
$user = User::whereUsername('cmgmyr')->get();
$admins = User::where('is_admin', true)->get();
$admins = User::whereIsAdmin(true)->get();
Super "Magic" where()
User::whereTypeAndStatus('admin', 'active')->get();
User::whereTypeOrStatus('admin', 'active')->get();
twitter.com/themsaid/status/102973154494295..
Dates
User::whereDate('created_at', date('Y-m-d'));
User::whereDay('created_at', date('d'));
User::whereMonth('created_at', date('m'));
User::whereYear('created_at', date('Y'));
when()
to eliminate conditionals
$query = Author::query();
if (request('filter_by') == 'likes') {
$query->where('likes', '>', request('likes_amount', 0));
}
if (request('filter_by') == 'date') {
$query->orderBy('created_at', request('ordering_rule', 'desc'));
}
when()
to eliminate conditionals
$query = Author::query();
$query->when(request('filter_by') == 'likes', function ($q) {
return $q->where('likes', '>', request('likes_amount', 0));
});
$query->when(request('filter_by') == 'date', function ($q) {
return $q->orderBy('created_at', request('ordering_rule', 'desc'));
});
replicate()
a Model
$invoice = Invoice::find(1);
$newInvoice = $invoice->replicate();
$newInvoice->save();
Pagination
// 1, 2, 3, 4, 5...
$users = User::where('active', true)->paginate(15);
// Previous/Next
$users = User::where('active', true)->simplePaginate(15);
// In Blade
{{ $users->links() }}
Pagination to Json
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"first_page_url": "https://my.app?page=1",
"last_page_url": "https://my.app?page=4",
"next_page_url": "https://my.app?page=2",
"prev_page_url": null,
"path": "https://my.app",
"from": 1,
"to": 15,
"data":[
{
// Result Object
},
{
// Result Object
}
]
}
Model Properties
protected $table = 'users';
protected $fillable = ['first_name', 'email', 'password']; // create()/update()
protected $dates = ['created', 'deleted_at']; // Carbon
protected $appends = ['full_name', 'company']; // additional JSON values
protected $casts = ['is_admin' => 'boolean', 'options' => 'array'];
protected $primaryKey = 'uuid';
public $incrementing = false;
protected $perPage = 25;
const CREATED_AT = 'created';
const UPDATED_AT = 'updated';
public $timestamps = false;
...and more!
Overriding updated_at
$product = Product::find(1);
$product->updated_at = '2020-01-01 10:00:00';
$product->save(['timestamps' => false]);
Primary Key Methods
$video = Video::find(1);
$video->getKeyName(); // 'id'
$video->getKeyType(); // 'int'
$video->getKey(); // 1
Accessors/Mutators
class User extends Model
{
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
public function setLastNameAttribute($value)
{
$this->attributes['last_name'] = strtolower($value);
}
}
Accessors/Mutators
class User extends Model
{
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
public function getLastNameAttribute($value)
{
return ucfirst($value);
}
public function getEmailAttribute($value)
{
return new Email($value);
}
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
}
Accessors/Mutators
$user = User::create([
'first_name' => 'Chris', // chris
'last_name' => 'Gmyr', // gmyr
'email' => 'cmgmyr@gmail.com',
]);
$user->first_name; // Chris
$user->last_name; // Gmyr
$user->email; // instance of Email
$user->full_name; // 'Chris Gmyr'
To Array/Json
$user = User::find(1);
return $user->toArray();
return $user->toJson();
You can also return $user
from a controller method and it will automatically return JSON.
Appending Values to JSON
class User extends Model
{
protected $appends = ['full_name']; // adds to toArray()
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
}
// or...
return $user->append('full_name')->toArray();
return $user->setAppends(['full_name'])->toArray();
Local Scopes
$posts = Post::whereNotNull('published_at')
->where('published_at', '<=', Carbon::now())
->latest('published_at')
->get();
Local Scopes
class Post extends Model
{
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', Carbon::now())
->latest('published_at');
}
}
Local Scopes
$posts = Post::published()->get();
Single Table Inheritance
Single Table Inheritance
$admins = User::where('is_admin', true)->get();
$customers = User::where('is_admin', false)->get();
Single Table Inheritance
class User extends Model
{
public function scopeAdmin($query)
{
return $query->where('is_admin', true);
}
public function scopeCustomer($query)
{
return $query->where('is_admin', false);
}
}
$admins = User::admin()->get();
$customers = User::customer()->get();
Single Table Inheritance
class Admin extends User
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(function ($query) {
$query->where('is_admin', true);
});
}
}
Single Table Inheritance
class Customer extends User
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(function ($query) {
$query->where('is_admin', false);
});
}
}
Single Table Inheritance
$admins = Admin::get();
$customers = Customer::get();
Single Table Inheritance
Read more:
Default Model Data
Default Model Data
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->string('role')->default('user'); // moderator, admin, etc
$table->rememberToken();
$table->timestamps();
});
Default Model Data
class User extends Model
{
protected $fillable = [
'name', 'email', 'password', 'role'
];
}
Default Model Data
$user = new User();
$user->name = 'Chris';
$user->email = 'cmgmyr@gmail.com';
$user->password = Hash::make('p@ssw0rd');
// $user->role is currently NULL
$user->save();
$user->role; // 'user'
Default Model Data
Remove ->default('user')
;
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->string('role'); // moderator, admin, etc
$table->rememberToken();
$table->timestamps();
});
Default Model Data
Set $attributes
class User extends Model
{
protected $fillable = [
'name', 'email', 'password', 'role'
];
protected $attributes = [
'role' => 'user',
];
}
Default Model Data
$user = new User();
$user->name = 'Chris';
$user->email = 'cmgmyr@gmail.com';
$user->password = Hash::make('p@ssw0rd');
// $user->role is currently 'user'!
$user->save();
$user->role; // 'user'
Default Model Data
$user = new User();
$user->name = 'Chris';
$user->email = 'cmgmyr@gmail.com';
$user->password = Hash::make('p@ssw0rd');
$user->role = 'admin'; // can override default
$user->save();
$user->role; // 'admin'
Default Models
Remember our previous example?
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
}
Default Models
We no longer need to provide a name
, use the User $attributes
property!
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class)->withDefault();
}
}
Default Model Data
class User extends Model
{
protected $fillable = [
'name', 'email', 'password', 'role'
];
protected $attributes = [
'name' => 'Guest Author',
'role' => 'user',
];
}
Default Model Data
Watch Colin DeCarlo's - Keeping Eloquent Eloquent from Laracon US 2016
streamacon.com/video/laracon-us-2016/colin-..
Sub-Queries
$customers = Customer::with('company')
->orderByName()
->paginate();
Get latest interactions?
<p>{{ $customer
->interactions()
->latest()
->first()
->created_at
->diffForHumans() }}</p>
Sub-Queries
public function scopeWithLastInteractionDate($query)
{
$subQuery = \DB::table('interactions')
->select('created_at')
->whereRaw('customer_id = customers.id')
->latest()
->limit(1);
return $query->select('customers.*')->selectSub($subQuery, 'last_interaction_date');
}
$customers = Customer::with('company')
->withLastInteractionDate()
->orderByName()
->paginate();
<p>{{ $customer->last_interaction_date->diffForHumans() }}</p>
Sub-Queries
Jonathan Reinink's Laracon 2018 Online Talk - Advanced Querying with Eloquent
github.com/reinink/laracon2018