20 เทคนิคใน Laravel Eloquent Model ที่เราอาจไม่รู้

6 กุมภาพันธ์ 2564
20 เทคนิคใน Laravel Eloquent Model ที่เราอาจไม่รู้

นอกจาก คลาส Eloquent Model ใน Laravel จะมีการใช้งานที่ง่ายดาย ช่วยให้เราเขียน code ติดต่อกับฐานข้อมูลได้ง่ายแล้ว คลาส Model ยังมี function ซ่อนไว้อีกมากมาย ในบทความนี้เราจะลองมาดูเทคนิดการเขียน code หลาย ๆ แบบที่จะช่วยให้คลาสของเราอ่านง่ายมากขึ้น บางเทคนิคช่วยลด code จากเดิมที่เราเขียนหลายบรรทัดทำให้เหลือเขียน code เพียงบรรทัดเดียวแล้วยังทำงานได้เหมือนเดิม

1. Increments และ Decrements

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

$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($produce_id)->decrement('stock'); // -1

2. เมธอด XorY

คลาส Model มี method หลายตัวที่เป็นการรวมสอง method เข้าด้วยกัน เช่น "ทำ X ก่อนถ้าไม่ได้ทำ Y"

ตัวอยาง 1 – findOrFail():

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

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

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

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

Example 2 – firstOrCreate():

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

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

ให้เขียนแบบนี้

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

3. เมธอด boot() ใน Model

ใน Model มีเมธอด boot() ที่เราสามารถแทรก code เพื่อดักการทำงานตาม Model Lifecycle ได้หลายตอน

class User extends Model
{
    public static function boot()
    {
        parent::boot();
        static::updating(function($model)
        {
            // ทำการ log
            // กำหนด property เพิ่ม เช่น
            // $model->something = transform($something);
        });
    }
}

ตัวอย่างที่ง่ายที่สุดน่าจะเป็นเรื่องการเซ็ตค่าบาง field ตอนที่กำลังสร้าง Model ครั้งแรก เช่น การกำหนดค่า primary key เป็นแบบ UUID

public static function boot()
{
  parent::boot();
  self::creating(function ($model) {
    $model->uuid = (string)Uuid::generate();
  });
}

สามารถดูเพิ่มเติมเกี่ยวกับ boot() และ Model Event เพื่อรู้จักกับ event แบบอื่น ๆ และวิธีการเขียน Event Listener มากขึ้น


4. เขียน Relation แบบมีเงื่อนไขและเรียงลำดับ

โดยปกติแล้วเราสามารถกำหนด Relation ของ Model ได้แบบนี้

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

แต่เรายังสามารถกำหนด where() หรือ orderBy() ต่อท้าย relation ได้เลยถ้าเรารู้ว่า relation ของเราถูกเรียกด้วยเงื่อนไขนี้แน่ ๆ อยู่แล้ว

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

หรืออีกวิธีการหนึ่งที่ผมชอบใช้คือการกำหนด relation โดยการเพิ่มเงื่อนไขจาก relation เดิมที่เรามีอยู่แล้ว เช่น

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

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

ด้วยวิธีการนี้ทำให้เราเรียกใช้ relation users() แบบปกติหรือ approvedUsers() ได้ขึ้นอยู่กับเงื่อนไขที่เราต้องการใช้งาน


5. Model property: timestamps, appends และอื่น ๆ

มี "Parameter" หลาย ๆ ตัวใน Model ที่เราสามารถกำหนดได้ในรูปแบบ property ของคลาส ที่เราอาจจะเคยเห็นกันบ่อย ๆ มีดังนี้

class User extends Model {
    protected $table = 'users'; // ชื่อตาราง
    protected $guarded = [];    // ไม่ต้องป้องการการ fill ค่าแบบ array
    protected $dates = ['created_at', 'deleted_at'];  // แปลง field เป็น date โดยใช้คลาส carbon
    protected $appends = ['field1', 'field2'];  // ต่อท้าย field เพิ่มเวลาแปลงเป็น json
}

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

protected $primaryKey = 'uuid'; // primary key ไม่จำเป็นต้องชื่อ field ว่า "id"
public $incrementing = false; // primary key ไม่จำเป็นต้อง auto-incrementing
protected $perPage = 25; // กำหนดจำนวน pagination ต่อ 1 หน้าที่คลาส Model ได้เลย (default 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 ที่ต้องการให้ดึงมาด้วยเลยโดยอัตโนมัติ

