PHP の array_filter() の落とし穴
こんにちは。 SAW です。
最近、 森永製菓のビスケット にハマってます。
MARIE と CHOICE が特にお気に入りです🍪
PHP で配列を操作するための標準関数の中に、 array_filter()
という関数があります。
この関数は、特定の条件に合致した要素のみを取り出した配列を新たに生成します。
$array = [1, 2, 3, 4, 5];
$filtered = array_filter($array, fn ($x) => $x % 2 === 0);
JavaScript にも、同様のメソッドとして、 Array.filter()
が存在します。
こちらも、条件に合致する要素のみを取り出した配列を新たに生成します。
const arr = [1, 2, 3, 4, 5];
const filtered = arr.filter((x) => x % 2 === 0);
どちらも 配列の要素の絞り込み という機能は同じですが、少しだけ挙動が異なります。
本記事では、 PHP の array_filter()
の落とし穴 と、その 回避方法 を紹介します。
対象読者
本記事で想定する読者層は次の通りです。
- PHP の基礎的な知識を有している
- 一般的な配列操作のメソッド (filter, map) について基本的な知識を有している
array_filter()
の落とし穴
エラーが起きる例
array_filter()
で条件に合致する要素に絞り込んだ配列について、各要素ごとに操作するような場合は注意が必要です。
例えば、以下のコード例のように、 name
と age
をキーにもつ連想配列の配列から、年齢 (age
) が 18 歳より大きい要素を絞り込んだあと、 array_map()
[1] を利用して名前 (name
) のリストを生成する場合を考えます。
$array = [
['name' => 'John', 'age' => 23],
['name' => 'Lisa', 'age' => 15],
['name' => 'Karen', 'age' => 24],
['name' => 'Fred', 'age' => 19],
];
// 年齢が 18 歳より大きいデータのみに絞り込み
$filtered = array_filter($array, fn ($x) => $x['age'] > 18);
// 年齢が 18 歳より大きい名前の配列を作成
$mapped = array_map(fn($x) => $x['name'], $filtered);
for ($i = 0; $i < count($mapped); $i++) {
echo $mapped[$i] . PHP_EOL;
}
一見、実行してもエラーが発生しなさそうなコード例ですが、上記のコードを実行すると以下のエラーメッセージが表示されます。
PHP Warning: Undefined array key 1
どうやら、未定義の配列のキーにアクセスしているようですが、一体どこで未定義のキーにアクセスしているのでしょうか?
原因調査
前節のコード例の $mapped
の構造を確認してみましょう。
以下は var_dump()
で出力した $mapped
の内容です。
array(3) {
[0]=>
string(4) "John"
[2]=>
string(5) "Karen"
[3]=>
string(4) "Fred"
}
なんということでしょう! 配列の添字が連番になってないではありませんか!
前節のコード例では、 for
文で $mapped[0]
, $mapped[1]
, $mapped[2]
, ... のように、0
から順に添字で配列の要素にアクセス していました。
しかし、 $mapped
には 1 の添字の要素はない ため、未定義の配列のキーにアクセスしているというエラーが発生しました。
$filtered
の中身も確認してみると、同様に配列の添字が連番になっていないことがわかります。
array(3) {
[0]=>
array(2) {
["name"]=>
string(4) "John"
["age"]=>
int(23)
}
[2]=>
array(2) {
["name"]=>
string(5) "Karen"
["age"]=>
int(24)
}
[3]=>
array(2) {
["name"]=>
string(4) "Fred"
["age"]=>
int(19)
}
}
array_filter()
のドキュメントを確認すると、以下のように、 要素のキーは保存される ことが書かれています。
連想配列だけでなく通常の配列も同じため、添字が歯抜けの配列が生成されます。
Array keys are preserved, and may result in gaps if the array was indexed.
落とし穴の回避方法
array_values()
を用いる例
array_filter()
のドキュメントには、以下のように array_values()
を使うことで配列の添字が振り直されると記述されています。
The result array can be reindexed using the array_values() function.
前章のコード例の array_filter()
の結果を array_values()
に渡すように書き換えてみます。
// 年齢が 18 歳より大きいデータのみに絞り込み
- $filtered = array_filter($array, fn ($x) => $x['age'] > 18);
+ $filtered = array_values(array_filter($array, fn ($x) => $x['age'] > 18));
// 年齢が 18 歳より大きい名前の配列を作成
$mapped = array_map(fn($x) => $x['name'], $filtered);
for ($i = 0; $i < count($mapped); $i++) {
echo $mapped[$i] . PHP_EOL;
}
array_values()
で添字が振り直されているため、前章のエラーが解消されました。
修正したプログラムの実行結果は以下の通りです。
John
Karen
Fred
foreach
文を用いる例
別の方法として、 for
文を foreach
文に書き直してみます。
foreach
文を利用することで、 配列の添字を利用することなく 各要素を取得するため、添字が歯抜けになっているかを意識する必要がありません。
for
文を foreach
文に書き換えた例は以下の通りです。
// 年齢が 18 歳より大きいデータのみに絞り込み
$filtered = array_filter($array, fn ($x) => $x['age'] > 18);
// 年齢が 18 歳より大きい名前の配列を作成
$mapped = array_map(fn($x) => $x['name'], $filtered);
- for ($i = 0; $i < count($mapped); $i++) {
- echo $mapped[$i] . PHP_EOL;
- }
+ foreach ($mapped as $element) {
+ echo $element . PHP_EOL;
+ }
こちらもエラーが発生することなく、 array_values()
を利用した例と同様の実行結果が得られます。
添字を使った配列の要素へのアクセスの是非について
本記事のコード例では、 for
文で添字を使って配列の要素にアクセスをしていました。
読者の方の中には、「そもそも for
文で書くのがよろしくない」と思われた方もいると思います。
実際に、 array_filter()
で配列の添字が歯抜けになった影響で、未定義のキーを使ったアクセスとしてエラーが発生していました。
言語の文法や機能にもよりますが、 PHP の foreach
文のように、 添字を使わずに配列の要素にアクセス する書き方ができるのであれば、そちらを使った方が良さそうです。
以下の記事で扱っている言語は TypeScript ですが、同様のことが「おわりに」で述べられています。
あるいは、for...of を使うなどなるべくインデックスアクセスに頼らないコードを意識してみるとよいでしょう。
今回のコード例のように、配列の要素を 1つずつ取り出して操作するような場合については、 for
文で添字を使ってアクセスするのではなく、 foreach
文を使って要素の値を変数に取り出すほうが良いでしょう。
添字を使いたいケース
以下のコード例のように、 PHPUnit などで入り組んだ構造のデータについてテストする際には、 アクセスする要素の位置を指定 するために添字を使って配列の要素にアクセスすることがあります。
public function testGetLessons(): void
{
$lessons = Lesson::where('author_id', 1);
$this->assertEquals('lesson title', $lessons[0]->title);
$this->assertCount(2, $lessons[0]->chapters);
$this->assertEquals('chapter title 1', $lessons[0]->chapters[0]->title);
$this->assertEquals('chapter title 2', $lessons[0]->chapters[1]->title);
}
PHP の Array destructuring を利用すれば、要素を添字を利用せずに、先頭から要素を取得できますが、少しコードが長くなってしまいます。
特定の位置の要素のみ取り出すといった制御もできないので、テストコードでは添字を使う方が便利な場合もあります。
public function testGetLessons(): void
{
$lessons = Lesson::where('author_id', 1);
[$lesson] = $lessons;
[$chapter1, $chapter2] = $lesson->chapters;
$this->assertEquals('lesson title', $lesson->title);
$this->assertCount(2, $lesson->chapters);
$this->assertEquals('chapter title 1', $chapter1->title);
$this->assertEquals('chapter title 2', $chapter2->title);
}
まとめ
本記事のまとめは次の通りです。
-
array_filter()
の落とし穴について説明 - 落とし穴の回避手段を紹介
PHP の配列は少し独特な仕様をしていると、改めて感じました(笑)
参考文献
-
array_filter()
とarray_map()
では、どちらも操作対象の配列とコールバック関数を指定しますが、 引数の順序が異なる 点に注意してください。 ↩︎
Discussion