PHP5からはクラスと配列に限って引数の型を宣言できるようになり、PHP7からはstring
やint
などのスカラ型も型宣言できるようになった。これにより、契約によるプログラミングや防衛プログラミングがより一層しやすくなった。
一方で、array
は他の言語では配列、マップ、タプル、オブジェクトなどに細分化される型をどれも引き受けることができるため、array
を受け取る関数を実装する側の防衛プログラミングは少々複雑な記述が必要になる。
防衛されていない実装
整数(int
)の配列を受け取り、その合計値を計算する関数を想像してほしい。前述したとおり、array
はその中身を制約することができないため、int
以外の配列でも次のコードは動作してしまう。標準出力にWarningは出るがロジックが止まることはなく、0が計算結果になる。
// ノーガードな実装
function sum(array $numbers): int
{
$sum = 0;
foreach ($numbers as $number) {
$sum += $number;
}
return $sum;
}
assert(sum([1, 1, 1]) === 3); // 正常系
assert(sum(['a', 'b', 'c']) === 0); // Warning: A non-numeric value encountered
配列の中身を保証する方法
いくつかパターンがあるので紹介したい。
assertion
ini_set('assert.exception', '1');
// assertionでガードした実装
function sum(array $numbers): int
{
foreach ($numbers as $number) {
assert(is_int($number), '値は整数でなければなりません');
}
$sum = 0;
foreach ($numbers as $number) {
$sum += $number;
}
return $sum;
}
assert(sum([1, 1, 1]) === 3);
try {
sum(['a', 'b', 'c']);
} catch (AssertionError $e) {
assert($e->getMessage() === '値は整数でなければなりません');
}
assert
の使い勝手がPHP7で改良されるまではInvalidArgumentException
を投げる方法がよくあった。
Adderメソッド
class Numbers
{
/**
* @var int[]
*/
private $numbers;
/**
* @param int[] $numbers
*/
public function __construct(array $numbers)
{
foreach ($numbers as $number) {
$this->addNumber($number);
}
}
// adderがintでない型を検出する
private function addNumber(int $number): void
{
$this->numbers[] = $number;
}
public function sum(): int
{
return array_sum($this->numbers);
}
}
assert((new Numbers([1, 1, 1]))->sum() === 3);
array_walk
を使って簡略化することもできる。
class Numbers
{
...
public function __construct(array $numbers)
{
array_walk($numbers, [$this, 'addNumber']);
}
...
}
array_map + クロージャー
class Numbers
{
/**
* @var int[]
*/
private $numbers;
/**
* @param int[] $numbers
*/
public function __construct(array $numbers)
{
$this->numbers = array_map(
function(int $number) { // ここの型宣言で`int`じゃないものを検出できる
return $number;
},
$numbers
);
}
public function sum(): int
{
return array_sum($this->numbers);
}
}
可変長引数 + 型宣言
以前にジェネリクスがないPHPでも配列中身のタイプヒントを可能にする「Splat Operator」で取り上げた、可変長引数と型宣言を組み合わせる方法。最もシンプルに書けるが、他の方法と比べて使える状況が限られる。
class Numbers
{
/**
* @var int[]
*/
private $numbers;
/**
* @param int[] $numbers
*/
public function __construct(int ...$numbers)
{
$this->numbers = $numbers;
}
public function sum(): int
{
return array_sum($this->numbers);
}
}
assert((new Numbers(...[1, 1, 1]))->sum() === 3);