2021年12月17日

PHP 8.1 來了,新功能與特性快速介紹

在 PHP 8.0 發佈一年後,PHP 8.1 在今年 2021 年 11 月 25 日也發佈了。

去年我曾寫了一篇文章,介紹 PHP 8.0,而過去這一整年,我在開發上也實際使用了不少 PHP 8.0 的新功能,例如 Nullsafe operatormatchConstructor property promotion 。

這些改進跟新功能,替開發帶來不少方便跟效率。我相信這次的 8.1 版本,PHP 團隊也會帶來不少令人驚豔的地方吧!

本篇一樣會介紹一些我感興趣的新功能,搭配一些簡單的試玩。大家可以參考看看。

PHP 8.1 新功能介紹

Enumerations 枚舉

以前要列舉一些狀態,只能利用類別 Class 搭配常數 const 使用,但是新的 Enumerations 改善了這個情況。

過去使用類別↓

class Color
{
    const RED = 'RED';
    const BLUE = 'BLUE';
    const GREEN = 'GREEN';
}

function setColor(string $color) 
{
    print $color;
}

現在利用枚舉↓

enum Color
{
    case RED;
    case BLUE;
    case GREEN;
}

function setColor(Color $color) 
{
    print $color->name;
}

使用 enum 定義一些值,這些值可以被歸納成同一個類型,之後只要指定傳入參數為這個類型的 enum ,就可以透過 PHP 來幫你把關。

setColor(Color::RED);
// ok

setColor('RED');
// PHP Fatal error:  
// Uncaught TypeError: setColor(): 
// Argument #1 ($color) must be of type ColorEnum, string given

不過這個乍看很簡單的新功能,再我稍微讀了一下 Enumerations 的文件後,才發現水還蠻深的,有興趣的人可以看看。

Readonly Properties 唯讀屬性

class Student 
{
    public readonly string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
    
    public function setName($newName) 
    {
        $this->name = $newName;
    } 
}

只能在建構子 __construct 中決定 readonly 的值。一旦初始化了就不能再改,不論是在內部或外部。

$s = new Student('Tom');
$s->name = 'Jack';
// PHP Fatal error:  
// Uncaught Error: Cannot modify readonly property Student::$name

$s->setName('Jack');
// PHP Fatal error:  
// Uncaught Error: Cannot modify readonly property Student::$name

readonly 是執行期決定的,同個類別中的每個物件可以有不一樣的值。但是 const 是定義的時候就寫好的,同個類別下個每個物件看到的都會一樣。

要注意的是,readonly 不能設定預設值,因為這樣就等同於 const。而目前也不支援 static

另外,可能是因為新的特性的關係,所以比較嚴格,readonly 被規定只能用在 typed properties,也是必須要明確定義它的型別,如範例 public readonly string $name中 的 string

First-class Callable Syntax

這個東西有點微妙。有一種開發上的特殊需求是把 callable 轉換成 Closure

比如說,strlen 是 PHP 內建的函式,它是一種 callable ,在過去要把它轉換成 Closure 時,需要透過 Closure::fromCallable ,但現在可以透過直接在函式名稱後面加上 (...) 的方式,來達到一樣的效果。

$strlenClosure = Closure::fromCallable('strlen');
var_dump($strlenClosure);
$strlenClosure = strlen(...);
var_dump($strlenClosure);

兩個的輸出都是:

object(Closure)#1 (1) {
  ["parameter"]=>
  array(1) {
    ["$string"]=>
    string(10) "<required>"
  }
}

有點微妙吧,一般人可能也不太有把 callable 轉成 Closure 的需求。不過我在查資料的時候找到這篇文章,它說明了需要轉換的情境,我覺得還不錯,有興趣的可以參考看看:The new Closure::fromCallable() in PHP 7.1

New in initializers

這也是一個增加便利性的功能,以往在參數的預設值,沒辦法用物件,因為物件需要透過 new 來實體化。但是從 8.1 開始可以了。

class Cellphone
{
    public function __construct(
        protected string $type 
    ) {}
}

