Kamis, 20 October 2022

Mekanisme Kerja Laravel: Panduan Untuk Pemula

Dalam artikel ini saya akan memberikan beberapa rule yang direkomendasikan dalam menggunakan Laravel.

Laravel
Tips and Tricks

Dalam artikel ini saya akan memberikan beberapa rule yang direkomendasikan dalam menggunakan Laravel.

N+1 Issue

Anggap dulu Anda membuat aplikasi toko online yang pastinya akan menggunakan tabel yang namanya produk yang biasanya berelasi ke kategori. Apa yang kita lakukan adalah menampilkan produk beserta dengan kategori yang kurang lebih struktur untuk tabel produk akan seperti ini:

products
    id - bigint
    category_id - bigint
    title - string
 
categories
    id - bigint
    name - string
    slug - string

Perhatikan tanda yang saya beri untuk kolom category_id, yang itu artinya setiap produk yang kita buat pastinya mempunyai id dari tabel kategori. Saya akan mencoba untuk menampilkan produknya yang itu kurang lebih seperti ini:

$products = Product::get();

foreach($products as $product) {
    echo $product->title . ' / ' . $product->category->name . '<br/>';
}

Apa yang dilakukan dari kode di atas adalah membuat query yang berulang pada tabel kategori. Lihat query di bawah:

select * from `products`
select * from `categories` where `categories`.`id` = 1 limit 1
select * from `categories` where `categories`.`id` = 1 limit 1
select * from `categories` where `categories`.`id` = 1 limit 1
select * from `categories` where `categories`.`id` = 1 limit 1
select * from `categories` where `categories`.`id` = 1 limit 1
select * from `categories` where `categories`.`id` = 2 limit 1
select * from `categories` where `categories`.`id` = 2 limit 1
...

Jika Anda ingin melihat query, coba pakai laravel debugbar, maka Anda akan melihat query yang berulang behind the scene nya. Anda mungkin sudah tahu, untuk menangani hal seperti ini bisa dengan yang namanya eager loading.

//$products = Product::get();
$products = Product::with('category:id,name,slug')->get();

foreach($products as $product) {
    echo $product->title . ' / ' . $product->category->name . '<br/>';
}

Tetapi poin nya bukan itu, saya ingin memberi tahu, bahwa Anda bisa melakukan yang namanya prevention dengan menambahkan 1 bari kode pada AppServiceProvider. By default, dia akan seperti ini kurang lebih:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }

    public function boot()
    {
        //
    }
}

Dan kita akan menambahkan yang tepat pada method boot seperti:

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register() {...}

    public function boot()
    {
        Model::preventLazyLoading(!$this->app->isProduction());
    }
}

Perhatikan bahwa disana saya memberikan !$this->app->isProduction() pada parameter nya, itu artinya error akan muncul pada saat mode local saja, jika sudah mode production, maka itu akan tidak kelihatan lagi error nya. Anda bisa set environment nya pada file .env.

Dan sekarang, jika Anda hilangkan eager load nya pada saat menampilkan produk, maka harusnya akan ada error exception yang kita terima. Perhatikan kode dibawah ini:

$products = Product::get();
//$products = Product::with('category:id,name,slug')->get();

foreach($products as $product) {
    echo $product->title . ' / ' . $product->category->name . '<br/>';
}

Lihat di browser, maka akan ada error exception yang itu dari LazyLoadingViolationException.

Attempted to lazy load [category] on model [App\Models\Product] but lazy loading is disabled.

Namun, jika kita ingin menampilkan error secara diam-diam, bisa dibilang kita ingin melihat ni apa-apa saja yang kena lazy load saat production. Kita bisa menggunakan yang namanya handleLazyLoadingViolationUsing, untuk itu kita bisa buat kode nya seperti:

public function boot()
{
    Model::preventLazyLoading();

    if ($this->app->isProduction()) {
        Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
            $class = get_class($model);

            info("Attempted to lazy load [{$relation}] on model [{$class}].");
        });
    }
}

Maka dengan seperti itu, jika aplikasi kita sudah di mode production dan memang ada yang memiliki isu n+1. Harusnya error akan muncul pada storage/logs/laravel.log. Jika Anda ingin melihat bagaimana melihat log ini dengan mudah, Anda bisa lihat pada artikel ini: Penampil Log yang Mantap untuk Laravel

Polymorphic mapping

Relasi yang kita gunakan jika kita ingin memanfaatkan satu tabel yang itu bisa di pakai di beberapa tabel. Bayangkan jika Anda mempunyai tabel artikel dan video dengan masing-masing dari mereka mempunyai komentar. Dan itu harusnya bisa dibuat 1 tabel saja untuk komentarnya, dan akan dipakaikan ke tabel artikel dan video.

Jadi kurang lebih struktur tabel nya akan seperti:

articles
    id - bigint
    title - string
 
videos
    id - bigint
    title - string
 
comments
    id - bigint
    user_id - bigint
    body - text
    commentable_id - bigint
    commentable_type - string

Dan harusnya isi dari kolom commentable_type akan menunjukkan model yang dipakai seperti:

mysql> select id, body, commentable_type, commentable_id from comments;
+----+------+--------------------+----------------+
| id | body | commentable_type   | commentable_id |
+----+------+--------------------+----------------+
|  1 | ...  | App\Models\Article |              3 |
|  2 | ...  | App\Models\Article |             44 |
|  3 | ...  | App\Models\Video   |              1 |
|  4 | ...  | App\Models\Article |              4 |
|  5 | ...  | App\Models\Video   |             66 |
|  6 | ...  | App\Models\Video   |             64 |
+----+------+--------------------+----------------+