นอกจากด้านบนแล้วจริง ๆ ยังมีมากกว่านี้อีก เราสามารถดูเพิ่มเติมได้จาก abstract Model class และลองดู trait ที่ model มีการใช้งาน


6. การ Find แบบหลาย record

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

$user = User::find(1);

แต่ผมเชื่อว่าคงมีไม่กี่คนที่รู้ว่าเราสามารถส่งค่าเป็น array ไปยังเมธอด find() ได้

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

7. WhereX

ยังมีวิธีการเขียนเมธอด where() ได้อีกแบบ

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

เป็นแบบนี้

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

เราสามารถใช้ชื่อ field ต่อท้าย where ได้เลยโดยเบื้องหลังอาศัยการใช้ เทคนิค overloading magic method __call() ใน PHP

นอกจากนี้ยังมีเมธอด 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. การเรียงข้อมูล orderBy ตาม relation

ข้อนี้เป็นทริคที่ซับซ้อนขึ้นมาอีกนิด สมมติว่าเรามีโมเดล Topic ที่เราต้องการเรียงลำดับตาม Post ล่าสุด ซึ่งเราพบเห็นได้ถ้าเราเขียน code สำหรับจัดการ content หรือ CMS ทั่วไป

เริ่มจากนิยาม relation สำหรับ Post ล่าสุดใน Topic

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

แล้วใน controller เราสามารถเขียนได้แบบนี้

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

ข้อสังเกตุ วิธีการนี้เราเรียก get() ซึ่งจะได้ออกมาเป็น collection ก่อน แล้วเราจึงเรียกเมธอด sortBydesc() ของ collection เพื่อเรียงลำดับจาก post ล่าสุดให้เราแทน


9. ใช้เมธอด when() – ไม่ต้องง้อ if-else

บางครั้งเราต้องการระบุเงื่อนไข query จาก request() ที่ส่งเข้ามาจาก user แบบนี้

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

นอกจากนี้เรายังสามารถส่งค่าจาก parameter ตัวแรกมายัง closure function เพื่อเป็น parameter ใน query ได้เลย

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

10. โมเดลเริ่มต้นสำหรับ BelongsTo

สมมติว่าเรามี Post ที่ขึ้นกับ Author ใน blade เราอาจมี code แบบนี้

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

ทีนี้ถ้า author เกิดถูกลบไปไม่ได้ถูกเซ็ตไว้แต่แรกเราจะเจอ error message ประมาณว่า “property of non-object”.

แน่นอนว่าเราสามารถ Null coalescing operator (??) ซึ่งมีมาตั้งแต่ PHP version 7 แล้วได้

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

แต่จริง ๆ แล้วเราสามารถป้องกันได้ตั้งแต่ตอนกำหนด relation เลย

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

ในตัวอย่างนี้ relation author() จะ return Model ว่าง App\Author ถ้าไม่การเซ็ตไว้ใน Post.

เรายังสามารถกำหนด property เริ่มต้นที่จะแสดงในจาก relation ได้เลย

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

11. เรียงลำดับ Order By Mutator

สมมติว่าเรามี code ดังนี้

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'); // ใช้ได้

สังเกตว่าไม่ใช่เมธอด orderBy จากคลาส Query Builder แต่เป็น sortBy จากคลาส Collection


12. กำหนดการเรียงลำดับโดย default ใน global scope

สมมติถ้าเราต้องการให้ User::all() เรียงลำดับจาก field name เสมอ เราสามารถกำหนดผ่าน global scope ได้ โดยกำหนดผ่านเมธอด boot ในคลาส Model คล้ายกับวิธีการที่เราใช้ด้านบน

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

    // เรียงจาก field name ASC
    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('name', 'asc');
    });
}

อ่านเพิ่มเติมเรื่อง Query Scopes ได้ที่นี่


13. Raw query method

บางครั้งการเขียน SQL query โดยตรงอาจจะสะดวกกว่าถ้าเงื่อนไขที่เราใช้เริ่มมีความซับซ้อน เราสามารถทำได้โดยใช้ raw method เช่น

// 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. Replicate สร้าง copy ของ model

บางครั้งเราต้องการคัดลอกข้อมูลจาก model หนึ่งไปยังอีก model หนึ่ง แทนที่เราจะเขียน field ทั้งหมดที่มีในรูปแบบ array ทีละ field แล้วส่งค่าให้ model อีกตัว เราสามารถใช้ method replicate() เพื่อทำการคัดลอก field ทั้งหมดไปยังอีก model หนึ่งได้แบบนี้

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

15. ใช้เมธอด Chunk() สำหรับข้อมูลจำนวนมาก

แม้จะไม่เกี่ยวกับ model โดยตรงอาจจะเป็นเรื่องของ Collection มากกว่า แต่ก็ยังเป็นเรื่องที่ใช้ด้วยกันได้ ถ้าเราต้องการประมวลผลข้อมูลจากฐานข้อมูลจำนวนมากเราสามารถแบ่งก้อน (chunk) ที่ประมวลผลเพื่อลดการใช้ memory ในการเก็บข้อมูลได้

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

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

ใช้ chunk() คลุมก่อน

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

16. สร้างอย่างอื่นที่เกี่ยวข้องกับคลาส model ตอนใช้ Artisan command

เรารู้จัก Artisan command เพื่อใช้สร้างคลาส Model

php artisan make:model Company

แต่เรายังสามารถกำหนด flag เพื่อ generate ไฟล์ที่เกี่ยวข้องกับ Model ได้อีกด้วย

php artisan make:model Company -mcr
  • -m จะสร้างไฟล์ migration 
  • -c จะสร้างไฟล์ controller
  • -r เพื่อระบุว่า controller ที่สร้างขึ้นเป็นแบบ resourceful

17. Override updated_at ตอน save

เราสามารถส่ง paramter ไปยังเมธอด save() ได้ วิธีใช้งานอย่างหนึ่งคือเพื่อบอกว่าเราไม่ต้องการให้แก้ค่า updated_at ถ้าเราไม่ต้องการ

$product = Product::find($id);
$product->updated_at = '2019-01-01 10:00:00';
$product->save(['timestamps' => false]);

18. ผลลัพธ์ของการ update() คืออะไร?

เคยสงสัยไหมว่า method update() จริง ๆ แล้ว return อะไรกลับมา

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

แน่นอนว่าข้อมูลถูก update ลง database แต่ว่าค่าในตัวแปร $result จะเก็บอะไรอยู่

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


19. แปลงวงเล็บเป็น query ให้ถูกต้อง

บางครั้งเราต้องใช้ And และ OR ร่วมกันใน SQL query เช่น

... 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 หลาย parameters

สุดท้าย เราสามารถส่ง parameter เป็นแบบ array ไปยัง orWhere() ได้
วิธีปกติ

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

เขียนแบบนี้แทนได้

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

ส่งท้าย

ในบทความนี้เราได้นำเสนอเทคนิดหลาย ๆ อย่างในการใช้เมธอดและ propery ในคลาส Model แบบขั้นเทพมากยิ่งขึ้น หากสนใจเทคนิคเกี่ยวกับ Eloquent Model ลองดูทริคที่น่าสนใจของ Laravel Eloquent ได้เพิ่มเติม

Phattarachai Chaimongkol

เกี่ยวกับ phattarachai.dev

มองหาคนช่วยทำ Web App ใช้ภายในธุรกิจอยู่มั้ยครับ
มีความชำนาญในการพัฒนา Web Application ด้วย Laravel รับพัฒนาโปรเจคให้ผู้ประกอบการ ธุรกิจ SME ทั้งขนาดเล็กและขนาดใหญ่ พัฒนาระบบใช้ในองค์กรทั้งภาครัฐและเอกชน เป็นพาร์ทเนอร์กับบริษัททางด้าน Digital Agency เพื่อพัฒนาโปรเจคให้แก่ลูกค้า ทักเข้ามาพูดคุยกันก่อนได้เลยครับ

เรื่องล่าสุด

Backup ฐานข้อมูลด้วย Laravel อย่างง่าย ๆ ด้วย Spatie DB Snapshots
6 กุมภาพันธ์ 2564
Backup ฐานข้อมูลด้วย Laravel อย่างง่าย ๆ ด้วย Spatie DB Snapshots
วิธีการให้ git จดจำ password โดยไม่ต้องระบุใหม่ทุกครั้ง
6 กุมภาพันธ์ 2564
วิธีการให้ git จดจำ password โดยไม่ต้องระบุใหม่ทุกครั้ง
วิธีการเช็ค Detect Browser ผู้ใช้จาก Laravel
6 กุมภาพันธ์ 2564
วิธีการเช็ค Detect Browser ผู้ใช้จาก Laravel