class Student
{
    public function __construct(
        protected Cellphone $cellphone = new Cellphone('iPhone')
    ) { }

    public function setCellphone(
        Cellphone $cellphone = new Cellphone('ZenFone')
    ) {
        $this->cellphone = $cellphone;
    }
};

在沒有傳入任何參數的情況下,就會直接使用預設的 new Cellphone('iPhone')

$student = new Student();

另外也特別測試了一下在非建構子的 function 能不能使用 new,答案是可以。

$student->setCellphone();

使用 setter 卻給預設值,是有點怪,不過這只是為了實驗,請大家將就一下。setter 的合理使用方式是這樣:

$student->setCellphone(new Cellphone('Google Pixel'));

Pure Intersection Types 純交集類別

(這個翻譯有點怪XD 不是很確定 pure 在這邊怎麼翻比較好)

各位不曉得還記不記得 PHP 8.0 新增了 Union Type,聯集類型,只要符合其中一個都可以。而交集類型呢,則必須要都符合才可以。

interface Interface_1 {};
interface Interface_2 {};
class A implements Interface_1, Interface_2{};
class B implements Interface_1{};

function test(Interface_1&Interface_2 $foo) {}

透過 & 來把所有需要符合的類型都串接起來。

test(new A); 
// ok

test(new B); 
// PHP Fatal error:  Uncaught TypeError: test(): 
// Argument #1 ($foo) must be of type Interface_1&Interface_2, B given

雖然有點瘋狂,但想要串接幾個都可以。

interface Interface_1 {};
interface Interface_2 {};
interface Interface_3 {};
interface Interface_4 {};
class A implements Interface_1, Interface_2, Interface_3 {};

function test(Interface_1&Interface_2&Interface_3&Interface_4 $foo) {}
test(new A);

我認為這個新特性最大的好處就是,我們可以不用在類別內在透過 instanceof 的方式,一個一個檢查必須要符合的類別了。

Never return type 從不返回類型

又是一個怪怪的翻譯。never return type 用來宣告 function 的回傳值,表示這個 function 不會 return ,可能是離開了,像是呼叫了 exit() 、拋了錯誤,或是一個無窮迴圈。

實際測試,這樣的情況會跳出錯誤訊息:

function testNever(): never {
    echo 'hello world';
}

testNever();
// PHP Fatal error:  
// Uncaught TypeError: testNever(): 
// never-returning function must not implicitly return

對,沒有呼叫 return 也是一種返回。必須要明確地離開才可以。

function testNever(): never {
    echo 'hello world';
    exit();
}

testNever(); // ok

Final class constants 最後類別常數

恩,一個很清楚明瞭的特性。常數宣告為 final 就不能被子類別複寫。

class A
{
    final public const TEST = "TEST_A";
}

class B extends A
{
    public const TEST = "TEST_B";
}

錯誤訊息:

PHP Fatal error:  B::TEST cannot override final constant A::TEST

Array unpacking support for string-keyed arrays

在 8.1 可以把以 string 作為 key 的陣列打開了。(unpacking 不曉得怎麼翻譯比較對,暫時翻譯成打開。)

所以根據官網範例,可以更簡單的做到陣列的合併。

$a = ['a' => 'Apple', 'b' => 'Banana'];
$b = ['b' => 'Melon', 'c' => 'Orange'];
$c = [...$a, ...$b]; 
// $C 等同於 ['a' => 'Apple', 'b' => 'Melon', 'c' => 'Orange'];

PHP 8.1 效能提升

前面介紹了一些 PHP 8.1 的新特性跟功能,除此之外,PHP 8.1 效能好像也提升了一點。

根據官網說明,相較於 8.0,8.1的測試結果:

  • Symfony Demo 加速了 23.0%
  • WordPress 加速了 3.5%

另外,秉持著實驗家精神,我自己也實測了一下 PHP 8.1 的速度。可以參考這篇文章:

PHP 8.1、8.0、7.4 效能實測結果,Laravel 8 速度提升了 15%

PHP 8.2 在 2022 年 12 月底釋出了,有興趣者可以前往這邊觀看。

參考連結

PHP 8.1 Released!