PHPで複雑な配列を使うのをやめよう!

この記事はウィルゲート Advent Calendar 2025 17日目の記事です。

adventar.org

こんにちは、開発室の武田(@fuwari_we)です!

突然ですが、コードを読んでいて「これは何のデータなんだろう?」と立ち止まってしまう瞬間ってありませんか?

変数名を見ても分からず、メソッドの引数を見てもイマイチわからない。結局、そのデータを生成している処理を遡ってようやく理解することができる。

私はそういうコードに出会うたびに、少しずつ脳内のリソースが削られていくような感覚を覚えます。 そしてその多くは、「データの意味がコード上で表現されていない」ことに起因しているように思います。

ことPHPでは、複雑なデータを扱う手段の一つとして配列が存在します。 PHPの配列は非常に強力で柔軟です。しかし、それは大きな武器である一方「何を表しているデータなのか」が分かりにくいという問題も孕んでいます。

この記事では、配列で意味のあるデータを扱うことで生じる問題と、それをどのように表現し直せるかを考えてみます。

想定読者

  • PHPで開発している方
  • 配列以外のデータの表現方法に興味がある方
  • データ構造を把握するためにコードを遡ることが多いと感じている方

この記事の結論

  • 配列は便利だが「意味を持ったデータ」を表現するのには向いていない
  • クラスやEnumを使うことで、データの意図や意味を表現することができる

なぜ私たちは配列の構造を把握しなきゃいけないのか

コードを読むとき、私たちは「この配列をどう扱えばいいか」を判断する必要があります。

  • どのキーにアクセスすればいいのか
  • nullチェックは必要か
  • 想定していない値が入る可能性はあるか

こうした判断をするためには、そのデータがどんな構造をしていて、どんな前提を持っているのかを知る必要があります。

もしそれが型やインターフェースとして表現されていれば、コードを読むだけで前提を共有できます。 しかし配列の場合、その前提はメソッドの引数などには現れません。

結果として私たちは「配列の構造を把握しにいく」という行動を取らざるを得なくなります。 生成処理を追い、場合によっては仕様書を確認し、頭の中で「この配列のルール」を組み立てることになります。

配列で渡した瞬間に失われやすい情報がある

配列は柔軟で、何でも入れられるからこそ便利です。 一方でその「何でも入れられる」性質が、受け渡しの場面では別の問題になります。

ここでは一例として、ユーザーの更新処理を考えてみます。

function updateUser(array $userData) { ... }

この配列は $userData という変数名から推測するに、ユーザー情報という意味を持っていそうなデータであるということは分かりそうですが、

  • どのようなユーザー情報を持っているのか
  • どの項目が必須で、どれが任意なのか
  • どのような形式を期待しているのか

といったことは、ここからは読み取ることができません。 その結果、コードを理解するために、配列の生成処理を遡ったり、仕様書を行き来する読み方が必要になります。

複雑な配列がつらくなる理由

意味を持ったデータを配列で扱い続けると、次のような問題が起きやすくなります。

  • どんなキーや値を持っているのか、ぱっと見て分からない
  • 同じ概念のデータなのに、実装ごとにデータ構造が微妙にズレていく
  • 正しい使い方がコードではなく、人に依存してしまう

これらはコメントやドキュメントで補うこともできますが、それは「読む人が正しく理解してくれること」を前提にしているに過ぎません。

このようにコードがデータの意味を語ってくれず、データの意味が人に依存している状態は、実装者によってブレが発生しやすく、場合によっては障害に繋がる恐れもあります。

問題のコード

ユーザー登録・更新処理を例に考えてみます。

実際には、配列が分かりやすい形で宣言されていることは少なく、フォーム入力の加工や外部のAPIレスポンスの整形、DBから取得した結果を組み立てたりなど、何かしらの生成ロジックを経て配列が出来上がっていることがほとんどです。

その結果、利用側で見えるのは「配列が渡ってくる」という事実だけ、という状態になります。

たとえば、生成された配列が以下のような形だったとします。

$userData = [
    'id' => 123,
    'name' => [
        'first' => 'Taro',
        'last' => 'Yamada',
    ],
    'email' => 'taro@example.com',
    'gender' => 'male',
];

