1. 配列の基本的な使い方(一次元・多次元・連想配列)
1.1 PHPにおける配列の本質
PHPの配列は「配列」と呼ばれているものの、C言語やJavaの固定長配列とは全く別物です。内部的には ハッシュテーブル(HashTable) をベースにした「順序付きマップ」であり、要素の挿入順序を保持しつつ、数値キーと文字列キーを混在させることができます。
このため、単純な数値添字配列(リスト)も、文字列キーを持つ連想配列(マップ)も、さらにはネストした多次元配列もすべて同じ「array型」で表現されます。
利点は圧倒的な柔軟性ですが、欠点は メモリ効率とアクセス速度がC言語的な配列より劣る ことです。
1.2 一次元配列
一次元配列は最も基本的な「リスト構造」です。
$fruits = ["apple", "banana", "orange"];
echo $fruits[1]; // banana
特徴:
- 0から始まる数値キーが自動的に割り当てられる
- 末尾追加は
$array[] = $value;
で可能 - PHP 8.1以降では
array_is_list()
を用いて「リストとして扱える配列か」を判定できる
現場での利用例としては「ループ処理対象のリスト」や「結果セットの単純な格納」など。
ただし数値キーの連続性を保証しないため、「本当にリストとして扱う」ことを前提にする場合は array_values()
で正規化しておくのが堅実です。
1.3 連想配列
連想配列は、文字列キーを利用して意味的なマッピングを表現します。
$user = [
"id" => 42,
"name" => "Taro",
"role" => "admin",
];
特徴:
- データの意味を明確にキーで表現できる
- JSONやDBの結果セットと親和性が高い
- 数値キーも混在できるが、混在は混乱を招くので避けるのがベストプラクティス
特にREST APIやORMで返すデータはほぼ「連想配列」になります。
しかし、大規模処理やドメイン層では「キーに意味を持たせすぎると生の配列が乱雑になりやすい」ため、DTO/Entityクラスを定義して配列から移し替えるのが堅牢です。
1.4 多次元配列
多次元配列は「配列の中に配列を持つ」入れ子構造です。
$users = [
["id" => 1, "name" => "Taro"],
["id" => 2, "name" => "Hanako"],
];
echo $users[1]["name"]; // Hanako
典型的な用途は DBテーブルの結果セットです。
しかしネストが深くなると [$i]["child"]["grandchild"]["prop"]
のようにアクセスが煩雑になり、
可読性の低下や null チェックの増加を招きます。
これを回避するために現場では以下の方策が取られます:
array_column()
やarray_combine()
を駆使してフラット化- 専用のDTO/Collectionに移す
- 多段の
isset()
/ nullsafe演算子 (?->
) を利用する
2. 配列の応用的な使い方(現場テクニック)
2.1 高階関数的な操作
PHP配列の強みの1つは、array_map
/ array_filter
/ array_reduce
といった宣言的な操作関数群を備えていることです。
$nums = [1, 2, 3, 4];
$squares = array_map(fn($n) => $n * $n, $nums);
$evens = array_filter($nums, fn($n) => $n % 2 === 0);
$sum = array_reduce($nums, fn($acc, $n) => $acc + $n, 0);
利点は「ループの意図が関数名で明確化される」こと。
欠点は「無名関数呼び出しのオーバーヘッド」であり、ホットパス(大量繰り返し)では foreach の方が速いです。
そのため現場では「ビジネスロジック層では高階関数」「インフラ寄りのホット処理では foreach」が使い分けられます。
2.2 SQL的操作を配列で再現
PHPの配列関数群を組み合わせると、SQL的な処理をアプリ層で完結できます。
$names = array_column($users, "name"); // SELECT name
$admins = array_filter($users, fn($u) => $u["role"] === "admin"); // WHERE role=admin
ただし注意点として、本来はDBで済ませるべき処理を配列でやってしまうと性能が大きく低下します。
「何件までなら配列処理でよいか」「何件以上はSQLに寄せるか」を判断するのが現場エンジニアの腕の見せ所です。
2.3 ソート
PHPのソート関数は豊富で、sort
/ ksort
/ usort
などがあります。
usort($users, fn($a, $b) => $a["id"] <=> $b["id"]);
カスタムソートは柔軟ですが、比較関数が呼ばれるたびにオーバーヘッドが発生するため、10万件以上のデータを usort で並べ替えるのは非現実的です。
そのような場合は DB側で ORDER BY を使うか、専用のインデックス構造を利用する方が現実的です。
3. 配列以外の代案紹介
PHP配列は「何でもできる」反面「どれも中途半端」です。そこで場面によって代替データ構造を使い分けます。
3.1 SplFixedArray
内部的に固定長で確保されるため、通常の配列よりメモリ効率が良いです。
数値キー専用のため、リスト構造を高速に扱いたい場合に有効です。
$arr = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $arr[$i] = $i;
3.2 SplObjectStorage
オブジェクトをキーとして保存できる特殊な構造です。
「オブジェクト集合」「グラフのノード管理」など特殊なユースケースで利用されます。
3.3 DS拡張(php-ds)
PECL拡張で提供されるデータ構造ライブラリ。
Vector
(動的配列、JavaのArrayList相当)Deque
(両端キュー)Map
(連想配列に相当だが高速)Set
(集合演算に強い)
大規模データを扱う場合は配列よりこちらの方が効率的。
3.4 フレームワークのコレクション
Laravelの Collection
などは、map
/ filter
/ groupBy
といった操作をチェーンで直感的に扱える。
実際の現場では「生配列 → Collectionに変換」がよく行われます。
4. パフォーマンス比較(実測例)
計測条件
- PHP 8.3 CLI
- 100万要素
- OPcache 有効
- JIT Off
計測コード抜粋
// array (forループで合計)
$data = range(1, 1_000_000);
$t1 = hrtime(true);
$sum1 = 0;
foreach ($data as $v) $sum1 += $v;
$t2 = hrtime(true);
// SplFixedArray (forループで合計)
$arr = new SplFixedArray(1_000_000);
for ($i=0; $i<1_000_000; $i++) $arr[$i] = $i+1;
$sum2 = 0;
for ($i=0; $i<1_000_000; $i++) $sum2 += $arr[$i];
$t3 = hrtime(true);
echo "array: ".(($t2-$t1)/1e9)." sec\n";
echo "SplFixedArray: ".(($t3-$t2)/1e9)." sec\n";
結果(一例
- 通常配列(
array_sum
) → 0.015秒 - SplFixedArray → 0.011秒
- DS\Vector → 0.010秒
純粋なPHPでのループ処理においては、SplFixedArray
が通常の配列よりもわずかに高速である傾向にあるようです
5. 現場での使い分け指針
- 小〜中規模
→ 通常配列(シンプル・柔軟) - 大量データ
→ SplFixedArray や DS\Vector(メモリ効率重視) - オブジェクト集合
→ SplObjectStorage - SQL的処理や集計
→ DBに寄せる(不要な配列操作は避ける)
6. PHP配列における典型的なアンチパターン
6.1 何でも配列に突っ込む(万能バッグ問題)
// NG: データの意味が混在
$user = [
"Taro", // 名前
28, // 年齢
true, // 有効フラグ
"admin" // ロール
];
- 問題点
- 要素に意味がなく、インデックス番号に依存
- 何番目が「年齢」か一目で分からない
- 項目追加時に壊れやすい
- 改善策
// OK: 連想配列で明示 $user = [ "name" => "Taro", "age" => 28, "active" => true, "role" => "admin" ];
または DTO/Entityクラスを用意し、プロパティで表現する。
6.2 多次元配列をそのまま業務ロジックに流用
// NG: DB結果をそのまま多次元配列で扱う
foreach ($orders as $order) {
echo $order["user"]["address"]["city"];
}
- 問題点
- nullチェックが煩雑(addressが存在しないとNotice発生)
- 入れ子が深くなるとコードが壊れやすい
- 改善策
- Collection/DTOに変換して階層を意識しないで済むようにする
- PHP 8.0+なら nullsafe演算子 (
?->
) を活用する
echo $order["user"]["address"]["city"] ?? "N/A";
6.3 巨大配列で無駄に array_filter / map を多重適用
// NG: 何万件も map → filter → reduce を重ねる
$total = array_reduce(
array_filter(
array_map(fn($o) => $o["price"] * $o["qty"], $orders),
fn($v) => $v > 0
),
fn($a, $b) => $a + $b,
0
);
- 問題点
- 中間配列を毎回生成するためメモリ・時間コストが高い
- ネストで可読性が悪い
- 改善策
// OK: 単一 foreach でまとめる $total = 0; foreach ($orders as $o) { $price = $o["price"] * $o["qty"]; if ($price > 0) $total += $price; }
→ ホットパスでは「高階関数より素直なループ」の方が速くて明瞭。
6.4 配列をDB代わりに使う
// NG: 在庫を配列で管理
$stocks = [
"A001" => 100,
"A002" => 200,
"A003" => 50,
];
// 毎回配列検索して更新
$stocks["A001"] -= 1;
- 問題点
- 競合(複数リクエスト更新)に弱い
- 保存先がメモリなのでプロセスが死ぬと消える
- 改善策
- 本当に必要なら Redis/DB を使う
- 配列はあくまで「一時のデータ構造」にとどめる
6.5 配列にロジックを詰め込みすぎる
// NG: 状態遷移を配列で定義
$workflow = [
"draft" => ["review"],
"review" => ["approved", "rejected"],
"approved" => []
];
- 問題点
- 配列のままだと状態遷移の不正チェックが困難
- ルール変更時に配列定義が複雑化
- 改善策
- Stateパターン / Enum(PHP 8.1+) で表現
enum Status: string { case Draft = "draft"; case Review = "review"; case Approved = "approved"; case Rejected = "rejected"; }
こうすることで状態遷移の制御を型レベルで担保できる。
6.6 配列に参照を残したまま利用
// NG
foreach ($users as &$u) {
$u["role"] = strtoupper($u["role"]);
}
// $u が参照のまま残る → 後続で事故
- 問題点
- 参照が持ち越され、後の変数操作が意図せず配列を汚す
- 改善策
foreach ($users as &$u) { $u["role"] = strtoupper($u["role"]); } unset($u); // 参照解除必須
7. まとめ:配列アンチパターンから学ぶ設計指針
- 配列は万能バッグではない → 意味づけ(連想配列 or クラス)を徹底
- ネスト深度は早めに解消 → DTO/Collectionで包む
- パフォーマンスが要求される箇所では素直なループ → array_*関数を濫用しない
- 長期的な永続化・状態管理は配列に頼らない → DB/Redisなどを活用
- 参照渡しは必ず
unset()
8. PHPStan で配列アンチパターンを炙り出す推奨セット
0. 導入(Composer)
composer require --dev \
phpstan/phpstan \
phpstan/extension-installer \
phpstan/phpstan-strict-rules \
spaze/phpstan-disallowed-calls \
phpstan/phpstan-deprecation-rules
# (必要に応じて)
composer require --dev \
phpstan/phpstan-phpunit \
phpstan/phpstan-symfony \
phpstan/phpstan-doctrine
- strict-rules… “緩い記述”を検出。配列×混在型の粗を拾いやすい
- disallowed-calls…
empty()
やグローバル関数など禁止 APIを列挙して検出 - deprecation-rules… 廃止予定 API の混入を検出
- (任意)フレームワーク連携
1. ベース設定(phpstan.neon
)
まずは レベル 8(最高) を目標。段階導入は CI で
--level
を上げていく。
parameters:
level: 8
inferPrivatePropertyTypeFromConstructor: true
reportUnmatchedIgnoredErrors: true
checkMissingIterableValueType: true # iterable の value 型必須
checkGenericClassInNonGenericObjectType: true
treatPhpDocTypesAsCertain: false # まずは「疑わしきは注意」に
checkExplicitMixed: true # mixed を露出させる
checkBenevolentUnionTypes: true
universalObjectCratesClasses: [] # 動的にプロパティを持つ「オブジェクト」の安全性を高めるための機能
paths:
- src
- app
tmpDir: var/cache/phpstan
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/deprecation-rules/rules.neon
# disallowed-calls は独自設定ファイルをインクルード
- phpstan.disallowed.neon
# 配列アンチパターンに関連する推奨ルールの微調整
rules:
# array_* の戻り値未検証(false/空配列など)を見逃さない
- PHPStan\Rules\IfElse\ConstantConditionRule
- PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule
services:
# array shape の積極活用(phpDoc を型として強く扱いたい場合)
- class: PHPStan\Type\Php\AssertTypeSpecifyingExtension
2. 禁止 API を明示(phpstan.disallowed.neon
)
ここで アンチパターンに直結する関数を“禁止 or 要レビュー”にできます。
includes:
- vendor/spaze/phpstan-disallowed-calls/disallowed-calls.neon
parameters:
disallowedFunctionCalls:
# 1) empty() の濫用('0' が空扱いになる罠)
- function: empty
message: 'empty() の使用を禁止。明示比較を用いること。'
# 2) array_filter のコールバック省略(非意図の falsey 除去)
- function: array_filter
# PHP 8.0以降では第二引数($callback)が省略可能
# allowParamsAnywhereは関数の引数全体を対象とするため、position: 1
allowParamsAnywhere:
- position: 1
message: 'array_filter()はコールバック必須。falsey一括削除の事故防止。'
# 3) each() / create_function() などの遺物
- function: each
message: 'each() は非推奨。foreach を使用。'
- function: create_function
message: 'create_function() は禁止。匿名関数を使用。'
disallowedConstantUsages:
# マジックナンバー的 const をプロジェクト側で禁止したいときに追加
[]
ポイント
empty()
禁止は配列検証と入力バリデーションの質を底上げarray_filter
コールバック必須で「意図しないデータ消失」を防止
3. 「配列をきちんと型で縛る」ための運用ルール
3.1 array shape を使う(phpDoc)
/** @var array{id:int, name:string, role:'admin'|'user'} $user */
$user = ['id' => 1, 'name' => 'Taro', 'role' => 'admin'];
- 利点:
$user['role']
などの取り違い検出、キー名のミス検出 - コツ:DTO/ValueObject を導入する移行期に「まず shape で縛る」
3.2 リストの保証
/** @var list<int> $ids */ // キーが0..nの連続整数
$ids = array_values($ids); // 正規化
list<T>
を明示すると「連想キー混入」が即検出される
3.3 mixed の遮断
checkExplicitMixed: true
で mixed の露出を警告- 外部入力は必ず Normalizers/Factory を通して強い型に変換
4. 典型アンチパターンに効く「検知パターン」
アンチパターン | 何で検知するか | 設定/対処 |
---|---|---|
何でも配列(意味不明な一次元) | mixed ・配列アクセス警告 | array shape で鍵付け、DTOへ移行 |
多次元配列をそのまま業務に流用 | 未定義キーアクセス/型不一致 | isset 乱立検出、shape 付与、DTO化 |
巨大配列に map/filter 多重適用 | 中間配列肥大は検知困難 | disallowed-calls で「無コールバック array_filter 禁止」+レビュー方針 |
配列を DB 代わりに使用 | 静的検知困難 | 設計ルール(レビュー)+ disallowed で file_put_contents 等の直接永続化禁止も可 |
参照 foreach の持ち越し | 静的検知やや困難 | カスタムルール(後述) or Rector と併用 |
5. カスタムルール(実例)
「
foreach ($arr as &$v)
を検知して注意喚起」する簡易ルール例。src/PhpStan/Rules/NoForeachByRefRule.php
<?php
declare(strict_types=1);
namespace App\PhpStan\Rules;
use PhpParser\Node;
use PhpParser\Node\Stmt\Foreach_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
final class NoForeachByRefRule implements Rule
{
public function getNodeType(): string { return Foreach_::class; }
public function processNode(Node $node, Scope $scope): array
{
/** @var Foreach_ $node */
if ($node->valueVar instanceof Node\Expr\Variable
&& $node->byRef === true) {
return ['foreach での参照渡し(&)は原則禁止。破壊的更新が必要なら unset() を忘れず、代替を検討。'];
}
return [];
}
}
サービス登録(phpstan.neon
)
services:
- class: App\PhpStan\Rules\NoForeachByRefRule
tags:
- phpstan.rules.rule
これで 参照付き foreach を機械的に検出できます。
例外を許すなら@phpstan-ignore-next-line
を局所的に許可。
6. 型付け強化の実務テク
- DTO/ValueObject/Enum を積極採用(配列のキー意味論を型へ昇格)
- Collection クラス(
implements IteratorAggregate
)で配列操作を包む array<string, User>
のように ジェネリック風 phpDoc を統一(Psalm 互換コメントでも可)- 「入力境界でスキーマ検証」(例:リクエスト→Normalizer→DTO→業務)
7. CI / 段階導入
- 段階レベルアップ:
--level=5 → 7 → 8
と少しずつ引き上げ - エラーが多い場合は baseline を使うが、毎週減らす目標を設定
vendor/bin/phpstan analyse --generate-baseline
- PR で
phpstan
を必須にし、disallowed-calls の違反が出たらブロック
8. 既存コードの“痛みなく”強化する順序
level=5
+checkExplicitMixed=true
+array shape
を要所にstrict-rules
を有効化(可読性の低い書き方を洗い出し)disallowed-calls
で empty()/無コールバック array_filter を封じる- 参照 foreach の カスタムルール を導入
- DTO/Collection へ段階移行、最終的に
level=8
9. サンプル:配列 shape と検出の効き方
/** @var array{id:int, items:list<array{id:int, price:positive-int}>} $order */
$order = getOrder(); // mixed からの脱却
$total = 0;
foreach ($order['items'] as $item) {
// キー typo: $item['prise'] → level=8 で検出(未定義キー)
$total += $item['price'];
}
- 誤キーやnull 混入が即検出され、**本番事故(「キー間違いで0円請求」系)**を未然に防止。
まとめ(導入の要点)
- strict-rules + disallowed-calls + level=8 を中核に
- array shape / list を多用し、配列の曖昧さを排除
- 参照 foreach や
empty()
、無コールバックarray_filter
など事故の温床を機械検知 - DTO/Collection で 「配列から型へ」 を計画移行
- CI で 段階引き上げ & ベースライン削減 を進める
📌 結論
- PHP配列は「柔軟で便利だが汎用的」
- パフォーマンスを求めるなら代替構造を検討
- 実務では「配列をどう使うか」より「どの時点で配列にせず処理を寄せるか」が最重要
コメント