ダメプログラマの技術メモ

プログラミングの技術メモや駄文など

CakePHP2.x系で多次元(三次元)配列をソートする

array_multisort関数を使用した二次元配列のソートはネット上によくあるのですが、三次元配列のソートに言及しているところはほとんどなかったので自作してみました。

CakePHPはモデルメソッドの戻り値が[インデックス][モデル名][フィールド名]のような三次元配列で表現されることが多いので利用できるシチュエーションは多いと思います。

また、CakePHPにはHash::sortという多次元配列をソートできるメソッドが存在するのですが、ソート対象のフィールドを1つしか指定できないため若干不便です。そのため自作したメソッドは何個でもフィールドを指定できるようにしています。

サンプルプログラム

ソートメソッドの使用例

<?php
public function index() {

	// ソート対象配列(Model::findの戻り値を想定)
	$models = array();

	// 1レコード目
	$models[0]['A']['field_a_1'] = '4';
	$models[0]['A']['field_a_2'] = '1';
	$models[0]['B']['field_b_1'] = '2';
	// 2レコード目
	$models[1]['A']['field_a_1'] = '30';
	$models[1]['A']['field_a_2'] = '1';
	$models[1]['B']['field_b_1'] = '4';
	// 3レコード目
	$models[2]['A']['field_a_1'] = '30';
	$models[2]['A']['field_a_2'] = '2';
	$models[2]['B']['field_b_1'] = '6';
	// 4レコード目
	$models[3]['A']['field_a_1'] = '9';
	$models[3]['A']['field_a_2'] = '3';
	$models[3]['B']['field_b_1'] = '6';

	// 多次元配列ソート
	$result = Common::sortArr($models, '{n}.A.field_a_1', SORT_ASC, '{n}.B.field_b_1', SORT_DESC);
	debug($result);
/*
	array(
		(int) 0 => array(
			'A' => array(
				'field_a_1' => '4',
				'field_a_2' => '1'
			),
			'B' => array(
				'field_b_1' => '2'
			)
		),
		(int) 1 => array(
			'A' => array(
				'field_a_1' => '9',
				'field_a_2' => '3'
			),
			'B' => array(
				'field_b_1' => '6'
			)
		),
		(int) 2 => array(
			'A' => array(
				'field_a_1' => '30',
				'field_a_2' => '2'
			),
			'B' => array(
				'field_b_1' => '6'
			)
		),
		(int) 3 => array(
			'A' => array(
				'field_a_1' => '30',
				'field_a_2' => '1'
			),
			'B' => array(
				'field_b_1' => '4'
			)
		)
	)
*/
	exit;
}

Common::sortArrがソートを行うメソッドです。

この手のコントローラやモデルなど様々なところで利用されるメソッドはapp/Vendorディレクトリの下にCommonクラス(名前は何でもよい)を作成して、その中でメソッドを定義すると使い勝手がよいと思います。

Common::sortArrメソッドの引数

第一引数にはソート対象配列を指定します。

第二引数(第四引数、、)はソート対象フィールドをCakePHPのHashパスで指定します。
Hashパスがわからない方は下のサイトを参考にして下さい。

■Hash — CakePHP Cookbook 2.x ドキュメント
http://book.cakephp.org/2.0/ja/core-utility-libraries/hash.html

Hashクラスには配列を操作するための便利なメソッドが沢山あり、特にHash::extractは本当によく使用します!!

第三引数(第五引数、、)は昇順ソートしたい場合はSORT_ASC、降順ソートしたい場合はSORT_DESCを指定します。

ソートメソッドの定義

<?php
/**
 * 多次元配列ソート
 * @param ソート対象配列、Hashパス、ソート方法(Hashパス、ソート方法は可変)
 * @return ソート後の多次元配列
 */
public static function sortArr() {

	// 引数取得
	$args = func_get_args();

	// ソート対象配列取得
	$data = array_shift($args);

	// ソート対象配列のキー取得(これをソートする)
	$orgKeys = array_keys($data);

	// ソート対象配列のキーをソート
	$params = array();
	$argc = count($args);
	for ($i = 0; $i < $argc; $i += 2) {
		$params[] = Hash::extract($data, $args[$i]);
		$params[] = $args[$i + 1];
		$params[] = SORT_NATURAL;
	}
	$params[] = &$orgKeys;
	call_user_func_array('array_multisort', $params);

	// ソート後のソート対象配列のキーを取得
	$sortedData = array_pop($params);

	// ソート後のキーを使用してソート対象配列をソートする
	$result = array();
	foreach ($sortedData as $s) {
		$result[] = $data[$s];
	}

	return $result;
}

結局、array_multisort関数を使用してソートしています(;´・ω・)

配列そのものではなく、配列のキーをソートしているのがポイントです。

ソート後はソートされたキーの順番で配列要素を取り出して、あたかも配列自体をソートしたかのように見せています。

あと「SORT_NATURAL」は自然順を意味しており、以下のサイトのarray1_sort_flagsのフラグ定数であれば何でも設定できます。

PHP: array_multisort - Manual
http://www.php.net/manual/ja/function.array-multisort.php

これについては状況に応じて「SORT_REGULAR」や「SORT_NUMERIC」などを設定してください。

パラメータにして外部から渡すようにした方が汎用性が高まってよいかもしれません。

最後に

このメソッドを使うメリットの一つは、Model::findメソッドなどでORDER BY句を指定する必要がなくなるということです。

インデックスを張っていないカラムをORDER BY句に含めると、EXPLAINのExtraフィールドにusing filesortが発生することがあります。

処理件数が少ない場合であればメモリ内で処理が完結するので気にしなくてよいですが、処理件数が多い場合は対策を講じる必要があります。

一番簡単な対策はインデックスを張ることですが、ディスク容量の増加・更新処理による負荷の増大などを考慮する必要があります(ここら辺はサーバスペック次第ですが。。)

またソーシャルゲームでありがちなカードのID順・レアリティ順・入手順などのように複数のソートがある場合、その全てに対応したインデックスを張るのは現実的ではありません。

そういった場合や少しでもDB負荷を軽くしたい場合は、SQLではなくアプリ側でソート処理を行った方がよいと思います^^