20 เทคนิค Laravel Eloquent ที่ช่วยให้เขียน Model สั้นลง

เทคนิค Eloquent 20 ข้อที่ช่วยให้โค้ดสั้นและอ่านง่ายขึ้น และบอกไว้ด้วยว่าข้อไหนเปลี่ยนไปแล้วใน Laravel 13

เผยแพร่
อัปเดต
— เพิ่มหมายเหตุสำหรับ Laravel 13 และแก้ข้อ 17
เวลาอ่าน
5 นาที
แปลจาก
Povilas Korop · laravel-news.com
แท็ก
#Eloquent#Laravel

Eloquent ใช้ง่ายอยู่แล้ว แต่ในคลาส Model ยังมีเมธอดซ่อนอยู่อีกเยอะ บทความนี้รวมเทคนิคที่ช่วยให้คลาสของเราอ่านง่ายขึ้น บางข้อย่อโค้ดหลายบรรทัดเหลือบรรทัดเดียว โดยทำงานเหมือนเดิมทุกอย่าง

1. เพิ่มหรือลดค่าด้วย increment() และ decrement()

แทนที่จะเขียน

$article = Article::find($article_id);
$article->read_count++;
$article->save();

เราเขียนแบบนี้ได้

$article = Article::find($article_id);
$article->increment('read_count');

หรือแบบนี้ก็ได้เหมือนกัน

Article::find($article_id)->increment('read_count');
Article::find($article_id)->increment('read_count', 10); // +10
Product::find($product_id)->decrement('stock'); // -1

2. ใช้เมธอดแบบ XorY แทน if

Model มีหลายเมธอดที่รวมสองอย่างไว้ในตัวเดียว คือ "ลองทำ X ก่อน ถ้าไม่ได้ค่อยทำ Y"

ตัวอย่างแรกคือ findOrFail() แทนที่จะเขียน

$user = User::find($id);
if (! $user) {
    abort(404);
}

เขียนบรรทัดเดียวได้เลย

$user = User::findOrFail($id);

ตัวอย่างที่สองคือ firstOrCreate() แทนที่จะเขียน

$user = User::where('email', $email)->first();
if (! $user) {
    $user = User::create([
        'email' => $email,
    ]);
}

ก็เขียนแค่นี้

$user = User::firstOrCreate(['email' => $email]);

3. ดักจังหวะต่าง ๆ ของ Model ใน boot()

ในเมธอด boot() ของ Model เราใส่โค้ดดักไว้ได้หลายจังหวะ เช่น ตอนจะสร้างหรือตอนจะอัปเดต

class User extends Model
{
    public static function boot()
    {
        parent::boot();

        static::updating(function ($model) {
            // เขียน log
            // กำหนด property เพิ่ม เช่น
            // $model->something = transform($something);
        });
    }
}

ตัวอย่างที่เจอบ่อยที่สุดคือตั้งค่าบาง field ตอนสร้าง Model ครั้งแรก เช่น สร้าง UUID ให้เอง

public static function boot()
{
    parent::boot();

    self::creating(function ($model) {
        $model->uuid = (string) Str::uuid();
    });
}

ถ้าอยากรู้ว่า Model มี event อะไรอีกบ้าง และเขียน Event Listener ยังไง ลองอ่าน Model Event ต่อได้

4. ใส่ where และ orderBy ไว้ใน relation เลย

ปกติเราเขียน relation ใน Model แบบนี้

public function users()
{
    return $this->hasMany(User::class);
}

แต่ถ้ารู้อยู่แล้วว่าจะเรียก relation นี้ด้วยเงื่อนไขเดิมทุกครั้ง เราต่อ where() หรือ orderBy() ไว้ท้าย relation ได้เลย

public function approvedUsers()
{
    return $this->hasMany(User::class)
        ->where('approved', true)
        ->orderBy('email');
}

อีกวิธีที่ผมชอบใช้คือสร้าง relation ใหม่ต่อจาก relation เดิมที่มีอยู่แล้ว

public function users()
{
    return $this->hasMany(User::class);
}

public function approvedUsers()
{
    return $this->users()
        ->where('approved', true)
        ->orderBy('email');
}

แบบนี้จะเรียก users() เฉย ๆ หรือ approvedUsers() ก็ได้ แล้วแต่ว่าตอนนั้นต้องการแบบไหน

5. ตั้งค่า Model ผ่าน property อย่าง timestamps และ appends

Model มี "พารามิเตอร์" หลายตัวที่เราตั้งเป็น property ของคลาสได้ ตัวที่เห็นกันบ่อย ๆ ก็มี

class User extends Model
{
    protected $table = 'users'; // ชื่อตาราง
    protected $guarded = []; // ไม่ต้องป้องกันการ fill ค่าแบบ array
    protected $casts = ['deleted_at' => 'datetime']; // แปลง field เป็นวันที่ (Carbon)
    protected $appends = ['field1', 'field2']; // field ที่ต่อท้ายเพิ่มเวลาแปลงเป็น JSON
}

