บทความ

Laravel 13.36 มีอะไรใหม่ Rule:: ครบทุกกฎ validation, DB::olap() และ worker ที่ไม่ติดฐานข้อมูลหลัก

Laravel 13.36 เพิ่ม Rule:: ให้กฎ validation ที่เหลือจนครบ ค่าที่มีจุลภาคไม่โดนแยกอีก มี DB::olap() สำหรับ PlanetScale ตัวเลือกให้ queue worker ล้าง sticky connection ระหว่าง job และแก้ช่องโหว่ใน Markdown mail

เผยแพร่
เวลาอ่าน
3 นาที
สรุปจาก
Paul Redmond · laravel-news.com

ทุกกฎ validation เขียนเป็น Rule:: ได้แล้ว

ปกติเราเขียนกฎ validation เป็น string อย่าง 'required_without:email' หรือ 'url:https' พิมพ์ชื่อกฎผิดก็ไม่มีอะไรเตือน มารู้อีกทีตอนฟอร์มนั้นมีคน submit แล้วเจอ error ส่วน Rule::unique(), Rule::in() หรือ Rule::enum() มีให้ใช้มานานแล้ว แต่ยังไม่ครบทุกกฎ ใน 13.36 @ericmp33 เติมที่เหลือจนครบ

// เดิม เขียนเป็น string
return [
    'phone' => ['required_without:email'],
    'website' => ['nullable', 'url:https'],
    'device_id' => ['uuid:4'],
    'tags.*' => ['distinct:ignore_case'],
    'password' => ['required', 'confirmed'],
];

// 13.36 เขียนเป็น Rule:: ได้ทุกกฎ
use Illuminate\Validation\Rule;

return [
    'phone' => [Rule::requiredWithout('email')],
    'website' => ['nullable', Rule::url()->protocols(['https'])],
    'device_id' => [Rule::uuid()->version(4)],
    'tags.*' => [Rule::distinct()->ignoreCase()],
    'password' => [Rule::required(), Rule::confirmed()],
];

กฎที่ไม่มีพารามิเตอร์อย่าง Rule::required(), Rule::nullable() หรือ Rule::json() คืนค่าเป็น string ธรรมดา ผลเหมือนเขียน 'required' เองทุกอย่าง ได้แค่ IDE ช่วยเติมชื่อ ส่วนกฎที่รับชื่อ field หรือค่า จะคืนเป็น object ที่ส่งพารามิเตอร์ให้ validator เป็น array ตรงนี้แหละที่เปลี่ยนผลจริง

ค่าที่มีจุลภาคไม่โดนแยกอีก

เวลา Laravel อ่านกฎแบบ string จะแยกพารามิเตอร์ที่จุลภาค ถ้าค่าที่เราเทียบมีจุลภาคอยู่ข้างใน ก็กลายเป็นสองค่าโดยไม่มีอะไรเตือน

// Laravel แยกเป็นสองค่า "ส่งด่วน" กับ " เก็บเงินปลายทาง"
'phone' => ['present_if:shipping,ส่งด่วน, เก็บเงินปลายทาง'],

// 13.36 ส่งไปเป็นค่าเดียว
'phone' => [Rule::presentIf('shipping', 'ส่งด่วน, เก็บเงินปลายทาง')],

// อยากให้ตรงค่าใดค่าหนึ่งก็ส่ง array
'phone' => [Rule::presentIf('shipping', ['express', 'cod'])],

เดิมต้องครอบค่าด้วยเครื่องหมายคำพูดเอง ("ส่งด่วน, เก็บเงินปลายทาง") ซึ่งเราไม่ค่อยนึกถึงจนกว่าจะเจอบั๊ก โดยเฉพาะตอนค่ามาจากตัวแปรหรือจากฐานข้อมูล

รอบนี้ Rule::date() ยังได้ equals() เพิ่มด้วย ใส่ได้ทั้งวันที่และชื่อ field

'delivered_on' => [Rule::date()->equals(today())],

ถ้ายังไม่คุ้นกับ validation ใน Laravel ลองอ่านLaravel Validation ทำได้กี่แบบก่อนได้ครับ

DB::olap() สำหรับคนที่ใช้ PlanetScale

เรื่องนี้เกี่ยวกับคนที่ใช้ PlanetScale หรือ MySQL บน Vitess ซึ่งรัน query ในโหมด OLTP เป็นค่าเริ่มต้น และจำกัดไว้ไม่เกิน 100,000 แถว รายงานยอดขายย้อนหลังหลายปีจึงพังได้ง่าย ๆ เดิมต้องสั่ง DB::statement("set workload = 'olap'") เอง ซึ่งมีผลแค่ connection ที่ใช้เขียน และถ้าลืมสลับกลับ connection นั้นก็ค้างอยู่ในโหมด OLAP ต่อ

