Tips, Tricks, and Good Practices with Laravel's Eloquent

Photo by Diz Play on Unsplash

Tips, Tricks, and Good Practices with Laravel's Eloquent

·

10 min read

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.

laravel.com/docs/5.6/eloquent


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() and get() which retrieve multiple results, an instance of Illuminate\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, the creating and created events will fire. If a model already existed in the database and the save() method is called, the updating / updated events will fire. However, in both cases, the saving / 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


Resources


Thank you!

Please say "hi"

twitter.com/cmgmyr

github.com/cmgmyr

chrisgmyr.com