แต่จริง ๆ ยังมีอีก

protected $primaryKey = 'uuid'; // primary key ไม่จำเป็นต้องชื่อ "id"
public $incrementing = false; // primary key ไม่จำเป็นต้อง auto-increment
protected $perPage = 25; // จำนวนรายการต่อหน้าของ pagination (ค่าเริ่มต้น 15)
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at'; // เปลี่ยนชื่อ field created_at, updated_at ได้
public $timestamps = false; // ไม่ใช้ field created_at, updated_at เลยก็ได้
protected $with = ['profile']; // relation ที่ดึงมาด้วยทุกครั้งโดยอัตโนมัติ

ยังมีอีกหลายตัว เปิดดูได้ใน คลาส Model และ trait ที่ Model ใช้

6. find() หลาย record ในครั้งเดียว

find() ทุกคนคงรู้จักกันดีอยู่แล้ว

$user = User::find(1);

แต่คงมีไม่กี่คนที่รู้ว่าเราส่ง array ให้ find() ได้

$users = User::find([1, 2, 3]);

7. เขียน where ให้สั้นลงด้วย whereX()

where() ยังเขียนได้อีกแบบ จากแบบนี้

$users = User::where('approved', 1)->get();

เขียนเป็นแบบนี้ได้

$users = User::whereApproved(1)->get();

ต่อชื่อ field ไว้ท้าย where ได้เลย ที่ทำได้เพราะ Laravel ใช้ magic method __call() ของ PHP (เทคนิคที่เรียกว่า overloading)

ส่วนเงื่อนไขวันที่และเวลา ก็มีเมธอด where เตรียมไว้ให้หลายตัว

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'));

8. เรียงข้อมูลตามค่าใน relation

ข้อนี้ยากขึ้นอีกนิด สมมติเรามี Topic และอยากเรียง Topic ตาม Post ล่าสุดของแต่ละอัน ซึ่งเจอบ่อยเวลาทำระบบจัดการ content หรือ CMS

เริ่มจากเพิ่ม relation ของ Post ล่าสุดใน Topic

public function latestPost()
{
    return $this->hasOne(Post::class)->latest();
}

แล้วใน controller เขียนแบบนี้

$topics = Topic::with('latestPost')
    ->get()
    ->sortByDesc('latestPost.created_at');

สังเกตว่าเราเรียก get() เอา collection ออกมาก่อน แล้วค่อยใช้ sortByDesc() ของ collection เรียงจาก post ล่าสุด

9. ใช้ when() แทน if

บางทีเราต้องใส่เงื่อนไข query ตามค่าที่ผู้ใช้ส่งมาใน request() แบบนี้

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()

Author::query()
    ->when(request('filter_by') == 'likes', function ($q) {
        return $q->where('likes', '>', request('likes_amount', 0));
    })
    ->when(request('filter_by') == 'date', function ($q) {
        return $q->orderBy('created_at', request('ordering_rule', 'desc'));
    });

when() ยังส่งค่าของพารามิเตอร์ตัวแรกเข้า closure ให้เราใช้ใน query ได้ด้วย

$authors = User::query()
    ->when(request('role', false), function ($q, $role) {
        return $q->where('role_id', $role);
    })
    ->get();

10. กัน error ตอน belongsTo ไม่มีข้อมูลด้วย withDefault()

สมมติ Post belongsTo Author และใน Blade เราเขียนแบบนี้

{{ $post->author->name }}

ถ้า author โดนลบไปแล้ว หรือ post นั้นไม่มี author ตั้งแต่แรก เราจะเจอ error ประมาณ Attempt to read property "name" on null

จะแก้ด้วย null coalescing operator (??) ที่มีมาตั้งแต่ PHP 7 ก็ได้

{{ $post->author->name ?? '' }}

แต่จริง ๆ เรากันไว้ตั้งแต่ใน relation ได้เลย

public function author()
{
    return $this->belongsTo(Author::class)->withDefault();
}

ถ้า Post ไม่มี author ตัว author() จะคืน Model Author เปล่า ๆ มาให้แทน

และยังกำหนดค่าเริ่มต้นให้ Model เปล่าตัวนั้นได้ด้วย

public function author()
{
    return $this->belongsTo(Author::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

11. เรียงข้อมูลตาม accessor ด้วย sortBy() ของ collection

สมมติเรามี accessor แบบนี้

public function getFullNameAttribute()
{
    return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}

ถ้าอยากเรียงตาม full_name จะเขียนแบบนี้ไม่ได้

$clients = Client::orderBy('full_name')->get(); // ใช้ไม่ได้

ต้องเรียก get() ก่อน แล้วค่อยเรียงด้วย sortBy() ของ collection

$clients = Client::get()->sortBy('full_name'); // ใช้ได้

สังเกตว่าตรงนี้เราใช้ sortBy ของ Collection ไม่ได้ใช้ orderBy ของ Query Builder

12. ให้ query เรียงตาม name ทุกครั้งด้วย global scope

ถ้าอยากให้ User::all() เรียงตาม field name ทุกครั้ง ใส่ global scope ไว้ในเมธอด boot() ของ Model ได้ แบบเดียวกับข้อ 3

protected static function boot()
{
    parent::boot();

    // เรียงตาม field name จากน้อยไปมาก
    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('name', 'asc');
    });
}

