PHP:手軽なのに型安全。多重度[1..*]の制約を実装する裏ワザ

オブジェクト指向するとき、「多重度が1以上」の制約を設計で想定することがある。例えば、「メッセージ」には「受信者」がいるが、「受信者は最低1名」という制約をクラス設計(UMLのクラス図)で表現すると次にのようになる。`recipients`は`Message`クラスのメンバで、`1..*`が多重度1以上を意味する。

<img width="776" alt="Astah_-__no_title_____.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/2354ff76-25ad-6ac2-8b5a-8179440d6167.png">

本稿では、こうした多重度が1以上のモデルを、PHPでいかに型安全[^型安全]にコードに落とし込んだらいいか、その裏ワザを紹介する。

[^型安全]: 静的言語でコンパイル時にプログラムの不整合が型情報から検出できるというのが型安全の典型かと思いますが、PHPではコンパイルが無いので、せめて実行時に未定義の動作にならないよう型宣言でその可能性を潰すという意味合いで「型安全」としています。

なお、本稿で提示するPHPのサンプルコードの完全版は[GitHub](https://github.com/suin/php-playground/tree/master/TypeSafeMultiplicity)で公開しているので、必要があれば参照していただきたい。

## オーソドックスな多重度制約の表現方法

今回紹介したい内容に行く前に、多重度制約を表現するオーソドックスな実装方法を見ておこう。

```php:Message.php
final class Message
{
    /**
     * @var Recipient[]
     */
    private $recipients;

    /**
     * @param Recipient[] $recipients
     */
    public function __construct(array $recipients)
    {
        if (count($recipients) < 1) {
            throw new InvalidArgumentException(
                'Recipients must be at least one'
            );
        }
        $this->recipients = $recipients;
    }
}
```

`Message`クラスのコンストラクタで`$recipients`を配列で受け取るようにする。それだけだと、空っぽの配列が渡されても動いてしまうので、多重度を保証するためにガード条件として`count($recipients) < 1`の表明を追加する。もし、多重度の制約にひっかかるようであれば、例外`InvalidArgumentException`が投げられるようなしかけにする。

`array`の中身は型宣言できないので、phpdocの`@param`アノテーションで`$recipients`は`Recipient[]`であることを説明する必要がある。もっと厳密なコードにするなら、`$recipients`の中身の型をチェックしたほうがいいだろう。

```php
foreach ($recipients as $recipient) {
    if (!$recipient instanceof Recipient) {
        throw new \InvalidArgumentException(
            'Element of $recipients must be instance of Recipient'
        );
    }
}
```

### オーソドックスな手法の問題点

オーソドックスな方法に問題点があるとしたら、多重度を表現、保証しようとすると次のような面倒くささが出てくることだろう。

* ガード条件をゴリゴリ書かないとならない。[^webmozart/assert]
* 書いたガード条件はテストする必要がある場合もある。
* 型宣言が`array`になるため、phpdocでドキュメントを書く手間がある。

[^webmozart/assert]: [webmozart/assert](https://github.com/webmozart/assert)というアサーションライブラリを使うとガード条件は素のPHPよりも宣言的に書ける。

## 多重度[1..*]制約を手軽かつ型安全に表現する方法

オーソドックスな方法の面倒さを確認できたので、本題の実装方法を見てみよう。配列であっても型宣言する方法として、以前『[ジェネリクスがないPHPでも配列中身のタイプヒントを可能にする「Splat Operator」](https://qiita.com/suin/items/781c35edc32aaafac01e)』という手法を紹介したが、多重度を表現する場合にも応用することができる。例えば次のコードだ:

```php
final class Message
{
    /**
     * @var Recipient[]
     */
    private $recipients;

    public function __construct(Recipient $recipient, Recipient ...$recipients)
    {
        \array_unshift($recipients, $recipient);
        $this->recipients = $recipients;
    }
}
```

コンストラクタに着目してほしい。コンストラクタには引数が2つある。1つ目は`Recipient`型の値を受け付ける。2つ目はSplat Operator(可変長引数)を使って0以上の`Recipient`を受け付ける。どちらも型宣言が`array`ではなく`Recipient`クラスになっている点が、オーソドックスな手法と大きく異なる。

この型宣言があるため、オーソドックスな方法では`foreach`で`$recipients`の中身の型をチェックしていたが、その手間はPHPが負ってくれるわけだ。

`Message`クラスを`new`するときは次のように配列変数のあたまに`...`をつける:

```php
$recipients = [new Recipient(), new Recipient(), new Recipient()];
new Message(...$recipients);
```

もしも`$recipients`が空の配列だった場合、つまり、「受信者が1以上」という多重度制約に反した場合は、`ArgumentCountError`になる:

```php:test.php
<?php

$recipients = [];
new Message(...$recipients); // PHP Fatal error:  Uncaught ArgumentCountError:
                             // Too few arguments to function Message::__construct(),
                             // 0 passed in .../test.php on line 4 and exactly 1
                             // expected in .../Message.php:14

```

オーソドックスな方法では`count($recipients) < 1`のガード条件で多重度[1..*]を保証していたが、そのチェックもPHPがしてくれるようになる。

## 多重度2、多重度3以上はどう実装する？

ここまで多重度1以上を例に見てきたが、多重度2や多重度3以上の場合などにも応用可能だ。例えば、プロフィールには必ず3つ以上の趣味があるといったモデルを想定してみよう。

<img width="752" alt="Astah_-__no_title_____.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/b866b458-f595-f6af-8df1-47828d4b18cd.png">

これも多重度1の例で実装したように、可変長引数とSplat Operatorを組み合わせて書くことができる:

```php:Profile.php
final class Profile
{
    /**
     * @var Hobby[]
     */
    private $hobbies;

    public function setHobbies(
        Hobby $hobby1,
        Hobby $hobby2,
        Hobby $hobby3,
        Hobby ...$hobbies
    ): void {
        $this->hobbies = func_get_args();
    }
}
```

多重度が高い場合は、引数が多くなるので、`func_get_args()`を使うとコード量も抑えられる。

## PhpStormとの相性もいい

ちなみに、PhpStormと本稿で紹介した方法は相性がいい。メソッドの引数が足りない、つまり、多重度を満たしてないときはエディタ上で警告がでるので、クライアントコードのコーディング中に問題に気がつけるメリットがある:

<img width="612" alt="php-playground___Volumes_dev_php-playground__-_____TypeSafeMultiplicity_TypeSafe_Profile_php__php-playground_.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/516e7ed9-1c2e-cfac-f0ed-2eaaded71502.png">

## 可変長引数の制約を突破したいときはファーストクラスコレクションを検討しよう

なお、可変長引数は最後の引数にしか用いられないというPHPの言語上の制約があるのには注意が必要だ。これが本稿で紹介した方法の欠点ではあるが、最後の引数でなくても多重度を保証したい場合は、ファーストクラスコレクションを作るといいだろう。

`Recipients`クラスはファーストクラスコレクション:

<img width="1191" alt="Astah_-__no_title_____.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/33f8eb65-cba9-677c-0792-cae6d296a0a5.png">

## まとめ

多重度の数に応じて引数を作り、可変長引数も組み合わせることで、多重度1以上、多重度N以上を手軽かつ型安全に実装することができる。