こちらの記事は PHP Advent Calendar 2023 の 8 日目の記事です。


今回紹介しているコードはこちらにも記載しています。
https://github.com/noritakaIzumi/codeigniter-validation-extended/tree/20231118/Validator

CodeIgniter のバリデーションルールは通常、

$validation->setRule('username', 'Username', 'required|max_length[30]|min_length[3]');

のように文字列で指定されますが、

$validation->setRule('username', 'Username', 'requaired|max_langth[3O]|mln_length[3]');
                                                  ^          ^      ^   ^

のように設定を間違えていた場合、実際に動作させるまでミスに気づくことができません。 (テストコードを書いていれば別ですが、ここでは書いていないものとします)

そこで、バリデーションルールを生成するためのクラスを用意し、ルール名やパラメータを間違えた場合にエディタの静的解析で気付くための方法を考えます。
また、エディタの自動補完もうまく使えるようにします。

環境

  • CodeIgniter 4.4.3
  • PHP 8.2.12

実装

目指す形

クラスを使って生成したルールを変換したときに、 Setting Custom Error Messages のようにフィールドラベルの付いた連想配列が生成されるのを目標とします。
rules はパイプ区切りの文字列もしくは配列で指定できますが、今回は配列で生成することとします。

$rules = [
    'username' => [
        'label'  => 'Username',
        'rules'  => ['required', 'max_length[30]', 'min_length[3]'],
        'errors' => [
            'required' => 'All accounts must have {field} provided',
        ],
    ],
    'password' => [
        'label'  => 'Password',
        'rules'  => ['required', 'max_length[255]', 'min_length[8]', 'alpha_numeric_punct'],
        'errors' => [
            'min_length' => 'Your {field} is too short. You want to get hacked?',
        ],
    ],
];

開発イメージ

各フィールドは Field クラスを用意し、コンストラクタで label, rules, errors を引数に取るのが良いかもしれません。
しかし、errors の各キーは rules で指定されるものに依存するので、エラーメッセージ込みのルール設定を渡せるようにします。

rules 向けに FieldRules クラスを用意し、インスタンスを生成した後で各ルールをメソッドチェーンで追加できるようにします。

Field クラスに export メソッドを用意し、目指している連想配列を出力できるようにします。

すなわち、以下のような書き方で実装できるものとします。

# username
$usernameFieldRules = new FieldRules();
$usernameFieldRules
    ->required(message: 'All accounts must have {field} provided')
    ->maxLength(30)
    ->minLength(3);
$usernameField = new Field(name: 'username', label: 'Username', rules: $usernameFieldRules);

# password
$passwordFieldRules = new FieldRules();
$passwordFieldRules
    ->required()
    ->minLength(8, message: 'Your {field} is too short. You want to get hacked?')
    ->maxLength(255)
    ->alphaNumericPunct();
$passwordField = new Field(name: 'password', label: 'Password', rules: $passwordFieldRules);

$rulesCreator = new RulesCreator();
$rulesCreator
    ->addField($usernameField)
    ->addField($passwordField);

$validation = \Config\Services::validation();
$validation->setRules($rulesCreator->export());

FieldRules クラス

FieldRules クラスには各ルール名に対応したメソッドを用意します。
ここでは例として minLength メソッドを実装します。

class FieldRules
{
    protected array $rules = [];
    protected array $errors = [];

    public function getRules(): array
    {
        return $this->rules;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    // min_length ルールはしきい値となる文字数が必要なので、int 型の引数を用意します。
    public function minLength(int $length, string $message = ''): static
    {
        $this->rules[] = "min_length[$length]";
        if ($message !== '') {
            $this->errors['min_length'] = $message;
        }
        // メソッドチェーンに対応させるため、$this を返します。
        return $this;
    }
}

他のメソッドも同様に実装します。

Field クラス

Field クラスは name, label のほかに先ほど作成した FieldRules クラスのインスタンスを持たせた value object とします。

readonly class Field
{
    /**
     * @param string $name
     * @param string $label
     * @param FieldRules $rules
     */
    public function __construct(public string $name, public string $label, public FieldRules $rules)
    {
    }
}

RulesCreator クラス

RulesCreator クラスには addField メソッドを用意して各フィールドの情報を持てるようにします。
また、CodeIgniter のバリデーターに渡せる形に変換する export() メソッドも用意します。

class RulesCreator
{
    /**
     * @var Field[]
     */
    protected array $fields = [];

    public function addField(Field $field): static
    {
        $this->fields[] = $field;
        return $this;
    }

    public function export(): array
    {
        $rules = [];
        foreach ($this->fields as $field) {
            $rule = [
                'label' => $field->label,
                'rules' => $field->rules->getRules(),
            ];
            $errors = $field->rules->getErrors();
            if ($errors !== []) {
                $rule['errors'] = $errors;
            }

            $rules[$field->name] = $rule;
        }

        return $rules;
    }
}

実行

以下のようにして実行することができます。

$validation = \Config\Services::validation();
$validation->setRules($rulesCreator->export());
if (! $validation->run($data)) {
    // handle validation errors
}

まとめ

今回のようにフレームワークをカスタマイズすることは、開発スピードの改善にもつながりますし、エディタの機能と組み合わせて実装ミスなどを減らすことにもつながります。 その一方で、フレームワークを熟知していない人にとっては、どれがカスタマイズした機能でどれがフレームワーク純正の機能なのか、分かりにくくなることもあります。 開発の心得やマニュアル・環境などを整備し、多様なエンジニアが効率的な開発をできるように工夫することが必要だと思います。