ここで困るのは「この配列をどう扱えばいいのか」が分からないことです。

  • name は常に first/last を持つのか?
  • gendermale/female/other なのか、それとも man/woman など別の表現があるのか?
  • email は正しいメールアドレスである前提でいいのか?

配列の形を見ただけでは、これらの前提が確定しません。

もちろん、生成ロジックまで遡るなどすることで一定データ構造や意味が見えてはきますが、結局のところ人間の目視チェックという信頼できない前提でしかないので、どこかで「念のためのチェック」を積み上げる必要が出てきます。

こうした問題を解消するために、ここからは段階的に表現を見直していきます。

まずはクラスで表現してみる

最初のステップとして、配列をそのまま渡すのをやめ、ユーザー情報をクラスで表現します。

final class User
{
    public function __construct(
        public string $name,
        public string $email,
        public string $gender,
    ) {}
}

これにより、「これはユーザー情報である」という前提が型として明示されます。 また、プロパティを見るだけでどのような情報を持っているかが一目で分かります。

しかし、まだ問題が残っています。

  • name はどのような構造を持つのか
  • email はメールアドレスとして正しいのか
  • gender はどんな値を取りうるのか

依然として呼び出し側の注意に委ねられています。

プロパティもクラスで表現する

次に、プロパティ自体もクラスとして表現します。 ここで狙いたいのは「値にも意味と制約を持たせる」ことです。

分かりやすくするため、まずemail を例にしてみます。 string $email のままだと、次のような値でも渡せてしまいます。

  • taro.example.com
  • taro@
  • 空文字

呼び出し側が毎回チェックしない限り、誤った値が紛れ込む可能性が残ります。

そこで、メールアドレスを表すクラスを作ります。

final class EmailAddress
{
    public function __construct(public readonly string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('不正なメールアドレスです。');
        }
    }
}

こうすると、EmailAddress を生成できた時点で「少なくともメールアドレスとして成立している」という前提を満たしたことになります。

この前提があることで、利用側は「念のためのチェック」を書かずに済みます。 不正な値は入り口で弾かれ、処理の本体は本来の関心事だけに集中することができます。

同様に、name もクラス化して構造を固定できます。

final class UserName
{
    public function __construct(
        public readonly string $first,
        public readonly string $last,
    ) {
        if ($first === '' || $last === '') {
            throw new InvalidArgumentException('名前は姓名ともに必須です。');
        }
    }
}

ユーザー全体は次のようになります。

final class User
{
    public function __construct(
        public UserName $name,
        public EmailAddress $email,
        public string $gender,
    ) {}
}

GenderをEnumとして表現する

gender は「形式」ではなく「選択肢」が問題になりやすい値です。 そこでEnumで表現します。

enum Gender: string
{
    case Male = 'male';
    case Female = 'female';
    case Other = 'other';
}

Enumを使うことで、「この値はこの選択肢の中から選ばれる」という制約がコードとして表現できます。 仕様が曖昧になりやすいところほど、コード側で選択肢を閉じておくと迷いが減ります。

改善後のコード例

ここまで反映すると、ユーザー情報は次のように表現できます。

$user = new User(
    new UserName('Taro', 'Yamada'),
    new EmailAddress('taro@example.com'),
    Gender::Male,
);

このデータを受け取る側は、配列だったときのように前提を推測する必要がありません。 名前の構造は明確で、メールアドレスは正しい形式であることが保証されています。 gender にどんな値が来るかもEnumを見るだけで把握できます。

結果として、「配列の中身を暗記する」「生成ロジックを遡る」といった作業を無くすことができました。

配列を使わない、ではなく濫用しない

配列そのものは悪いものではありません。 単純なデータの集合や、一時的な加工では依然有効です。

問題になるのは、意味を持ったデータを構造や制約を持たないまま配列で扱い続けてしまうことです。

たとえば、

  • 同じような配列構造が何度も登場する
  • 「このキーは必須」「この値はこの形式」といった前提が増えてきた
  • コメントやドキュメントでの説明がないと扱い方が伝わらない

こうしたことが増えてきたら、ぜひ配列以外の表現を検討してみてください。

ウィルゲート Advent Calendar 2025」、翌日は山本さんの「明日からでも実践できる!AIを活用した業務改善例」です!