Ini adalah standard dari laravel yang memasukkan full namespace dari model tersebut. Yang itu bisa dibilang tidak good practice. Kenapa seperti itu, bayangkan jika kita merubah classname nya dengan alasan yang bisa saja. Maka semua kode akan break karena telah mengambil class tersebut sebagai identifier nya.

Untuk merubah default ini kita bisa menggunakan yang namanya morphMap pada metode boot yang ada pada AppServiceProvider.

namespace App\Providers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register() {...}

    public function boot()
    {
        ...
        Relation::morphMap([
            'video' => \App\Models\Video::class,
            'article' => \App\Models\Article::class,
        ]);
    }
}

Dengan begitu, maka nanti setiap ada penambahan file harusnya akan sudah di map dengan ketentuan yang sudah kita buat. Namun, jika data yang Anda miliki sudah ada, jangan lupa untuk memperbarui nya secara manual ya seperti:

UPDATE `comments`
SET commentable_type = 'video'
WHERE commentable_type = 'App\\Models\\Video'

Dengan begitu, harusnya sekarang akan sudah berubah, dan jangan lupa update juga untuk artikel nya. Jadi sekarang jika kita lihat isi tabel nya akan sudah berubah seperti:

mysql> select id, body, commentable_type, commentable_id from comments;
+----+------+------------------+----------------+
| id | body | commentable_type | commentable_id |
+----+------+------------------+----------------+
|  1 | ...  | article          |              3 |
|  2 | ...  | article          |             44 |
|  3 | ...  | video            |              1 |
|  4 | ...  | article          |              4 |
|  5 | ...  | video            |             66 |
|  6 | ...  | article          |             64 |
+----+------+------------------+----------------+

Model strictness

Sejak laravel versi 9.35.0, kita telah diberikan metode baru untuk model yaitu Model::shouldBeStrict, dimana metode 3 strictness eloquent yaitu:

  1. Model::preventLazyLoading()
  2. Model::preventSilentlyDiscardingAttributes()
  3. Model::preventsAccessingMissingAttributes()

Sebelum kita lanjut lebih jauh, disini saya akan menunjukkan struktur dari tabel users yang saya miliki.

users
    id - bigint
    name - string
    email - string
    email_verified_at - datetime
    password - string
    remember_token - string
    created_at - datetime
    updated_at - datetime

Baik, sekarang saya akan mencoba menampilkan single user dengan mencoba memasukkan field yang tidak ada di dalam struktur tabel nya yaitu about.

Route::get('users/{user}', function (User $user) {
    return [
        "id" => $user->id,
        "name" => $user->name,
        "about" => $user->about,
        "created_at" => $user->created_at->format('j M Y, g:i a'),
    ];
});

Seharusnya, by default about akan menjadi null, lihat outputnya di browser akan sudah seperti:

{
    "id": 3,
    "name": "Jimmy Page",
    "about": null,
    "created_at": "20 Oct 2022, 4:41 am"
}

Dan jika Anda perhatikan baik-baik, json itu menampilkan semua kolom yang ada di tabel users kecuali password, dan sudah pasti. Disini kita tidak punya yang namanya kolom about. Sekarang, saya akan menambahkan 1 baris kode lagi pada metode boot seperti:

public function boot()
{
    Model::shouldBeStrict();
  
    Model::preventLazyLoading();
    if ($this->app->isProduction()) {...}

    Relation::morphMap([...]);
}

Perhatikan pada baris yang tandai, itu adalah penambahan yang saya maksud, guna untuk memberitahu kita, mana nih attribute yang tidak ada dalam kolom tabel nya yang kita coba untuk tampilkan. Refresh di browser, dan sekarang akan muncul exception error dari MissingAttributeException yang kurang lebih itu seperti:

The attribute [about] either does not exist or was not retrieved for model [App\Models\User].

Sama layaknya seperti preventLazyLoading, kita tentunya juga bisa membuat ini diam-diam di waktu mode production.

public function boot()
{
    Model::shouldBeStrict();

    Model::preventLazyLoading();
    
    if ($this->app->isProduction()) {
        Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
            $class = get_class($model);

            info("Attempted to lazy load [{$relation}] on model [{$class}].");
        });
        
        Model::handleLazyLoadingViolationUsing(...);
    }

    Relation::morphMap(...);
}

Dan jika Anda memang tidak ingin menampilkan exception lewat log laravel, entah mungkin itu dengan alasan tersendiri. Anda bisa matikan itu dengan cara seperti:

public function boot()
{
    Model::preventAccessingMissingAttributes();
    Model::preventSilentlyDiscardingAttributes();

    Model::preventLazyLoading(!$this->app->isProduction());

    Relation::morphMap([
        'video' => \App\Models\Video::class,
        'article' => \App\Models\Article::class,
    ]);
}

Saya sengaja mengaktifkan preventAccessingMissingAttributes, karena itu bergantung dengan kebenaran data yang kita tidak tampilkan, kita tidak bisa menjamin sampai mana typo yang kita lakukan. Jadi harusnya, dengan adanya metode ini, memudahkan kita melihat dimana kesalahan kita untuk kemudian kita benarkan.

Kesimpulan

Bisa sama-sama kita simpulkan bahwa menggunakan laravel itu yang penting explicit. Semua yang diberikan by default itu bisa dibilang implicit, sehingga tugas kita sebagai developer membuatnya explicit. Semoga artikel ini bermanfaat, saya Irsyad. Dan saya akan melihat Anda nanti di artikel selanjutnya.