บทความ

Syntax ใหม่ใน PHP 8 มีอะไรบ้าง

รวม syntax ที่ PHP 8.0 เพิ่มเข้ามาและเรายังใช้กันทุกวัน ตั้งแต่ named arguments, match, nullsafe ไปจนถึง constructor property promotion พร้อมโค้ดก่อนและหลังให้เทียบกัน

เผยแพร่
อัปเดต
— จัดหัวข้อใหม่ แก้ตัวอย่างให้ตรงกับ PHP 8.4 และเพิ่มตัวอย่าง match กับ attributes ใน Laravel
เวลาอ่าน
5 นาที
สรุปจาก
Brent Roose · stitcher.io
แท็ก
#PHP

PHP ออกเวอร์ชัน 8.0 มาตั้งแต่วันที่ 26 พ.ย. 63 พร้อมฟีเจอร์และ syntax ใหม่หลายอย่าง โพสต์นี้หยิบเฉพาะตัวที่เราน่าจะได้ใช้ตอนเขียนโค้ดทั่วไป โดยเรียงตามบทความ What's new in PHP 8 ของ stitcher.io และตัวอย่างจากหน้า release ของ php.net

Named arguments ส่งค่าตามชื่อ parameter ได้เลย

เดิมถ้าอยากเปลี่ยนแค่ parameter ตัวท้าย ๆ เราต้องใส่ค่าของทุกตัวที่อยู่ก่อนหน้าให้ครบ ถึงจะเป็นค่า default ก็ตาม พอใช้ named arguments (RFC) เราระบุเฉพาะตัวที่ต้องการ ข้าม optional parameter ที่เหลือ และไม่ต้องเรียงตามลำดับ

// ไม่ใช้ named arguments: อยากปิด double_encode ต้องใส่ flags กับ encoding ให้ครบก่อน
htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8', false);

// ใช้ named arguments: ใส่เฉพาะตัวที่ต้องการ ที่เหลือใช้ค่า default
htmlspecialchars($string, double_encode: false);

ผสมกันได้ด้วย โดยใส่แบบตามลำดับไว้ก่อน แล้วค่อยตามด้วยแบบมีชื่อ

Union types รับได้หลาย type ในตัวเดียว

PHP เป็นภาษา dynamic type ก่อน PHP 8 ถ้า parameter รับได้ทั้ง int และ float เราทำได้แค่ไม่ใส่ type แล้วเขียนบอกไว้ใน docblock ซึ่ง PHP ไม่ได้เช็คให้ ใน PHP 8 เราเขียนหลาย type คั่นด้วย | ได้เลย (RFC) แล้ว PHP เช็คให้ตอนรัน

// PHP 7
class Number {
  /** @var int|float */
  private $number;

  /**
   * @param float|int $number
   */
  public function __construct($number) {
    $this->number = $number;
  }
}
new Number('NaN'); // ผ่าน ไม่มี error

// PHP 8
class Number {
  public function __construct(
    private int|float $number
  ) {}
}

new Number(1);     // ใช้ได้
new Number(1.234); // ใช้ได้
new Number('NaN'); // TypeError

ใส่ void ใน union type ไม่ได้ เพราะ void แปลว่าไม่คืนค่าอะไรเลย ส่วนถ้าอยากให้รับ null ได้ด้วย เขียนได้สองแบบ คือใส่ |null หรือใส่ ? หน้าชื่อ type

public function foo(Foo|null $foo): void;

public function bar(?Bar $bar): void;

แต่ ? ใช้ได้กับ type เดียวเท่านั้น ถ้ามีหลาย type ต้องเขียน int|string|null จะเขียน ?int|string ไม่ได้

Nullsafe operator ?-> เรียกเมธอดต่อจากค่าที่อาจเป็น null

เราอาจคุ้นกับ Null coalescing operator ?? ที่มีมาตั้งแต่ PHP 7 แต่ ?? ช่วยไม่ได้ตอนเรียกเมธอดต่อจากค่าที่เป็น null เพราะ PHP จะ throw Error ตั้งแต่ตอนเรียกเมธอด ที่ผ่านมาเราจึงต้องดักด้วย ternary ก่อน หรือถ้าเขียน Laravel ก็ครอบด้วย helper optional()

$startDate = $booking->getStartDate();

// PHP
$dateAsString = $startDate ? $startDate->asDateTimeString() : null;

// Laravel
$dateAsString = optional($startDate)->asDateTimeString();

PHP 8 เพิ่ม ?-> (RFC) ถ้าค่าทางซ้ายเป็น null ทั้ง chain จะหยุดแล้วคืน null ทันที

$dateAsString = $booking->getStartDate()?->asDateTimeString();

optional() ยังมีอยู่ใน Laravel 13 แต่โค้ดใหม่ใช้ ?-> แทนได้เลย อ่านเพิ่มเกี่ยวกับ nullsafe operator ได้ที่นี่

