PHPにおける型付き配列の作成アプローチ
PHPでは型付き配列を定義することができません。どの配列も任意の値を含むことができるため、コードベースの一貫性を維持するのが難しくなります。ここでは、既存のPHP機能を使用してオブジェクトの型付きコレクションを作成するためのいくつかの代替策を紹介します。
問題を特定する
PHPの配列は非常に柔軟なデータ構造です。スカラー値から複雑なオブジェクトまで、好きなものを配列に追加できます:
$arr = [
"foobar",
123,
new DateTimeImmutable()
];
実際には、このように多様な範囲の値を持つ配列を実際に必要とすることは稀です。むしろ、配列には同じ種類の値が複数回含まれる可能性が高いです。
$times = [
new DateTimeImmutable(),
new DateTimeImmutable(),
new DateTimeImmutable()
];
次に、配列内のすべての値に作用するメソッドを作成することができます:
final class Stopwatch {
protected array $laps = [];
public function recordLaps(array $times) : void {
foreach ($times as $time) {
$this -> laps[] = $time -> getTimestamp();
}
}
}
このコードはを反復処理します
DateTimeInterfaceインスタンス内の
$times. 時間のUnixタイムスタンプ表現(整数として測定された秒数)は、その後に保存されます
$lapsこのコードの問題は、それが前提を置いていることです
$times全てから成り立っている
DateTimeInterface
インスタンス。これが事実であることを保証するものは何もないため、呼び出し元は混在する値の配列を渡すことができます。値の1つが実装されていなかった場合
DateTimeInterface, 呼びかけ
getTimestamp()違法であり、ランタイムエラーが発生するでしょう。
$stopwatch = new Stopwatch();
// OK
$stopwatch -> recordLaps([
new DateTimeImmutable(),
new DateTimeImmutable()
]);
// Crash!
$stopwatch -> recordLaps([
new DateTimeImmutable(),
123 // can't call `getTimestamp()` on an integer!
]);
可変引数を使った型の整合性の追加
理想的には、その問題は以下を明確に指定することで解決されるでしょう。
$times配列はただ含むことができます
DateTimeInterfaceインスタンス。PHPは型付き配列をサポートしていないため、代わりに他の言語機能を探さなければなりません。
最初の選択肢は可変引数を使用して展開することです。
$times渡される前の配列
recordLaps(). 可変長引数を使用すると、関数は不明な数の引数を受け入れることができ、それらは単一の配列として利用可能になります。私たちの使用ケースにおいて重要なのは、可変長引数を通常通り型指定できることです。渡される各引数は、指定された型でなければなりません。
可変長引数は、数学関数に一般的に使用されます。ここでは、与えられたすべての引数を合計する簡単な例を示します:
function sumAll(int ...$numbers) {
return array_sum($numbers);
}
echo sumAll(1, 2, 3, 4, 5); // emits 15
sumAll()配列が渡されていません。その代わりに、PHPは複数の引数を受け取り、それを結合します。
$numbers配列。その
intタイプヒントは各値が整数でなければならないことを意味します。これは保証として機能します。
$numbers整数のみで構成されます。これをストップウォッチの例に適用できます:
final class Stopwatch {
protected array $laps = [];
public function recordLaps(DateTimeInterface ...$times) : void {
foreach ($times as $time) {
$this -> laps[] = $time -> getTimestamp();
}
}
}
$stopwatch = new Stopwatch();
$stopwatch -> recordLaps(
new DateTimeImmutable(),
new DateTimeImmutable()
);
サポートされていない型を渡すことはもはや不可能です
recordLaps()そうしようとする試みは、はるかに早い段階で表面化することになります。
getTimestamp()コールが試みられています。
もしすでに渡すための時間の配列を持っているなら
recordLaps()それをスプラット演算子で展開する必要があります。
...) メソッドを呼び出すとき。直接渡そうとすると失敗します - それは可変引数の一つとして扱われ、これらは必須です。
intそしてではなく
array$times = [
new DateTimeImmutable(),
new DateTimeImmutable()
];
$stopwatch -> recordLaps(...$times);
可変引数の制限
可変引数は、関数にアイテムの配列を渡す必要があるときに大変役立ちます。しかし、使用方法にはいくつかの制限があります。
最も重要な制限は、関数ごとに可変引数のセットを1つしか使用できないことです。これは、各関数が1つの「型付けされた」配列しか受け入れられないことを意味します。さらに、可変引数は、通常の引数の後に最後に定義する必要があります。
function variadic(string $something, DateTimeInterface ...$times);本質的に、可変引数は関数とだけ使用できます。つまり、プロパティとして配列を保存したり、関数から返したりする必要があるときには役立ちません。これをストップウォッチのコードで見ることができます -
Stopwatchクラスには
laps整数のタイムスタンプのみを格納するための配列です。現時点では、これが実際であることを強制する方法はありません。
コレクションクラス
これらの状況では、別のアプローチを選択する必要があります。ユーザーランドPHPで「型付き配列」に近いものを作成する方法の一つは、専用のコレクションクラスを書くことです。
final class User {
protected string $Email;
public function getEmail() : string {
return $this -> Email;
}
}
final class UserCollection implements IteratorAggregate {
private array $Users;
public function __construct(User ...$Users) {
$this -> Users = $Users;
}
public function getIterator() : ArrayIterator {
return new ArrayIterator($this -> Users);
}
}
その
UserCollectionクラスは、通常配列が期待される場所ならどこでも使用できるようになりました。
Userインスタンス。
UserCollection可変個引数を使用して、一連のを受け入れます
Userそのコンストラクター内のインスタンス。たとえその
$Usersプロパティはジェネリックとして型ヒントを付ける必要があります。
arrayそれはコンストラクタでのみ書き込まれるため、ユーザーインスタンスのみで構成されることが保証されています。
提供することは魅力的に思えるかもしれませんが
get() : arrayコレクションのすべてのアイテムを公開するメソッド。これは避けるべきです。なぜなら、それは私たちを曖昧な状況に戻してしまうからです。
arrayタイプヒントの問題。代わりに、コレクションはイテラブルにされ、消費者がそれを使用できるようになっています。
foreachループ。このようにして、私たちはコードが安全にユーザーのみを含むと仮定できる型ヒント可能な「配列」を作成することに成功しました。
function sendMailToUsers(UserCollection $Users) : void {
foreach ($Users as $User) {
mail($user -> getEmail(), "Test Email", "Hello World!");
}
}
$users = new UserCollection(new User(), new User());
sendMailToUsers($users);
コレクションをより配列のようにする
コレクションクラスは型ヒントの問題を解決しますが、配列の便利な機能の一部を失うことになります。PHPの組み込み関数のような
count()そして
isset()あなたのカスタムコレクションクラスでは動作しません。
これらの機能のサポートは、追加の組み込みインターフェースを実装することによって追加できます。もしあなたが実装した場合
Countable, あなたのクラスは〜と一緒に使用できます
count():
final class UserCollection implements Countable, IteratorAggregate {
private array $Users;
public function __construct(User ...$Users) {
$this -> Users = $Users;
}
public function count() : int {
return count($this -> Users);
}
public function getIterator() : ArrayIterator {
return new ArrayIterator($this -> Users);
}
}
$users = new UserCollection(new User(), new User());
echo count($users); // 2
実装する
ArrayAccessコレクション内のアイテムに配列構文を使用してアクセスできるようにします。また、それは
isset()と
unset()関数。PHPがあなたのアイテムと対話できるように、4つのメソッドを実装する必要があります。
final class UserCollection implements ArrayAccess, IteratorAggregate {
private array $Users;
public function __construct(User ...$Users) {
$this -> Users = $Users;
}
public function offsetExists(mixed $offset) : bool {
return isset($this -> Users[$offset]);
}
public function offsetGet(mixed $offset) : User {
return $this -> Users[$offset];
}
public function offsetSet(mixed $offset, mixed $value) : void {
if ($value instanceof User) {
$this -> Users[$offset] = $value;
}
else throw new TypeError("Not a user!");
}
public function offsetUnset(mixed $offset) : void {
unset($this -> Users[$offset]);
}
public function getIterator() : ArrayIterator {
return new ArrayIterator($this -> Users);
}
}
$users = new UserCollection(
new User("example@example.com"),
new User("hello@world.com")
);
echo $users[1] -> getEmail(); // hello@world.com
var_dump(isset($users[2])); // false
あなたは今、含むことができるクラスを持っています。
Userインスタンス、および配列のように見え、感じるもの。一つ注目すべき点は
ArrayAccessは
offsetSet実装 - として
$valueに違いない
mixedこれにより、互換性のない値がコレクションに追加される可能性があります。渡された値の型を明示的にチェックします。
$valueこれを防ぐために。
結論
最近のPHPリリースは、言語をより強い型付けと一貫性のあるものに進化させました。ただし、これはまだ配列の要素には適用されていません。型ヒントを使用して
arrayしばしばリラックスしすぎていますが、自分自身のコレクションクラスを構築することで制限を回避することができます。
可変引数と組み合わせることで、コレクションパターンはコード内の集約値の型を強制するための実用的な方法です。コレクションに型ヒントを付けることで、そこに存在するのは1種類の値だけであることを知りながら、それらを反復処理できます。
