🙆‍♀️

PHPのAttributeでバリデーションを実装してみた

2022/03/19に公開

PHPのAttributeでバリデーションを実装してみた

今更ながら、PHPのAttributeを使って何か作ってみたいと思い、オブジェクトのプロパティの値をバリデーションするパッケージを作成してみました。

完成したパッケージは、以下の通りです。
https://github.com/takemo101/simple-prop-check
https://packagist.org/packages/takemo101/simple-prop-check

主に、EntityやDTOやValueObjectの値をバリデーションするのに利用できるかと思います。

Javaのアノテーションを参考にしました

まず、Attributeで何を作ろうかと考えたときに、真っ先に思いついたのは、JavaのフレームワークSpringのアノテーションで行うバリデーションでした。

前々からあのバリデーションは便利だと思っていたので、プロパティの値をパリデーションするAttributeを使ったライブラリを作成することにしました。

Attributeについての説明は、以下をお読みください。
https://www.php.net/manual/ja/language.attributes.overview.php

使い方

まずはcomposerでインストールします。

composer require takemo101/simple-prop-check

インストールが完了したら、とりあえずEntityクラスを作成します。

<?php

class User {
	public function __construct(
		// ユーザーの名前
		private string $name,
		// ユーザーのメールアドレス
		private string $email,
		// ユーザーの年齢
		private int $age,
	) {}
}

このままだと、クラスのプロパティである名前や年齢として無効な値が入ってしまいます、、、
そこで、今回作成したパッケージの出番です!
プロパティに入力する値のルールを設定して、バリデーションで無効な値が入らないようにします。

プロパティのバリデーションのルールは、プリセットとしていくつか用意しているので、そのルールをAttributeクラスで設定します。

<?php

use Takemo101\SimplePropCheck\Preset\String\ {
    Email,
    MaxLength,
};
use Takemo101\SimplePropCheck\Preset\Numeric\Between;
use Takemo101\SimplePropCheck\Preset\NotEmpty;
use Takemo101\SimplePropCheck\PropCheckFacade;

class User {
	public function __construct(
		// 名前は60文字まで有効とするルール AND 空文字を無効とするルール
		#[NotEmpty]
		#[MaxLength(60)]
		private string $name,
		// メールアドレスはアドレス形式を有効とするルール AND 空文字を無効とするルール
		#[NotEmpty]
		#[Email]
		private string $email,
		// 年齢は10から200歳までを有効とするルール
		#[Between(10, 200)]
		private int $age,
	) {
		//
	}
}

// PropCheckFacadeのcheckメソッドにオブジェクトを渡すことでバリデーションを行い、
// プロパティのバリデーションルールに違反していない場合は true を返し、
// プロパティのバリデーションルールに違反している場合は false を返します。
$result = PropCheckFacade::check(new User(
	'名前',
	'xxx@xxx.com',
	8, // 年齢を8歳としているので、ルール違反となり false を返す
));

var_dump($result); // 結果 bool(false) となる

// バリデーション結果を真偽値ではなく例外として返したい場合は、
// PropCheckFacadeのexceptionメソッドにオブジェクトを渡します。
PropCheckFacade::exception(new User(
	'名前',
	'xxx@xxx.com',
	12, // 年齢を12歳としているのでルール違反とはならないので、例外は発生しない
));

以上のようにプロパティの有効性をチェックすることができます。
ただ、実際開発でこれを利用するとなると、Entityクラスのコンストラクタで、PropCheckFacadeのexceptionメソッドに$thisを渡してバリデーションするなどの工夫が必要になります。

あと、stringやintなどのプリミティブな型だけではなく、オブジェクトや配列などのバリデーションにも対応しています。

<?php

use Takemo101\SimplePropCheck\Preset\Array\MaxSize;
use Takemo101\SimplePropCheck\Preset\NotEmpty;
use Takemo101\SimplePropCheck\{
    PropCheckFacade,
    AfterCall,
    Effect,
};

// バリデーション後に呼び出すメソッド名
#[AfterCall('initialize')]
class Company
{
    /**
     * コンストラクタ
     *
     * @param string $companyName
     * @param Member[] $members
     */
    public function __construct(
        #[NotEmpty]
        private string $companyName,
        // Effectを設定することで
	// オブジェクトやオブジェクト配列を全てバリデーションできる
        #[Effect]
        // 会社メンバーは最大3名までを有効とするルール
        #[MaxSize(3)]
        private array $members,
    ) {
        // コンストラクタに例外が発生するバリデーションチェックを仕込む
        PropCheckFacade::exception($this);
    }

    /**
     * バリデーション完了後に呼び出される初期化メソッド
     *
     * @return void
     */
    private function initialize()
    {
        // 初期化処理
    }
}

class Member
{
    public function __construct(
        // 空文字を無効とするルール
        #[NotEmpty]
        private string $name,
    ) {
        //
    }
}

// バリデーションルールに違反していないので例外は起こらない
$company = new Company(
    '会社名',
    [
	new Member('メンバー名'),
	new Member('メンバー名'),
	new Member('メンバー名'),
    ],
);

// メンバーの最大人数のルールに違反しているので例外が発生する
$company = new Company(
    '会社名',
    [
	new Member('メンバー名'),
	new Member('メンバー名'),
	new Member('メンバー名'),
	new Member('メンバー名'),
    ],
);

// 一部メンバーの名前が空でルールに違反しているので例外が発生する
$company = new Company(
    '会社名',
    [
	new Member('メンバー名'),
	new Member(''),
    ],
);

以上のように、Attributeを利用することで、プロパティのバリデーション処理をサクッと実装できるようになります(他にもオリジナルルールを作成したり、色々なカスタマイズもできますが、ここでは割愛します)
Attribute便利です!

Attributeを使ってみての感想

Attributeを使うことで、色々なサポート処理を隠蔽することができるので便利だなーと思った反面、リフレクションを多用することになり、その事により飛び道具的な実装が可能になるので、どこまでの機能や処理をAttributeで実装するかのバランスが難しいなと思った次第です。

ただ、アイディア次第では色々な使い方が出来そうなので、今後もガンガンAttributeを利用していくことになりそうです。

Discussion