บทความ

readonly property ใส่ค่าเริ่มต้นได้ไหม PHP 8.6 ทำได้แล้ว

PHP 8.6 ยกเลิกข้อห้ามที่มีมาตั้งแต่ PHP 8.1 ตอนนี้เขียน public readonly string $title = '…' ได้เลย ใช้คู่กับ interface property ของ PHP 8.4 ได้พอดี แต่ค่าเริ่มต้นนับเป็นการเขียนครั้งเดียวของ readonly แล้ว constructor จึงเขียนทับไม่ได้

เผยแพร่
เวลาอ่าน
3 นาที
สรุปจาก
Eric Van Johnson · shocm.me
แท็ก
#PHP

ก่อน PHP 8.6 ใส่ค่าเริ่มต้นไม่ได้

readonly property มาตั้งแต่ PHP 8.1 คือ property ที่เขียนค่าได้ครั้งเดียว และเขียนได้จากในคลาสเท่านั้น หลังจากนั้นอ่านได้อย่างเดียว แต่ถ้าใส่ค่าเริ่มต้นไว้ตรงที่ประกาศ PHP จะหยุดตั้งแต่ตอน compile

final class DailySalesReport
{
    public readonly string $title = 'ยอดขายรายวัน';
}
Fatal error: Readonly property DailySalesReport::$title cannot have default value

RFC readonly ตัวแรกให้เหตุผลไว้ว่า readonly ที่มีค่าเริ่มต้นก็คือค่าคงที่ดี ๆ นี่เอง ใช้ const แทนได้ ซึ่งก็จริงอยู่หลายปี จนมี interface property

interface property ทำให้ const ใช้แทนไม่ได้

PHP 8.4 ให้ interface บังคับได้ว่าคลาสต้องมี property ชื่ออะไร (อ่านเพิ่มได้ใน PHP 8.4 มีอะไรใหม่) สมมติเรามีรายงานหลายตัวที่ระบบส่งออกเป็น Excel ทุกตัวต้องบอกชื่อรายงานกับ view ที่ใช้

interface Report
{
    public string $title { get; }
    public string $view { get; }
}

const ไม่นับเป็น property จึงใช้ทำตาม interface นี้ไม่ได้ ใน PHP 8.4 และ 8.5 ถ้าอยากให้ค่าพวกนี้เป็น readonly ก็ต้องเขียน constructor ขึ้นมาใส่ค่าเอง

final class DailySalesReport implements Report
{
    public readonly string $title;
    public readonly string $view;

    public function __construct()
    {
        $this->title = 'ยอดขายรายวัน';
        $this->view = 'reports.daily-sales';
    }
}

ใน readonly class ทุก property เป็น readonly อยู่แล้ว ไม่ต้องเขียนซ้ำทีละตัว คลาสที่เป็นแค่ข้อมูลกำกับแบบนี้ (ชื่อ, view, รายการ step ที่ต้องรัน) จึงสั้นลงจนเหลือแค่ค่าจริง ๆ

property ที่มีค่าเริ่มต้นใช้ได้กับ interface ที่ขอแค่ { get; } เท่านั้น ถ้า interface ขอ { get; set; } ด้วย readonly ทำตามไม่ได้ และ PHP ก็ไม่ยอมตั้งแต่ตอน compile

constructor เขียนทับค่าเริ่มต้นไม่ได้

ข้อนี้น่าจะเจอเป็นอย่างแรก ค่าเริ่มต้นนับเป็นการเขียนครั้งเดียวที่ readonly อนุญาตไปแล้ว พอ constructor พยายามเขียนอีก PHP ก็ไม่ยอม

final readonly class DailySalesReport
{
    public string $title = 'ยอดขายรายวัน';

    public function __construct(string $title)
    {
        $this->title = $title;
    }
}

new DailySalesReport('ยอดขายสาขาเชียงใหม่');
// Error: Cannot modify readonly property DailySalesReport::$title

ถ้าอยากได้ค่าตั้งต้นที่คนสร้าง object เปลี่ยนได้ ให้ใส่ค่าเริ่มต้นที่พารามิเตอร์ของ constructor property promotion แบบที่ทำกันอยู่แล้ว RFC นี้ไม่ได้แตะส่วนนั้น

final readonly class DailySalesReport
{
    public function __construct(
        public string $title = 'ยอดขายรายวัน',
    ) {}
}

new DailySalesReport()->title;                       // ยอดขายรายวัน
new DailySalesReport('ยอดขายสาขาเชียงใหม่')->title;  // ยอดขายสาขาเชียงใหม่