Attributes ใส่ metadata ให้คลาสโดยไม่ต้องใช้ docblock

Attributes (RFC) คือสิ่งที่ภาษาอื่นเรียกว่า annotations ใช้แปะข้อมูลเพิ่มให้คลาส เมธอด property หรือ parameter แล้วให้โค้ดอื่นอ่านผ่าน Reflection เดิมเราต้องเขียนเป็น comment ใน docblock แล้วให้ library ไป parse เอง

ตัวอย่างจาก RFC มีสองไฟล์ คือคลาส attribute กับคลาสที่เอา attribute ไปแปะ

namespace App\Attributes;

use Attribute;

#[Attribute]
class ExampleAttribute
{
    public function __construct(
        public ?string $value = null,
    ) {}
}

อ่านค่ากลับผ่าน Reflection ได้แบบนี้

$attribute = (new ReflectionClass(Foo::class))->getAttributes(ExampleAttribute::class)[0];

$attribute->newInstance()->value; // 'class'

ใน Laravel 13 เราเจอ attributes บ่อยขึ้นมาก เช่นกำหนด $fillable, $hidden และ observer ของ model บนหัวคลาสได้เลย

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;

#[Fillable(['name', 'email'])]
#[Hidden(['password'])]
#[ObservedBy(UserObserver::class)]
class User extends Authenticatable
{
    // …
}

Match expression (RFC)

match (RFC) ทำงานคล้าย switch แต่ต่างกันหลายจุด

  • คืนค่าออกมาให้เก็บใส่ตัวแปรได้เลย ไม่ต้องมี break
  • เทียบแบบ strict (===) ไม่แปลง type ให้ เช่น '1' ไม่ตรงกับ 1
  • ไม่ไหลต่อไปทำ arm ถัดไปเหมือน switch ที่ลืมใส่ break
  • ถ้าไม่มี arm ไหนตรงและไม่มี default PHP จะ throw UnhandledMatchError
$result = match ($input) {
    0 => 'hello',
    '1', '2', '3' => 'world',
    default => 'unknown',
};

ถ้าอยากเช็คเงื่อนไขแทนค่าคงที่ ใส่ true เป็นตัวตั้ง แล้ว PHP จะเลือก arm แรกที่เงื่อนไขเป็นจริง

$grade = match (true) {
    $score >= 80 => 'A',
    $score >= 70 => 'B',
    $score >= 60 => 'C',
    default => 'F',
};

อ่านเพิ่มเกี่ยวกับ match expression ได้จากลิงก์นี้

Constructor property promotion ประกาศ property ใน constructor ได้เลย

บ่อยครั้งที่เรารับค่าผ่าน constructor แล้วต้องประกาศ property ไว้รับค่านั้น เดิมเราเขียนแบบนี้

// PHP 7.4
class Money
{
    public Currency $currency;

    public int $amount;

    public function __construct(
        Currency $currency,
        int $amount
    ) {
        $this->currency = $currency;
        $this->amount = $amount;
    }
}

PHP 8 ให้รวมสองส่วนนี้ไว้ใน constructor ได้เลย (RFC)

// PHP 8
class Money
{
    public function __construct(
        public Currency $currency,
        public int $amount,
    ) {}
}

โค้ดสั้นลงเยอะ และตั้งแต่ PHP 8.1 ใส่ readonly หน้า property ที่ promote ได้ด้วย ดูเพิ่มเติมเรื่อง Constructor Property Promotion

static เป็น return type ได้แล้ว

เดิมเราใส่ self เป็น return type ได้อยู่แล้ว แต่เพิ่งจะใส่ static ได้ใน PHP 8 (RFC) ต่างกันตรงที่ static หมายถึงคลาสที่เรียกจริง พอคลาสลูกเรียกเมธอดนี้ก็ได้ object ของคลาสลูกกลับไป เหมาะกับเมธอดแบบ fluent หรือ factory

class Foo
{
    public function test(): static
    {
        return new static();
    }
}

mixed type บอกว่าตั้งใจรับได้ทุก type

ก่อนมี mixed ถ้า function ไม่ใส่ type เราไม่รู้ว่าคนเขียนตั้งใจรับทุก type หรือแค่ลืมใส่ mixed (RFC) บอกให้ชัดว่าตั้งใจ เช่น

  • function คืนค่าได้หลาย type รวมถึง null
  • parameter รับค่าได้ทุก type
  • function ที่เขียน type ให้แคบกว่านี้ไม่ได้จริง ๆ

mixed เท่ากับ union ของ type เหล่านี้ทั้งหมด

  • array
  • bool
  • callable
  • int
  • float
  • null
  • object
  • resource
  • string

ใช้ได้ทั้งกับ parameter, property และ return type

mixed รวม null ไว้อยู่แล้ว จึงใส่ ? นำหน้าให้เป็น nullable อีกไม่ได้

// Fatal error: Type mixed cannot be marked as nullable since mixed already includes null
function bar(): ?mixed {}

