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

本稿では、こうした多重度が1以上のモデルを、PHPでいかに型安全1にコードに落とし込んだらいいか、その裏ワザを紹介する。
なお、本稿で提示するPHPのサンプルコードの完全版はGitHubで公開しているので、必要があれば参照していただきたい。
オーソドックスな多重度制約の表現方法
今回紹介したい内容に行く前に、多重度制約を表現するオーソドックスな実装方法を見ておこう。
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
の中身の型をチェックしたほうがいいだろう。
foreach ($recipients as $recipient) {
if (!$recipient instanceof Recipient) {
throw new \InvalidArgumentException(
'Element of $recipients must be instance of Recipient'
);
}
}
オーソドックスな手法の問題点
オーソドックスな方法に問題点があるとしたら、多重度を表現、保証しようとすると次のような面倒くささが出てくることだろう。
- ガード条件をゴリゴリ書かないとならない。2
- 書いたガード条件はテストする必要がある場合もある。
- 型宣言が
array
になるため、phpdocでドキュメントを書く手間がある。
多重度[1..*]制約を手軽かつ型安全に表現する方法
オーソドックスな方法の面倒さを確認できたので、本題の実装方法を見てみよう。配列であっても型宣言する方法として、以前『ジェネリクスがないPHPでも配列中身のタイプヒントを可能にする「Splat Operator」』という手法を紹介したが、多重度を表現する場合にも応用することができる。例えば次のコードだ:
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
するときは次のように配列変数のあたまに...
をつける:
$recipients = [new Recipient(), new Recipient(), new Recipient()];
new Message(...$recipients);
もしも$recipients
が空の配列だった場合、つまり、「受信者が1以上」という多重度制約に反した場合は、ArgumentCountError
になる:
<?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つ以上の趣味があるといったモデルを想定してみよう。

これも多重度1の例で実装したように、可変長引数とSplat Operatorを組み合わせて書くことができる:
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と本稿で紹介した方法は相性がいい。メソッドの引数が足りない、つまり、多重度を満たしてないときはエディタ上で警告がでるので、クライアントコードのコーディング中に問題に気がつけるメリットがある:

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

まとめ
多重度の数に応じて引数を作り、可変長引数も組み合わせることで、多重度1以上、多重度N以上を手軽かつ型安全に実装することができる。
-
静的言語でコンパイル時にプログラムの不整合が型情報から検出できるというのが型安全の典型かと思いますが、PHPではコンパイルが無いので、せめて実行時に未定義の動作にならないよう型宣言でその可能性を潰すという意味合いで「型安全」としています。 ↩
-
webmozart/assertというアサーションライブラリを使うとガード条件は素のPHPよりも宣言的に書ける。 ↩