แยกง่าย ๆ คือ ค่าที่ ไม่มีวันเปลี่ยน ใส่ไว้ที่ property ส่วนค่าที่ แค่มีค่าตั้งต้น ใส่ไว้ที่พารามิเตอร์

คลาสลูกเปลี่ยนค่าได้ และ clone เปลี่ยนได้ครั้งเดียว

คลาสลูกประกาศ property ชื่อเดิมพร้อมค่าใหม่ได้ ตามกฎ inheritance ปกติ

class ExportJob
{
    public readonly int $priority = 1;
}

final class UrgentExportJob extends ExportJob
{
    public readonly int $priority = 5;
}

new ExportJob()->priority;       // 1
new UrgentExportJob()->priority; // 5

ส่วนใน __clone() และ clone แบบส่งค่าใหม่ที่มาใน PHP 8.5 readonly ที่มีค่าเริ่มต้นเขียนใหม่ได้อีกครั้งเดียว เหมือน readonly ตัวอื่นที่มีค่าแล้ว

final class Counter
{
    public readonly int $value = 1;

    public function withValue(int $value): self
    {
        return clone($this, ['value' => $value]);
    }
}

new Counter()->value;               // 1
new Counter()->withValue(2)->value; // 2

object ที่ serialize ไว้ยังถือค่าเก่า

readonly ที่มีค่าเริ่มต้นนับว่ามีค่าแล้วตั้งแต่สร้าง object จึงไปอยู่ในข้อมูลที่ serialize() ด้วย ถ้าวันหนึ่งเราแก้ค่าเริ่มต้นในโค้ดแล้ว deploy object ที่ unserialize กลับมาจากของเดิมจะยังเป็นค่าเก่า

ใน Laravel เรื่องนี้เกิดได้กับ queue job ที่ค้างอยู่ใน queue ตอน deploy และของที่เก็บใน cache เป็น object ถ้าค่านั้นสำคัญ ให้รอ queue ว่างหรือล้าง cache ที่เกี่ยวข้องหลัง deploy หรืออ่านค่าจาก const แทนในจุดที่ต้องได้ค่าล่าสุดเสมอ

ข้ออื่นที่ RFC เขียนไว้

  • unset() ไม่ได้ เพราะ property มีค่าแล้ว ท่าเดิมที่ unset readonly ไว้ให้ __get() โหลดค่าทีหลัง ใช้กับ property แบบนี้ไม่ได้
  • public(set) ไม่ได้ช่วยให้เขียนได้ public public(set) readonly int $id = 1; ก็ยังเขียนทับไม่ได้ เพราะเขียนไปแล้วตั้งแต่สร้าง
  • trait สองตัวที่ประกาศ property ชื่อเดียวกัน ต้องมีค่าเริ่มต้นเท่ากัน ถ้าต่างกัน PHP หยุดด้วย fatal error ตามกฎ trait เดิม
  • Reflection hasDefaultValue() คืน true และ getDefaultValue() คืนค่านั้นตามปกติ
  • static property กับ property hooks ไม่เปลี่ยน readonly ยังใส่ hook ไม่ได้เหมือนใน PHP 8.4

ใช้ได้เมื่อไหร่

PHP 8.6 มีกำหนดออกวันที่ 19 พ.ย. 2569 ตอนนี้อยู่ช่วง release candidate การเปลี่ยนแปลงนี้แค่ผ่อนกฎตอน compile โค้ดเดิมจึงไม่พัง และไม่ต้องย้ายอะไร

ช่วงแรกเครื่องมืออาจยังตามไม่ทัน PhpStorm, PHPStan หรือ Psalm ที่ยังไม่อัปเดตอาจขึ้นเตือนว่า readonly มีค่าเริ่มต้นไม่ได้ ทั้งที่ PHP 8.6 รันได้ ถ้าจะใช้ในโปรเจกต์ ให้อัปเดตเครื่องมือพวกนี้ไปพร้อมกัน และแก้ "php" ใน composer.json เป็น ^8.6 ให้ server ทุกตัวรู้ว่าต้องใช้เวอร์ชันนี้

รายละเอียดและตัวอย่างเพิ่มเติมอ่านต่อได้ที่บทความต้นฉบับของ Eric Van Johnson (เจอผ่านลิงก์ที่ Freek Van der Herten แนะนำไว้) และ RFC ฉบับเต็ม

อ่านต่อ

ล่าสุด

ดูทั้งหมด →