$30 off During Our Annual Pro Sale. View Details »

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

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

Chris Gmyr

August 16, 2018
Tweet

More Decks by Chris Gmyr

Other Decks in Programming

Transcript

  1. TIPS, TRICKS, AND GOOD
    PRACTICES
    WITH
    LARAVEL'S ELOQUENT
    PRESENTED BY CHRIS GMYR

    View Slide

  2. View Slide

  3. 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!
    3 — Twitter/GitHub: @cmgmyr

    View Slide

  4. 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.
    https://laravel.com/docs/5.6/eloquent
    4 — Twitter/GitHub: @cmgmyr

    View Slide

  5. THE BASICS

    View Slide

  6. A MODEL
    class Post extends Model
    {
    // look Ma, no code!
    }
    - id
    - title
    - created_at
    - updated_at
    $post = Post::find(1);
    6 — Twitter/GitHub: @cmgmyr

    View Slide

  7. ARTISAN GOODIES
    php artisan make:model Product
    7 — Twitter/GitHub: @cmgmyr

    View Slide

  8. 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
    8 — Twitter/GitHub: @cmgmyr

    View Slide

  9. CRUDDY

    View Slide

  10. CREATING
    $user = new User();
    $user->first_name = 'Chris';
    $user->email = '[email protected]';
    $user->save();
    10 — Twitter/GitHub: @cmgmyr

    View Slide

  11. CREATING
    $user = User::create([
    'first_name' => 'Chris',
    'email' => '[email protected]',
    ]);
    Note: $fillable/$guarded properties
    11 — Twitter/GitHub: @cmgmyr

    View Slide

  12. UPDATING
    $user = User::find(1);
    $user->email = '[email protected]';
    $user->save();
    12 — Twitter/GitHub: @cmgmyr

    View Slide

  13. UPDATING
    $user = User::find(1);
    $user->update([
    'email' => '[email protected]',
    ]);
    Note: $fillable/$guarded properties
    13 — Twitter/GitHub: @cmgmyr

    View Slide

  14. UPDATING
    $user = User::find(1);
    $user->fill([
    'email' => '[email protected]',
    ]);
    $user->save();
    Note: $fillable/$guarded properties
    14 — Twitter/GitHub: @cmgmyr

    View Slide

  15. DELETING
    $user = User::find(1);
    $user->delete();
    15 — Twitter/GitHub: @cmgmyr

    View Slide

  16. DELETING
    User::destroy(1);
    User::destroy([1, 2, 3]);
    User::destroy(1, 2, 3);
    16 — Twitter/GitHub: @cmgmyr

    View Slide

  17. "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 */]);
    17 — Twitter/GitHub: @cmgmyr

    View Slide

  18. QUERYING

    View Slide

  19. 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();
    19 — Twitter/GitHub: @cmgmyr

    View Slide

  20. CHUNKING
    User::chunk(50, function ($users) {
    foreach ($users as $user) {
    //
    }
    });
    20 — Twitter/GitHub: @cmgmyr

    View Slide

  21. 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;
    });
    21 — Twitter/GitHub: @cmgmyr

    View Slide

  22. 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();
    22 — Twitter/GitHub: @cmgmyr

    View Slide

  23. RELATIONSHIPS

    View Slide

  24. 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
    24 — Twitter/GitHub: @cmgmyr

    View Slide

  25. RELATIONSHIPS
    class Video extends Model
    {
    public function comments()
    {
    return $this->hasMany(Comment::class);
    }
    }
    $video = Video::find(1);
    foreach ($video->comments as $comment) {
    // $comment->body
    }
    25 — Twitter/GitHub: @cmgmyr

    View Slide

  26. 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
    }
    26 — Twitter/GitHub: @cmgmyr

    View Slide

  27. DEFAULT CONDITIONS AND ORDERING
    class Video extends Model
    {
    public function comments()
    {
    return $this->hasMany(Comment::class)
    ->where('approved', true)
    ->latest();
    }
    }
    27 — Twitter/GitHub: @cmgmyr

    View Slide

  28. 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();
    }
    }
    28 — Twitter/GitHub: @cmgmyr

    View Slide

  29. DEFAULT MODELS
    Default models can be used with belongsTo(),
    hasOne(), and morphOne() relationships.
    29 — Twitter/GitHub: @cmgmyr

    View Slide

  30. DEFAULT MODELS
    {{ $post->author->name }} // error if author not found
    class Post extends Model
    {
    public function author()
    {
    return $this->belongsTo(User::class);
    }
    }
    30 — Twitter/GitHub: @cmgmyr

    View Slide

  31. DEFAULT MODELS
    {{ $post->author->name ?? '' }} // meh
    class Post extends Model
    {
    public function author()
    {
    return $this->belongsTo(User::class);
    }
    }
    31 — Twitter/GitHub: @cmgmyr

    View Slide

  32. DEFAULT MODELS
    {{ $post->author->name }} // better!
    class Post extends Model
    {
    public function author()
    {
    return $this->belongsTo(User::class)->withDefault();
    }
    }
    32 — Twitter/GitHub: @cmgmyr

    View Slide

  33. DEFAULT MODELS
    class Post extends Model
    {
    public function author()
    {
    return $this->belongsTo(User::class)->withDefault([
    'name' => 'Guest Author',
    ]);
    }
    }
    33 — Twitter/GitHub: @cmgmyr

    View Slide

  34. 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
    34 — Twitter/GitHub: @cmgmyr

    View Slide

  35. EVENTS
    class User extends Model
    {
    protected $dispatchesEvents = [
    'saved' => UserSaved::class,
    'deleted' => UserDeleted::class,
    ];
    }
    35 — Twitter/GitHub: @cmgmyr

    View Slide

  36. OBSERVERS
    php artisan make:observer UserObserver --
    model=User
    class ModelObserverServiceProvider extends ServiceProvider
    {
    public function boot()
    {
    User::observe(UserObserver::class);
    }
    }
    36 — Twitter/GitHub: @cmgmyr

    View Slide

  37. OBSERVERS
    class UserObserver
    {
    public function created(User $user)
    {
    }
    public function updated(User $user)
    {
    }
    public function deleted(User $user)
    {
    }
    }
    37 — Twitter/GitHub: @cmgmyr

    View Slide

  38. boot() METHOD
    class Post extends Model
    {
    public static function boot()
    {
    parent::boot();
    self::creating(function ($model) {
    $model->uuid = (string) Uuid::generate();
    });
    }
    }
    38 — Twitter/GitHub: @cmgmyr

    View Slide

  39. 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
    }
    39 — Twitter/GitHub: @cmgmyr

    View Slide

  40. HELPER
    METHODS

    View Slide

  41. INCREMENTS AND DECREMENTS
    $post = Post::find(1);
    $post->stars++;
    $post->save();
    $post->stars--;
    $post->save();
    41 — Twitter/GitHub: @cmgmyr

    View Slide

  42. 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
    42 — Twitter/GitHub: @cmgmyr

    View Slide

  43. 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');
    43 — Twitter/GitHub: @cmgmyr

    View Slide

  44. CHECK IF RECORDS EXIST
    Instead of count(), you could use...
    User::where('username', 'cmgmyr')->exists();
    User::where('username', 'cmgmyr')->doesntExist();
    44 — Twitter/GitHub: @cmgmyr

    View Slide

  45. 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;
    45 — Twitter/GitHub: @cmgmyr

    View Slide

  46. "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();
    46 — Twitter/GitHub: @cmgmyr

    View Slide

  47. SUPER "MAGIC" WHERE()
    User::whereTypeAndStatus('admin', 'active')->get();
    User::whereTypeOrStatus('admin', 'active')->get();
    https://twitter.com/themsaid/status/
    1029731544942952448
    47 — Twitter/GitHub: @cmgmyr

    View Slide

  48. 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'));
    48 — Twitter/GitHub: @cmgmyr

    View Slide

  49. 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'));
    }
    49 — Twitter/GitHub: @cmgmyr

    View Slide

  50. 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'));
    });
    50 — Twitter/GitHub: @cmgmyr

    View Slide

  51. replicate() A MODEL
    $invoice = Invoice::find(1);
    $newInvoice = $invoice->replicate();
    $newInvoice->save();
    51 — Twitter/GitHub: @cmgmyr

    View Slide

  52. 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() }}
    52 — Twitter/GitHub: @cmgmyr

    View Slide

  53. 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
    }
    ]
    }
    53 — Twitter/GitHub: @cmgmyr

    View Slide

  54. 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!
    54 — Twitter/GitHub: @cmgmyr

    View Slide

  55. OVERRIDING updated_at
    $product = Product::find(1);
    $product->updated_at = '2020-01-01 10:00:00';
    $product->save(['timestamps' => false]);
    55 — Twitter/GitHub: @cmgmyr

    View Slide

  56. PRIMARY KEY METHODS
    $video = Video::find(1);
    $video->getKeyName(); // 'id'
    $video->getKeyType(); // 'int'
    $video->getKey(); // 1
    56 — Twitter/GitHub: @cmgmyr

    View Slide

  57. 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);
    }
    }
    57 — Twitter/GitHub: @cmgmyr

    View Slide

  58. 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}";
    }
    }
    58 — Twitter/GitHub: @cmgmyr

    View Slide

  59. ACCESSORS/MUTATORS
    $user = User::create([
    'first_name' => 'Chris', // chris
    'last_name' => 'Gmyr', // gmyr
    'email' => '[email protected]',
    ]);
    $user->first_name; // Chris
    $user->last_name; // Gmyr
    $user->email; // instance of Email
    $user->full_name; // 'Chris Gmyr'
    59 — Twitter/GitHub: @cmgmyr

    View Slide

  60. 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.
    60 — Twitter/GitHub: @cmgmyr

    View Slide

  61. 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();
    61 — Twitter/GitHub: @cmgmyr

    View Slide

  62. LOCAL SCOPES
    $posts = Post::whereNotNull('published_at')
    ->where('published_at', '<=', Carbon::now())
    ->latest('published_at')
    ->get();
    62 — Twitter/GitHub: @cmgmyr

    View Slide

  63. LOCAL SCOPES
    class Post extends Model
    {
    public function scopePublished($query)
    {
    return $query->whereNotNull('published_at')
    ->where('published_at', '<=', Carbon::now())
    ->latest('published_at');
    }
    }
    63 — Twitter/GitHub: @cmgmyr

    View Slide

  64. LOCAL SCOPES
    $posts = Post::published()->get();
    64 — Twitter/GitHub: @cmgmyr

    View Slide

  65. SINGLE TABLE
    INHERITANCE

    View Slide

  66. SINGLE TABLE INHERITANCE
    $admins = User::where('is_admin', true)->get();
    $customers = User::where('is_admin', false)->get();
    66 — Twitter/GitHub: @cmgmyr

    View Slide

  67. 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();
    67 — Twitter/GitHub: @cmgmyr

    View Slide

  68. SINGLE TABLE INHERITANCE
    class Admin extends User
    {
    protected static function boot()
    {
    parent::boot();
    static::addGlobalScope(function ($query) {
    $query->where('is_admin', true);
    });
    }
    }
    68 — Twitter/GitHub: @cmgmyr

    View Slide

  69. SINGLE TABLE INHERITANCE
    class Customer extends User
    {
    protected static function boot()
    {
    parent::boot();
    static::addGlobalScope(function ($query) {
    $query->where('is_admin', false);
    });
    }
    }
    69 — Twitter/GitHub: @cmgmyr

    View Slide

  70. SINGLE TABLE INHERITANCE
    $admins = Admin::get();
    $customers = Customer::get();
    70 — Twitter/GitHub: @cmgmyr

    View Slide

  71. SINGLE TABLE INHERITANCE
    Read more:
    > https://twitter.com/cmgmyr/status/
    885204646498893824
    > https://tighten.co/blog/extending-models-in-
    eloquent
    71 — Twitter/GitHub: @cmgmyr

    View Slide

  72. DEFAULT MODEL
    DATA

    View Slide

  73. 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();
    });
    73 — Twitter/GitHub: @cmgmyr

    View Slide

  74. DEFAULT MODEL DATA
    class User extends Model
    {
    protected $fillable = [
    'name', 'email', 'password', 'role'
    ];
    }
    74 — Twitter/GitHub: @cmgmyr

    View Slide

  75. DEFAULT MODEL DATA
    $user = new User();
    $user->name = 'Chris';
    $user->email = '[email protected]';
    $user->password = Hash::make('p@ssw0rd');
    // $user->role is currently NULL
    $user->save();
    $user->role; // 'user'
    75 — Twitter/GitHub: @cmgmyr

    View Slide

  76. 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();
    });
    76 — Twitter/GitHub: @cmgmyr

    View Slide

  77. DEFAULT MODEL DATA
    Set $attributes
    class User extends Model
    {
    protected $fillable = [
    'name', 'email', 'password', 'role'
    ];
    protected $attributes = [
    'role' => 'user',
    ];
    }
    77 — Twitter/GitHub: @cmgmyr

    View Slide

  78. DEFAULT MODEL DATA
    $user = new User();
    $user->name = 'Chris';
    $user->email = '[email protected]';
    $user->password = Hash::make('p@ssw0rd');
    // $user->role is currently 'user'!
    $user->save();
    $user->role; // 'user'
    78 — Twitter/GitHub: @cmgmyr

    View Slide

  79. DEFAULT MODEL DATA
    $user = new User();
    $user->name = 'Chris';
    $user->email = '[email protected]';
    $user->password = Hash::make('p@ssw0rd');
    $user->role = 'admin'; // can override default
    $user->save();
    $user->role; // 'admin'
    79 — Twitter/GitHub: @cmgmyr

    View Slide

  80. DEFAULT MODELS
    Remember our previous example?
    class Post extends Model
    {
    public function author()
    {
    return $this->belongsTo(User::class)->withDefault([
    'name' => 'Guest Author',
    ]);
    }
    }
    80 — Twitter/GitHub: @cmgmyr

    View Slide

  81. 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();
    }
    }
    81 — Twitter/GitHub: @cmgmyr

    View Slide

  82. DEFAULT MODEL DATA
    class User extends Model
    {
    protected $fillable = [
    'name', 'email', 'password', 'role'
    ];
    protected $attributes = [
    'name' => 'Guest Author',
    'role' => 'user',
    ];
    }
    82 — Twitter/GitHub: @cmgmyr

    View Slide

  83. DEFAULT MODEL DATA
    Watch Colin DeCarlo's - Keeping Eloquent Eloquent from
    Laracon US 2016
    https://streamacon.com/video/laracon-us-2016/colin-
    decarlo-keeping-eloquent-eloquent
    83 — Twitter/GitHub: @cmgmyr

    View Slide

  84. SUB-QUERIES
    $customers = Customer::with('company')
    ->orderByName()
    ->paginate();
    Get latest interactions?
    {{ $customer
    ->interactions()
    ->latest()
    ->first()
    ->created_at
    ->diffForHumans() }}
    84 — Twitter/GitHub: @cmgmyr

    View Slide

  85. 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();
    {{ $customer->last_interaction_date->diffForHumans() }}
    85 — Twitter/GitHub: @cmgmyr

    View Slide

  86. SUB-QUERIES
    Jonathan Reinink's Laracon 2018 Online Talk - Advanced
    Querying with Eloquent
    https://github.com/reinink/laracon2018
    86 — Twitter/GitHub: @cmgmyr

    View Slide

  87. RESOURCES
    > https://laravel.com/docs/5.6/eloquent
    > https://laravel-news.com/eloquent-tips-tricks
    > https://twitter.com/themsaid/status/1029731544942952448
    > https://twitter.com/cmgmyr/status/885204646498893824
    > https://tighten.co/blog/extending-models-in-eloquent
    > https://streamacon.com/video/laracon-us-2016/colin-decarlo-keeping-
    eloquent-eloquent
    > https://github.com/reinink/laracon2018
    > https://eloquentbyexample.com
    87 — Twitter/GitHub: @cmgmyr

    View Slide

  88. THANK YOU!
    PLEASE SAY "HI"
    TWITTER.COM/CMGMYR
    GITHUB.COM/CMGMYR
    CHRISGMYR.COM

    View Slide