【PHP8】属性(attribute)を使ってみる

PHP

php8から使えるようになった属性(Attribute)を使ってみました。
C#などでは属性を使ったりみたことはあったのですが、あまりいちから書くことがなかったので、今回は属性を自作してみました。

属性を使って、2つのことを試してみました。
クラスに属性を付加して、付けた属性名とパラメータを取得しています。
– クラスのプロパティに必須チェックの属性を付けて、必須項目のチェックをしてみました。

今回作成したサンプルはgithubのこちらに置いています。
https://github.com/YasuakiHirano/poc-php8-docker/blob/master/attribute.php

属性とは..?(attribute)

PHP以外の言語を触ったことない人には属性は馴染みがないと思います。
他の言語をやっていても、書かなくてもプログラムを作ることは可能なので、プロジェクトによっては、あまり使用されていないこともあります。

属性はクラスやメソッド、関数、プロパティに追加情報を付加できるものです。
付加した情報はReflectionAPIを利用して、取得したり内容を変更したりすることができます。

クラスに付加されている属性の取得・引数を取得や、プロパティの値を取得することができます。

公式の概要はこちらになります。

属性を作成する

下記のような、簡単な属性を作成してみました。

Authorは作成者の情報の属性、Descriptionは説明の情報の属性です。
Requiredは必須項目のチェックのために作ってみました。

#[Attribute]
class Author {
    public $value;

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

#[Attribute]
class Description extends Author{

}

#[Attribute]
class Required {
    public $value;

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

属性はこのように#[Attribute]を付けたクラスで作成することができます。
値を設定するだけの属性です。

AutherとDescriptionはクラスに付けて、情報を取得するようにしてみます。
Requiredはクラスのプロパティに付加して、プロパティの値が空かチェックするようにしてみます。

作成した属性の使用

クラスに属性を付加する

Userというクラスを作りました。

#[Author("hirano"), Description("説明hogehoge")]
class User {
// ----- 省略 -----

クラスの頭に#から初めて、#[Author("hirano"), Description("説明hogehoge")]と書いている箇所が属性を設定しているところです。
複数属性を設定するには、カンマで区切るといいようです。
作者に名前を入れて、説明を適当に入れてみました。
属性のパラメータも複数渡すことができ、複数渡す場合はカンマで区切ります。

これを取得したいときは、下記のように取得します。

echo "----- class属性の取得開始 -----\n";
$reflection = new ReflectionClass(User::class);
foreach ($reflection->getAttributes() as $attribute) {
    if ($attribute) {
        echo $attribute->getName() . ":" . $attribute->getArguments()[0]."\n";
    }
}
echo "----- class属性の取得終了 -----\n\n";

ReflectionClassを使用します。
ReflectionClassgetAttributesメソッドを呼ぶと、設定している属性情報が取得できます。
ReflectionAttributeインスタンスが設定されている数、返ってきます。

複数返ってくるのでforeachでループして、$attributeで受けるようにしました。
設定値がある場合は、$attribute->getName()で設定されている属性名、$attribute->getArguments()で渡しているパラメータが取得できます。

属性のパラメータは複数渡されることがあるので、今回は[0]で最初に渡されたパラメータのみを取得しています。
こちらを実行すると、こうなります。

----- class属性の取得開始 -----
Author:hirano
Description:説明hogehoge
----- class属性の取得終了 -----

プロパティに属性を付加して使用する

Userクラスの解説

Userクラスを作成しましたが、全容はこんな感じです。
プロパティとして、年齢($age)・名前($name)・性別($gender)を用意しました。

年齢と名前は必須項目として、Required属性を付けてみました。

そして、validateメソッドを作成しました。
このvalidateメソッドを呼ぶと、自分のインスタンスの年齢・名前に値が入ってなかったらエラーメッセージを返すようにしました。

class User {
    #[Required]
    public ?int $age;
    #[Required]
    public ?string $name;
    public ?int $gender;

    function __construct(?int $age, ?string $name, ?int $gender)
    {
       $this->age = $age; 
       $this->name = $name; 
       $this->gender = $gender; 
    }

    /**
     * 必須項目が入っているかチェックする
     * @param void
     * @return array $errors エラーテキストの配列
     */
    public function validate() {
        $errors = [];
        $reflection = new ReflectionClass($this);
        $props = $reflection->getProperties();
        foreach ($props as $prop) {
            $attribute = $prop->getAttributes() ? $prop->getAttributes()[0] : null;

            if ($attribute && $attribute->getName() === 'Required') {
                $value = $prop->getValue($this);
                if (!$value) {
                    array_push($errors, $prop->getName() . 'には値を入れてください。');
                }
            }
        }

        return $errors;
    }
}

validateメソッドの内容としては、最初に自分のインスタンスのReflectionClassを取得します。

$reflection = new ReflectionClass($this);

$reflection->getProperties();を呼ぶとクラスのプロパティ情報が取得できます。
取得したプロパティをループで回しています。

ループの中で、プロパティからgetAttributesメソッドを呼んで、プロパティの属性情報を取得しています。
属性があって、Required属性がついていた場合、という判定をやっています。

if ($attribute && $attribute->getName() === 'Required') {

この場合は、必須属性が付いているプロパティなので、次の行で、getValueメソッドを呼んで値を取得します。
そして値が空だった場合は、$errorsにメッセージを格納して、返しています。

$value = $prop->getValue($this);
if (!$value) {
    array_push($errors, $prop->getName() . 'には値を入れてください。');
}

プロパティからgetNameメソッドを呼ぶと、プロパティ名を取得できます。

属性を使って必須チェック

実際に、このクラスを使用してチェックしてみました。
最初は引っかからないパターンです。全てのプロパティに値を設定しているので、エラーになりません。

echo "空チェック属性を使ったテスト\n";
echo "----- test1 start -----\n";
$user = new User(19, 'yamada', 1);
$errors = $user->validate();
if ($errors) {
    var_dump($errors);
}
echo "----- test1 end -----\n\n";

出力すると、こうなります。
エラーが出力されていないですね。

----- test1 start -----
----- test1 end -----

次に名前と性別の値が空のパターンです。
名前は必須属性を付けているのでエラーになります。

echo "----- test2 start -----\n";
$user = new User(30, null, null);
$errors = $user->validate();
if ($errors) {
    var_dump($errors);
}
echo "----- test2 end -----\n\n";

出力すると、こうなります。
名前がエラーになっていることが確認できました。

----- test2 start -----
array(1) {
  [0]=>
  string(40) "nameには値を入れてください。"
}
----- test2 end -----

最後に年齢と名前が空のパターンです。
どちらも必須属性を付けているのでエラーになります。

echo "----- test3 start -----\n";
$user = new User(null, null, 1);
$errors = $user->validate();
if ($errors) {
    var_dump($errors);
}
echo "----- test3 end -----\n";

出力すると、こうなります。
2つの項目がエラーになって、返ってきました。

----- test3 start -----
array(2) {
  [0]=>
  string(39) "ageには値を入れてください。"
  [1]=>
  string(40) "nameには値を入れてください。"
}
----- test3 end -----

終わりに

使ってみましたが、適切な使いどころがいまいち…

今回サンプルで書いたような、クラスのプロパティに対して一括で何か処理したい時などに使う感じですかね…🤔
一度処理を書いておけば、プロパティが増えた場合でも、属性を付加してあげるだけで、処理が簡単にできますね。

コメント

タイトルとURLをコピーしました