📘

いまさらPHP 8 の新機能を見てみる①

2021/08/31に公開約5,100字

PHP 8 について何も知らないのでいまさらですが、確認してみました。
たくさんあったので、今回は一部のみです。

タイトルに①とありますが、やる気があれば続けます。
多分②はないでしょう。

ドキュメント

https://www.php.net/manual/ja/migration80.new-features.php

名前付き引数

名前ベースで引数を渡すことができます。
引数の順番を意識する必要がなくなります。

function foo($a, $b, $c) {
    echo $a . PHP_EOL;
    echo $b . PHP_EOL;
    echo $c . PHP_EOL;
}

echo foo(b: 1, a: 2, c: 3);
# 2
# 1
# 3

// 位置を指定した引数が名前付き引数より前にあればOK
echo foo(2, c: 3, b: 1);

// これはエラー
echo foo(b: 1, a: 2, 3);
# PHP Fatal error:  Cannot use positional argument after named argument in php shell code on line 1

アトリビュート

コードにメタデータの情報を埋め込むことができます。
トリビュートで定義したメタデータは実行時にリフレクションAPIを使って調べることができます。
小難しいのでサンプルで確認します。

class SetUp {}

#[Attribute]
class Foo 
{
    public string $name;
    public int $age;

    #[SetUp]
    public function required()
    {
        if (!isset($this->name))
        {
            throw new RuntimeException("nameが未設定");
        }

        if (!isset($this->age))
        {
            throw new RuntimeException("ageが未設定");
        }
    }

    public function bar()
    {
        echo "name: {$this->name}, age: {$this->age}" . PHP_EOL;
    }
}

function executeAction(Foo $foo)
{
    $reflection = new ReflectionObject($foo);

    foreach ($reflection->getMethods() as $method) {
        $attributes = $method->getAttributes(SetUp::class);

        if (count($attributes) > 0) {
            $methodName = $method->getName();

            $foo->$methodName();
        }
    }

    $foo->bar();
}

$foo = new Foo();
$foo->name = "太郎";
$foo->age = 35;

executeAction($foo);
# name: 太郎, age: 35

$foo = new Foo();
$foo->age = 35;

executeAction($foo);
# PHP Warning:  Uncaught RuntimeException: nameが未設定 in php shell code:12
# Stack trace:
# #0 php shell code(11): Foo->required()
# #1 php shell code(1): executeAction(Object(Foo))
# #2 {main}
#   thrown in php shell code on line 12

$foo = new Foo();
$foo->name = "太郎";

executeAction($foo);
# PHP Warning:  Uncaught RuntimeException: ageが未設定 in php shell code:17
# Stack trace:
# #0 php shell code(11): Foo->required()
# #1 php shell code(1): executeAction(Object(Foo))
# #2 {main}
#   thrown in php shell code on line 17

リフレクションAPIを使ってメタデータを調べています。#[SetUp]のアトリビュートのとこが動いてます。
フレームワークとして上手に利用できると実装が楽になりそうですね。

コンストラクタのプロパティ昇格機能

コンストラクタの引数を 対応するオブジェクトのプロパティに昇格させることができます。要は短縮記法です。

// PHP 7以前の書き方
class HogeOld {
    private int $id;
    private string $name;
    public function __construct(int $id, string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
    public function getId(): int
    {
        return $this->id;
    }
}

// PHP 8の新機能
class HogeNew
{
    // コンストラクタの引数にアクセス修飾子を定義
    // public や protected も可能
    public function __construct(private int $id, private string $name)
    {
    }
    public function getId(): int
    {
        return $this->id;
    }
}

$hogeNew = new HogeNew(1, 'a');
echo $hogeNew->getId();
# 1

union 型

型を union として宣言すると、ひとつではなく、 複数の異なる型を値として受け入れることができます。 union 型は、T1|T2|... という文法を使って指定します。

function a(string|int $n): string|int
{
    return ($n % 2) === 0 ? (string) $n : (int) $n;
}
var_dump(a(1));
# int(1)
var_dump(a(2));
# string(1) "2"

// nullが指定できる
// ?int と同義
function b(int $n): int|null
{
    return ($n % 3) === 2 ? null : $n;
}
var_dump(b(0));
# int(0)
var_dump(b(1));
# int(1)
var_dump(b(2));
# NULL

// false疑似型
// function c(): false はダメ
// 他にも false|null, ?falseもダメ
function c($s): string|false
{
    return gettype($s) === 'string' ? $s : false;
}
var_dump(c('a'));
# string(1) "a"
var_dump(c(1));
# bool(false)

match 式

match 式は、値の一致をチェックした結果に基づいて評価結果を分岐します。switch 文と似ていますが、match 式は複数の候補と比較される制約式を持ちます。switch 文とは異なり、三項演算子のように値を評価します。switch 文とは異なり、弱い比較(==)ではなく、型と値の一致チェック(===)に基づいて行われます。

5割ぐらいはswitch文です。型と値の一致チェック(===)に基づいて行われます がポイントなのと、各条件の処理が1行しか書けないところが大きいかなと思いました。

$x = 1;

$result = match($x) {
  "1" => "stringの1です",
  1 => "intの1です",
};

echo $result;
# intの1です

// 条件不一致の場合はエラーになります
$x = 2;

$result = match($x) {
  "1" => "stringの1です",
  1 => "intの1です",
};
# PHP Warning:  Uncaught UnhandledMatchError: Unhandled match value of type int in # php shell code:1
# Stack trace:
# #0 {main}
#   thrown in php shell code on line 1

nullsafe 演算子

PHP 8.0.0 以降では、プロパティやメソッドは "nullsafe" 演算子 ?-> を使ってアクセスすることもできます。 nullsafe 演算子は既に述べたメソッドやプロパティと同じように振る舞いますが、 オブジェクトが null と評価された場合に、例外はスローされず、 null が返される点だけが異なります。 オブジェクトの評価がチェインの一部だった場合は、 残りのチェインはスキップされます。

この演算子の働きは、 オブジェクトにアクセスするたびに is_null() でラップするコードに似ています。 しかし、よりコンパクトです。

PHPのマニュアルにあるコードが分かりやすいのでそのまま引用します。

https://www.php.net/manual/ja/language.oop5.basic.php#language.oop5.basic.nullsafe
// PHP 8.0.0 以降は、このように書けます:
$result = $repository?->getUser(5)?->name;

// これは、以下のコードチェックと同等です:
if (is_null($repository)) {
    $result = null;
} else {
    $user = $repository->getUser(5);
    if (is_null($user)) {
        $result = null;
    } else {
        $result = $user->name;
    }
}

使い方によってはすごくいいシンプルになると思います。
ただ、$result = $repository?->getUser(5)?->name; の後にnullチェックするぐらいなら、この機能を使わずに書く方が読みやすい気がします。うーん、本当かなー。どうだろう。

終わり

Discussion

ログインするとコメントできます