$revenue = DB::olap(fn () => Order::query()
    ->selectRaw('customer_id, sum(total) as revenue')
    ->where('created_at', '>=', now()->subYears(3))
    ->groupBy('customer_id')
    ->get());

olap() สลับทั้ง connection อ่านและเขียนเป็น OLAP ก่อนรัน callback แล้วสลับกลับให้เมื่อจบ ถ้าฐานข้อมูลไม่ใช่ Vitess ก็แค่รัน callback เฉย ๆ โค้ดเดียวกันจึงรันกับ MySQL ในเครื่องเราได้ และมี isVitess() ไว้เช็คด้วย

Queue worker ล้าง sticky connection ระหว่าง job ได้

ถ้าใช้ฐานข้อมูลเครื่องเดียว ข้ามหัวข้อนี้ไปได้เลยครับ

พอเว็บโตจนแยกฐานข้อมูลเป็นเครื่องหลักไว้เขียน กับ read replica ไว้อ่าน เราตั้ง read กับ write แยกกันใน config/database.php และมักเปิด sticky ไว้ด้วย

'mysql' => [
    'read' => ['host' => [env('DB_READ_HOST')]],
    'write' => ['host' => [env('DB_HOST')]],
    'sticky' => true,
    // ...
],

sticky ทำให้หลังเขียนข้อมูลครั้งแรก การอ่านที่เหลือใน request นั้นไปอ่านจากเครื่องหลักแทน เพราะ replica อาจยังตามไม่ทัน เราจะได้เห็นข้อมูลที่เพิ่งบันทึก ใน request ปกติไม่มีปัญหา จบ request ก็เริ่มใหม่ แต่ queue worker เป็น process ที่รันยาว job เดียวที่เขียนข้อมูลจะทำให้ job ที่ตามมาอีกเป็นหมื่นตัวอ่านจากเครื่องหลักทั้งหมด จนกว่า worker จะ restart ตอน deploy รอบหน้า replica ที่ตั้งไว้แทบไม่ได้ใช้

13.36 ให้สั่ง worker ล้างสถานะนี้หลังจบแต่ละ job ได้

use Illuminate\Queue\Worker;

public function boot(): void
{
    Worker::$flushStickyConnections = true;
}

Laravel ปิดไว้เป็นค่าเริ่มต้น เพราะ job บางตัวอาจต้องอ่านสิ่งที่ job ก่อนหน้าใน worker เดียวกันเพิ่งเขียน ถ้าเปิดแล้ว job นั้นจะไปอ่านจาก replica ที่อาจยังไม่มีข้อมูล ก่อนเปิดลองไล่ดู job ที่ทำงานต่อกันเป็นทอด ๆ ก่อน คนเปิด PR เสนอให้เปิดเป็นค่าเริ่มต้นใน Laravel 14

ของเล็ก ๆ อีกสามอย่าง

  • Mercure ส่ง broadcast ทันที event ที่ส่งผ่าน Mercure อย่างเดียวทำงานแบบ ShouldBroadcastNow ไม่ต้องรอ queue ถ้าอยากให้เข้า queue เหมือนเดิม ตั้ง MERCURE_ASYNC=true
  • #[DeleteWhenMissingModels] กับ #[FailOnTimeout] ใช้กับ mailable ที่เข้า queue ได้แล้ว เดิม Laravel ไม่ส่งสอง attribute นี้ต่อให้ job ส่งอีเมล ถ้า order โดนลบก่อนอีเมลออก job ก็ fail ไปกองใน failed_jobs และถ้า SMTP ช้าจน timeout job จะลองใหม่ ทั้งที่อีเมลอาจออกไปแล้ว ลูกค้าเลยได้สองฉบับ
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use Illuminate\Queue\Attributes\FailOnTimeout;

#[DeleteWhenMissingModels]
#[FailOnTimeout]
class OrderShipped extends Mailable implements ShouldQueue
{
    public function __construct(public Order $order) {}
}
  • แก้ช่องโหว่ใน Markdown mail ข้อความที่ใส่ backslash ในลิงก์หลบตัวป้องกัน URL ของ Markdown mail ได้ ถ้าแอปของเราเอาข้อความที่ผู้ใช้กรอกไปใส่ในอีเมลแบบ Markdown ควร composer update laravel/framework รอบนี้

ดูรายการเต็มและ PR ของแต่ละข้อได้ในบทความต้นฉบับของ Laravel News

อ่านต่อ

ล่าสุด

ดูทั้งหมด →