結論から言うと、レガシーコードにも一定の効果はありますが、本当になんとかしたいレガシー特有の配列操作では、メリットよりも array shapesのメンテ手間 のほうが勝ってしまいます。
最初に array shapes(array{id: int, name: string} みたいな定義)を知ったとき、「お、これでクラス作らなくても配列のまま手軽に型安全にできるじゃん!」と感動した記憶があります。
ところが、実際のレガシーコードでは次のような野生の配列操作が頻出します。(もちろん、昔のぼくもやっていました……!)
- 関数に配列参照(&)を渡して、中でキーを追加する
- 0〜n の添字(リスト)配列なのに、なぜか最後に
totalキーが追加される
これらを真面目に array shapes で表現しようとすると、破綻が始まります。
地雷1:オプショナルだらけの「なんちゃって型安全」
「関数に配列参照を渡して、キーを追加する」を表現しようとすると、array shapes は ?(オプショナル)だらけになります。
/** @param array{id: int, name: string, status?: string, updated_at?: string, ...} */Code language: PHP (php)
そして、PHPStanのLevel 6あたりから、この ? がついたキーにアクセスしようとすると、至る所で isset() や empty() のチェックを強要されるようになります。結果として、型安全の恩恵よりも「静的解析を黙らせるためのコード」が増え、型安全の崩壊を迎えます。
地雷2:構造が途中で歪む配列
「0〜n の添字アクセスの配列」だけなら、次のようにきれいに記述できます。
/** @param array<MyItem> */Code language: PHP (php)
しかし、ここに「途中で total キーが追加される」という仕様が入ってくると、もはやリストとしての部分を正しく表現できません。無理やり書こうとすると、次のような怪文書が生まれます。
/** @param array{0: MyItem, 1: MyItem, total?: int} */Code language: PHP (php)
結論:型パズルで時間を溶かすより、とっととDTOへ
こういったレガシーコードに遭遇したときは、array shapes 記法の型パズルで時間を溶かすのはやめましょう。
次のようなオブジェクト(DTO)に詰め替えて、配列には「同じ型のリスト」という純潔を保たせるリファクタリングをしたほうが、圧倒的に実利があります。
readonly class SearchResult
{
public function __construct(
/** @var MyItem[] */
public array $items,
public int $total
) {}
}Code language: PHP (php)
配列のまま無理やり延命するのではなく、早めに引き際を見極めてクラスに隠蔽する。これこそが、メンテコストを爆発させないための正攻法です。
関連記事

