PHPの多次元配列でユニークキー検索っぽい事をする

複数のレコードを持つデータセットを PHP で扱う際に、「ユニークキー検索」みたいなことが出来ると嬉しい時がある。

当然だが、データセットが「ユニークキー」を持っている仕様でないと、期待する結果は得られないので、参考にする場合は注意されたし。

 

データセットは array(配列型)で持つ

データセットは RDBMS や JSON から取得するシーンが多いと思うので、今回はこの2点を中心に解説する。

RDBMS から PDO 経由で取得

PHP でデータベースを扱う際、管理人は PHP Data Objects(PDO)しか使わないので、解説は PDO のみでご容赦ください。

今回検索で使用するのは「配列用の関数」なので、PDOStatement::fetch の mode に PDO::FETCH_ASSOC を指定しておく。

~(中略)~
$stt = $this->dbo->prepare($sql);
$stt->execute($param);
$data = $stt->fetchAll(PDO::FETCH_ASSOC);

これで「カラム名で添字を付けた配列」が結果セットに返ってきます。

API 等から JSON データを取得

近年、API 等で利用する頻度も高い JSON データの取得、特に何も指定せずに json_decode でパースすると、オブジェクト型で変数に格納される。

json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed

今回は配列型でデータを持ちたいので、第2引数の $associative に true を指定してやると良い。

~(中略)~
$data = json_decode($json, true);

中身はこんな感じ

双方の中身を確認した所、以下のように同じものが格納されていたので準備完了とする。

~(中略)~
var_dump($data);

/*
array(3) {
  [0]=>
  array(3) {
    ["ID"]=>
    string(1) "1"
    ["NAME"]=>
    string(4) "hoge"
    ["FAVO"]=>
    string(5) "oniku"
  }
  [1]=>
  array(3) {
    ["ID"]=>
    string(1) "2"
    ["NAME"]=>
    string(4) "piyo"
    ["FAVO"]=>
    string(5) "yasai"
  }
  [2]=>
  array(3) {
    ["ID"]=>
    string(1) "3"
    ["NAME"]=>
    string(4) "foo"
    ["FAVO"]=>
    string(5) "oniku"
  }
}
*/

 

配列に含まれる指定キーの値を検索するコード

取り合えず今回のお題を達成できるコードは以下の通り、非常にシンプルな書き方で実現出来る。

// $data には前項で取得した配列が格納されている
$key = array_search("3", array_column($data, "ID"), true);
$row = $key !== false ? $data[$key]: false;

SQL の WHERE 句みたいなイメージ

このコードは SQL のクエリに当てはめて考えると、それぞれの役割が理解しやすいかも知れない。

// PHP
$key = array_search(<検索する値>, array_column(<テーブル名>, <検索カラム名>), true);
<レコード内容> = $key !== false ? $data[$key]: false;
-- MySQL
SELECT <レコード内容> FROM <テーブル名> WHERE <検索カラム名> = <検索する値> LIMIT 1;

こんな感じ。

SQL クエリ側に「LIMIT 1」と書かれているのは、最初に発見したレコードのキーを返すと言う、PHP の array_search の仕様に合わせるため。

これは要するに、必ず単一の結果しか返ってこないと言う事だ。

当然だが、データセットが「ユニークキー」を持っている仕様(同じキーが 2 つ以上無い)でないと、期待する結果は得られないので、参考にする場合は注意されたし。

冒頭でこう書いたのはこの仕様が原因だが、プライマリキー等の「ユニークな値を検索する」と言う用途なら問題無く利用できる。

関数化

検索の度にいちいち書くのは非効率的なので、2回以上実行するなら関数化しておくと良さそうだ。

function searchID(&$data, $id) {
  $key = array_search($id, array_column($data, "ID"), true);
  return $key !== false ? $data[$key]: false;
}

var_dump(searchID($data, "3"));
/*
array(3) {
  ["ID"]=>
  string(1) "3"
  ["NAME"]=>
  string(3) "foo"
  ["FAVO"]=>
  string(5) "oniku"
}
*/

var_dump(searchID($data, "4"));
/*
bool(false)
*/

 

先に実行される array_column

折角なので何故こうなるかも勉強しておく。

array_column(array $array, int|string|null $column_key, int|string|null $index_key = null): array

この関数は、第1引数に検索したい配列(前述の<テーブル名>)を指定し、第2引数に取得したいキー(前述の<検索カラム名>)を指定すると、対応する値が配列となって返って来る。

ちょっと $data の中身を見やすく整理して、array_column の実行結果と見比べてみよう。


$data = [
  ["ID" => "1", "NAME" => "hoge", "FAVO" => "oniku"],  // レコード #0
  ["ID" => "2", "NAME" => "piyo", "FAVO" => "yasai"],  // レコード #1
  ["ID" => "3", "NAME" => "foo", "FAVO" => "oniku"]    // レコード #2
];

print_r(array_column($data, "ID"));
/*
Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
*/

このように、ID の中身が配列になって返ってきている事が確認できる。

この返ってきた配列のキーは、それぞれ $data の「レコード #0~#2 のキーと等しい」と言うのがポイントになっている。

 

次に実行される array_search

array_column が解決されると、次に実行されるのが array_search となる。

array_search(mixed $needle, array $haystack, bool $strict = false): int|string|false

第1引数に検索したい値(前述の<検索する値>)を渡し、第2引数に検索したい配列(前述の array_column の結果)が渡される。

そして、検索する値が見つかればそのキーを、見つからなかったら false を返してくる。

なお、第3引数の $strict に true を指定すると、比較が厳密化(型比較など)するので、管理人的には true を指定する事をおススメする。(※PHP の暗黙の型変換は、思わぬバグを招く事が稀に良くある。)

array_column の実行結果を見やすく整理して、array_search の実行結果を見比べてみよう。

$column = [
  "1",  // array_column の実行結果 #0
  "2",  // array_column の実行結果 #1
  "3"   // array_column の実行結果 #2
];
$key = array_search("3", $column, true);
var_dump($key);
/*
int(2)
*/

値が文字列の “3" と等しいのは実行結果 #2(キー:2) 、と言う事が分かる。

そして、この実行結果のキーは大元の $data のキーと等しいため、array_search で得られたキーを $data のキーとする事で、検索元のデータセット(前述の<レコード内容>)が取得できる、と言う仕組みだ。

$row = $key !== false ? $data[$key]: false;

$key には 0 も false も入る可能性があるので、厳密な比較が必要な事も忘れないように。

 

関連記事

PHPの多次元配列でSQL検索っぽい事をする