ดูรายละเอียดเรื่อง Query Scopes ได้ในเอกสารของ Laravel

13. เขียน SQL ตรง ๆ ด้วยเมธอดกลุ่ม raw

พอเงื่อนไขเริ่มซับซ้อน บางทีเขียน SQL ตรง ๆ ง่ายกว่า ใช้เมธอดกลุ่ม raw ได้เลย เช่น

// whereRaw
$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "TX", ?, 100)', [200])
    ->get();

// havingRaw
Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();

// orderByRaw
User::where('created_at', '>', '2016-01-01')
    ->orderByRaw('(updated_at - created_at) desc')
    ->get();

14. คัดลอก Model เป็นแถวใหม่ด้วย replicate()

ถ้าอยากคัดลอกข้อมูลจาก Model หนึ่งไปเป็นอีกแถว ไม่ต้องไล่เขียนทุก field เป็น array แล้วส่งให้ Model ใหม่ ใช้ replicate() คัดลอกทุก field ได้เลย

$task = Task::find(1);
$newTask = $task->replicate();
$newTask->save();

15. ใช้ chunk() เมื่อข้อมูลเยอะ

ถ้าต้องวนข้อมูลเยอะ ๆ จากฐานข้อมูล แบ่งดึงทีละก้อน (chunk) จะกิน memory น้อยกว่า

แทนที่จะเขียน

$users = User::all();
foreach ($users as $user) {
    // ...
}

ใช้ chunk() แทน

User::chunk(100, function ($users) {
    foreach ($users as $user) {
        // ...
    }
});

16. สร้าง migration และ controller พร้อม Model ในคำสั่งเดียว

คำสั่ง Artisan ที่ใช้สร้าง Model เรารู้จักกันอยู่แล้ว

php artisan make:model Company

แต่ใส่ flag เพิ่มเพื่อสร้างไฟล์ที่เกี่ยวข้องไปพร้อมกันได้ด้วย

php artisan make:model Company -mcr
  • -m สร้างไฟล์ migration
  • -c สร้างไฟล์ controller
  • -r ให้ controller ที่สร้างเป็นแบบ resource controller

17. save() โดยไม่ให้ updated_at เปลี่ยน

บางทีเราอยาก save Model โดยไม่ให้ updated_at เปลี่ยนเป็นเวลาปัจจุบัน เช่น ตอนแก้ข้อมูลย้อนหลัง

$product = Product::find($id);
$product->stock = 0;

Product::withoutTimestamps(fn () => $product->save());

18. update() คืนค่าอะไร

เคยสงสัยไหมว่าเมธอด update() คืนค่าอะไรกลับมา

$result = Product::whereNull('category_id')->update(['category_id' => 2]);

query นี้อัปเดตฐานข้อมูลแน่ ๆ แต่ใน $result มีอะไร

update() คืนจำนวนแถวที่อัปเดตไป (affected rows) ถ้าอยากรู้ว่าอัปเดตไปกี่ record ก็ใช้ค่านี้ได้เลย ไม่ต้องทำอะไรเพิ่ม

19. จัดกลุ่ม AND กับ OR ด้วย closure ให้ได้วงเล็บที่ถูก

บางทีเราต้องใช้ AND กับ OR ปนกันใน SQL เช่น

... WHERE (gender = 'Male' AND age >= 18) OR (gender = 'Female' AND age >= 65)

ถ้าเขียนแบบนี้ query ที่ได้จะไม่ตรงกับที่เราต้องการ

$q->where('gender', 'Male');
$q->orWhere('age', '>=', 18);
$q->where('gender', 'Female');
$q->orWhere('age', '>=', 65);

ต้องจัดกลุ่มด้วย closure แบบนี้ ยาวขึ้นนิดหน่อยแต่ได้เงื่อนไขที่ถูก

$q->where(function ($query) {
    $query->where('gender', 'Male')
        ->where('age', '>=', 18);
})->orWhere(function ($query) {
    $query->where('gender', 'Female')
        ->where('age', '>=', 65);
});

20. ส่งหลายเงื่อนไขให้ orWhere() ในครั้งเดียว

ข้อสุดท้าย orWhere() รับ array ได้ จากที่ปกติเขียนแบบนี้

$q->where('a', 1);
$q->orWhere('b', 2);
$q->orWhere('c', 3);

ย่อเหลือแบบนี้ได้

$q->where('a', 1);
$q->orWhere(['b' => 2, 'c' => 3]);

อยากได้เทคนิค Eloquent เพิ่มอีก

ลองอ่าน 6 เทคนิค Eloquent ที่ช่วยให้โค้ดอ่านง่ายขึ้น ต่อได้ครับ

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →