20 เทคนิคใน Laravel Eloquent Model ที่เราอาจไม่รู้
6 กุมภาพันธ์ 2564
นอกจาก คลาส 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 ได้เพิ่มเติม