throw เป็น expression ใช้ใน arrow function กับ ?? ได้

PHP 8 เปลี่ยน throw จาก statement เป็น expression (RFC) เราจึงเขียน throw ในจุดที่เดิมเขียนไม่ได้ เช่น

$triggerError = fn () => throw new MyError();

$foo = $bar['offset'] ?? throw new OffsetDoesNotExist('offset');

ใช้ ::class กับ object ได้แล้ว

อาจเป็นเรื่องเล็ก แต่ใช้บ่อยพอสมควร (RFC) ใน PHP 7 ถ้าอยากรู้ว่า object เป็นคลาสอะไร เราต้องเรียก get_class() แต่ PHP 8 ใช้ ::class กับตัวแปรได้ตรง ๆ

$foo = new Foo();

// PHP 7
var_dump(get_class($foo));

// PHP 8
var_dump($foo::class);

catch แบบไม่ต้องรับตัวแปร

ก่อน PHP 8 ทุก catch ต้องมีตัวแปรมารับ exception ถึงเราจะไม่ได้ใช้ตัวแปรนั้นเลยก็ตาม PHP 8 ให้ละตัวแปรได้ (RFC)

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

// PHP 7
try {
    // Something goes wrong
} catch (MySpecialException $exception) {
    Log::error('Something went wrong');
}

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

// PHP 8
try {
    // Something goes wrong
} catch (MySpecialException) {
    Log::error('Something went wrong');
}

เรายังต้องระบุ type ของ exception ที่จะดักอยู่ดี ถ้าอยากดักทุกแบบโดยไม่สนประเภท ใช้ Throwable เป็น type ใน catch

ใส่ comma ต่อท้าย parameter list ได้

ตอนเรียก function เราใส่ comma ต่อท้าย argument ตัวสุดท้ายได้ตั้งแต่ PHP 7.3 แล้ว PHP 8 เพิ่มให้ใส่ได้ตอนประกาศ function ด้วย (RFC)

public function doSomething(
    string $a,
    int $b,
    Foo $c,
) {
    // …
}

รวมถึง parameter และรายการ use ของ closure (RFC)

$longArgs_longVars = function (
    $longArgument,
    $longerArgument,
    $muchLongerArgument,
) use (
    $longVar1,
    $longerVar2,
    $muchLongerVar3,
) {
    // body
};

เวลาเพิ่ม parameter ทีหลัง diff ใน git จะเปลี่ยนแค่บรรทัดเดียว

Stringable interface สำหรับคลาสที่มี __toString()

Stringable (RFC) ใช้เป็น type hint ของคลาสอะไรก็ได้ที่มีเมธอด __toString() และเราไม่ต้องเขียน implements Stringable เอง พอคลาสมี __toString() PHP ก็ implement ให้อัตโนมัติ

class Foo
{
    public function __toString(): string
    {
        return 'foo';
    }
}

function bar(string|Stringable $stringable) { /* … */ }

bar(new Foo());
bar('abc');

str_contains() เช็คว่าใน string มีคำที่หาไหม

PHP 8 เพิ่ม str_contains() (RFC) เราจึงไม่ต้องใช้ strpos() แล้วเทียบกับ false อีก จากเดิมที่เขียนแบบนี้

if (strpos('string with lots of words', 'words') !== false) { /* … */ }

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

if (str_contains('string with lots of words', 'words')) { /* … */ }

ระวังกรณีคำที่หาเป็น string ว่าง str_contains() จะคืน true เสมอ

ถ้าเคยเห็น str_contains() ในโปรเจกต์ที่รันบน PHP 7 มาก่อน ส่วนใหญ่มาจาก symfony/polyfill-php80 ที่ติดมากับ dependency บน PHP 8 ขึ้นไปเป็น function ของ PHP เองแล้ว ส่วนใน Laravel ยังมี Str::contains() ที่รับหลายคำหรือไม่สนตัวพิมพ์เล็กใหญ่ได้

str_starts_with() และ str_ends_with()

คล้ายกับ str_contains() PHP 8 เพิ่มอีกสอง function ไว้เช็คต้นและท้ายของ string (RFC)

str_starts_with('haystack', 'hay'); // true
str_ends_with('haystack', 'stack'); // true

ฟีเจอร์อื่นของ PHP 8.0 ดูต่อได้ใน release note

ที่หยิบมาเป็นแค่ส่วนที่ผมคิดว่ามีผลกับวิธีเขียนโค้ด PHP ของเรามากที่สุด PHP 8.0 ยังมีอีกหลายเรื่อง เช่น JIT, การเทียบ string กับตัวเลขที่เปลี่ยนไป และ breaking change จาก function ที่ deprecated มาตั้งแต่ PHP 7.x ดูรายละเอียดทั้งหมดได้ที่ PHP 8.0 Release Announcement และบทความ What's new in PHP 8

อ่านต่อ

ล่าสุด

ดูทั้งหมด →