>>CakePHP 2.2 から コアが提供する Utility のライブラリに新たに class Hash が加わりました。

次回のメジャーバージョンアップでは Set クラスが廃止され、Hash クラスに移行するとのことです。

http://bakery.cakephp.org/articles/markstory/2012/04/30/cakephp_2_1_2_2_2_0-beta_released

New Hash class

A new utility library Hash was added. It is intended as a replacement for the Set class featuring improved performance, and a more consistent API. All internal calls to Set were replaced and Set has been deprecated and will be removed in the next major version.

内部的にこれまでの Set クラスを置き換えたものになっており、実際コアライブラリのほとんどの呼び出しでは Set から Hash に移行しています。

便利なものも多いので、再利用可能なメソッドはどんどん使っていきたいですね。個人的には Hash::extract や Hash::combine などをよく使います。

例えば、同じ対象のデータなのにModel::find( ‘list’ ) や Model::find( ‘threaded’ ) などを Model::find( ‘all’ ) と併せて呼び出すような場合は、オーバーヘッドを避けるためにModel::find( ‘all’ ) したデータを手元において置いて、それを Hash::combine や Hash::nest で組み立てる方法を知っておくといいかも知れません。

以下に新旧メソッドの比較とあわせて簡単な説明、場合によってはソースコードやサンプルコードを掲載しました。適宜追加・更新されていますので、ページの内容は変更する可能性があります。

詳しい使い方はこちらの公式マニュアルで
http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html

メソッド 引数 戻り値 備考

apply

>>API

array $data
$path
$function
 mixed $path が指示する $data (=Hash::extract の結果) に対し
call_user_func して、その結果を返す。
Hash::map の場合とは異なり、コールバックに渡されるのは抜き出したものを配列にしたもの。
Hash::map の場合は逐次コールバックに渡される。
[参照]Hash::map, Hash::reduceコアでは使用されていない模様。

check

>>API

array $data
string $path=null
boolean $path の位置にデータがセットされているかを返す。
ソースを見れば分かりますが、いったん Hash::extract( $data, $path ) を実行して、その戻り値から判定しています。
それなりにコストのかかるプロセスのようなので、 is_set を使用できる場合は素直にそちらを用いた方がよさそうです。
少なくともこんな使い方は同じ処理を2回やってるんで無駄

if( Hash::check( $data, $path ) ) {
    $a = Hash::extract( $data, $path );
}

ちなみに 2.2.1 の時点のコアでこれを用いている個所は見当たりませんでした。

classicExtract Set にて廃止

combine

>>API

array $data
$keyPath=null
$valuePath=null
$groupPath=null
 array 複雑な配列をいい感じに連想配列に加工できる
Model と TreeBehavior で使われている
Sample
モデルのキーと値で連想配列を作成するときによくModel::find(‘list’)を使いますが、もっと複雑なことをしたい場合や、普通にfindしたデータが手元にあるので、それを利用してリストを作りたい場合なんかに使えます。※実際 Model::find(‘list’) でもこのメソッドが用いられている。

$data = array(
	'Post' => array(
		'id' => 3,
		'title' => 'Hi!',
		'content' => 'I made a chocolate cake!',
		'Author' => array(
			'id' => '77',
			'name' => 'Boo',
		),
		'Comment' => array(
			array(
				'id' => '123',
				'comment' => 'Good!',
				'Commenter' => array(
					'id' => '22',
					'name' => 'Foo',
				),
			),
			array(
				'id' => '124',
				'comment' => 'Thank you!',
				'Commenter' => array(
					'id' => '77',
					'name' => 'Boo',
				),
			),
			array(
				'id' => '125',
				'comment' => 'I\'m hungry.',
				'Commenter' => array(
					'id' => '55',
					'name' => 'Woo',
				),
			),
			array(
				'id' => '126',
				'comment' => 'Come on a my house!',
				'Commenter' => array(
					'id' => '77',
					'name' => 'Boo',
				),
			),
		),
	),
);

$keyPath = 'Post.Comment.{n}.id';
$valuePath = 'Post.Comment.{n}.Commenter.name';
$combined = Hash::combine( $data, $keyPath, $valuePath );
print_r( $combined );

$valuePath = 'Post.Comment.{n}.comment';
$groupPath = 'Post.Comment.{n}.Commenter.id'; // コメンターのIDでグループ化する
$combined = Hash::combine( $data, $keyPath, $valuePath, $groupPath );
print_r( $combined );

?>
<pre><?php print_r( $combined ); ?></pre>

この結果は

Array
(
    [123] => Foo
    [124] => Boo
    [125] => Woo
    [126] => Boo
)
Array(
    [22] => Array
        (
            [123] => Good!
        )
    [77] => Array
        (
            [124] => Thank you!
            [126] => Come on a my house!
        )
    [55] => Array
        (
            [125] => I'm hungry.
        )
)

contains

>>API

array $data
array $needle
boolean in_array の配列バージョンのようなもの。パラメータの順番は逆。
コアでの使用は見当たらず。
countDim Hash::dimensions へ移行

diff

>>API

 array $data
$compare
array 二つの配列を{キー:値}で比較して、異なるもののみを併せて返す。
同じキーで値が異なる場合は、内部で $data + $compare されているので前者が優先される。
コアライブラリでは、 PaginatorComponent, DboSource で用いられている

dimensions

>>API

array $data integer 配列の次数を数えて返す。
最初のエントリしか検索しない。
不均一な深度を持つ配列の最大値を知りたい場合は Hash::maxDimensions を用いる。
enum Set にて廃止

expand

>>API

$data
$separator = ‘.’
array Setにおいても2.2で追加。
配列のキーをドット区切りで渡すと多次元の配列にネストして返す。
区切り文字は第2引数で指定可能。
これとほぼ反対のメソッドとしてHash::flattenがある。
Sample

$data = array(
    'My.name.is' => 'Yamada Taro'
);
$expanded = Hash::expand( $data );
// $expanded = array( 'My' => array( 'name' => array( 'is' => 'Yamada Taro' ) ) );

extract

>>API

array $data
$path
array 複雑なパスを指定して、配列内のデータを取得できる。
Set::extract とは引数の順番が逆なので注意。
単純なパスの場合は内部でHash::getがコールされる。
以下のコアライブラリが利用している
ConsoleShell, Acl/IniAcl, Model, Model/Permission, AclBehavior, DboSource, Email/CakeEmail
Hash の以下のメソッドからも頻繁に使用されている
combine, format, check, map, reduce, apply, sort, nest
これらのメソッドに限り、キーの指定に extract と同じ正規表現を使用可能
Comments of the source

Gets the values from an array matching the $path expression.
The path expression is a dot separated expression, that can contain a set
of patterns and expressions:

- `{n}` Matches any numeric key, or integer.
- `{s}` Matches any string key.
- `Foo` Matches any key with the exact same value.

There are a number of attribute operators:

 - `=`, `!=` Equality.
 - `>`, `=`, `find('all')` call:

- `1.User.name` Get the name of the user at index 1.
- `{n}.User.name` Get the name of every user in the set of users.
- `{n}.User[id]` Get the name of every user with an id key.
- `{n}.User[id>=2]` Get the name of every user with an id key greater than or equal to 2.
- `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`.

@param array $data The data to extract from.
@param string $path The path to extract.
@return array An array of the extracted values.  Returns an empty array
  if there are no matches.

filter

>>API

array $data
$callback = array(‘self’,’_filter’)
array コールバックを指定して配列を再帰的にフィルタリングする。
コールバックを指定しない場合
($var === 0 || $var === ‘0’ || !empty($var))
で評価されなかったエントリがすべてふるいにかけられる。
DboSource, Router, Helper, FormHelper で使用されている

flatten

>>API

array $data
$separator=’.’
array 多次元配列のキーをドットで連結した単一キーの1次元配列にして返す。
デリミタは第2引数で変更可能
このほぼ反対のメソッドとして Hash::expand がある
コアライブラリでは Configure/IniReader, SecurityComponent, Acl/PhpAcl, Router, Routing/CakeRoute で使用されている
Sample

$data = array( 'My' => array( 'Favorite' => array( 'coffee', 'curry' ) ) );
$flatten = Hash::flatten( $data );
// $flatten = array( 'My.Favorite.0' => 'coffee', 'My.Favorite.1' => 'curry' );

format

>>API

array $data
array $path
$format
array 配列の任意の位置をパス指定して、フォーマットで vsprintf した値を返す
キーは通常の整数インクリメント

get

>>API

array $data
$path
mixed Hash::extract のシンプル版。パスの指定はキー文字(数値を含む)をドットで連結したものか配列のみ。{n},{s}などのワイルドカードやスラッシュ区切りの表現を使用できない。その分、プロセスが省エネ化されている。
複雑なパス指定はHash::extractを用いる。
Set::getも比較的最近追加された。
AclShell, AuthComponent, CookieComponent, IniAcl, Configure, Model, CakeSession, CakeRequest, Helper, FormHelper で使用されている。
Sample

// 以下のようなデータがあった場合
$data = array( 'Post' => array( 'Comment' => array( array( 'content' => 'Hello !' ) ) ) );
$firstComment = Hash::get( $data, 'Post.0.content' ); // このように取得できる。
$firstComment = Hash::get( $data, array( 'Post', 0, 'content' ) ); // 同じ結果

insert

>>API

array $data
$path
$values=null
array 第1引数の配列内の第2引数で指定したパスに対して、第3引数の値で更新した値を取得する。
{n}と{s}をワイルドカード的な表現で使用可能。
Hash::extractでは使用可能な簡単な演算(id>3など)はここでは適用不可。
IniReader, CookieComponent, Configure, CakeSession, CakeRequest で使用されている
Sample

<?php
$data = array(
	'Post' => array(
		array(
			'content' => 'Post-1',
		),
		array(
			'content' => 'Post-2',
		),
	),
	'Widget' => array(
		array(
			'content' => 'Widget-1',
		),
	),
	array(
		'content' => 'data-content',
	),
);
$path = '{s}.1.content';
$values = 'Inserted';
$inserted = Hash::insert( $data, $path, $values );
?><pre><?php print_r( $inserted ); ?></pre>

この結果は

Array
(
    [Post] => Array
        (
            [0] => Array
                (
                    [content] => Post-1
                )
            [1] => Array
                (
                    [content] => Inserted
                )
        )

    [Widget] => Array
        (
            [0] => Array
                (
                    [content] => Widget-1
                )

            [1] => Array
                (
                    [content] => Inserted
                )
        )

    [0] => Array
        (
            [content] => data-content
        )
)

map

>>API

array $data
$path
$function
array Hash::extract($data, $path) で指定した配列のデータに対して array_map( $function, $array ) した結果を返す。
Hash::apply の場合とは異なり、こちらはコールバックに逐次データが渡される。
Hash::apply は一旦配列にしたものをコールバックに渡す。
[参照] Hash::apply, Hash::reduce

maxDimensions

>>API

 array $data integer 配列の最大次数を返す。
matches Set にて廃止

merge

>>API

array $data
$merge
[$n …]
array (値がスカラー値ではなく配列である場合に限り)再帰的に array_merg させる。
php の array_merge と array_merge_recursive のどちらとも異なる。
CakePHPのコアではいたるところで使用されている。
Component/Auth/BaseAuthenticate, Component/Auth/BaseAuthorize, Configure, Object, Engine/ConsoleLog, Engine/FileLog, Model, ContainableBehavior, CakeSession, CakeRequest, HttpSocket, Debugger, Hash, NumberHelper, TextHelper, TimeHelper

mergeDiff

>>API

array $data
array $compare
array 二つの配列を比較して、差分を再帰的にマージする。
二次以上の配列に対応した array_merge に似た働きをする。
※いまひとつ使いどころが分からない
※二つの構造の比較工程の片方を省略している(後者が配列である場合に前者を確認しない)ので、引数を無造作に渡すとWarningが発生する恐れがある。前者が文字列で後者が配列の場合に発生する。
※整数キーを文字列キーと区別しない点において array_merge と異なる
I18n, DboSource で使用
The source code[Hash::mergeDiff](v2.2.1)

public static function mergeDiff(array $data, $compare) {
	if (empty($data) && !empty($compare)) {
		return $compare;
	}
	if (empty($compare)) {
		return $data;
	}
	foreach ($compare as $key => $value) {
		if (!array_key_exists($key, $data)) {
			$data[$key] = $value;
		} elseif (is_array($value)) {
			$data[$key] = self::mergeDiff($data[$key], $compare[$key]);
		}
	}
	return $data;
}

nest

>>API

array $data
array $options=array()
Options:

  • idPath = “{n}.$alias.id”
  • parentPath = “{n}.$alias.parent_id”
  • children = ‘children’
  • root = null
array $data を指定した条件で入れ子にして再構成した配列を返す。
Model::find( ‘threaded’ ) の振る舞いと言えば分かりやすいかも知れない。
というよりむしろ、そのために作られたメソッドのようなもの。

normalize

>>API

array $data
$assoc=true
array $data に整数キーのエントリがあった場合、それを”文字列” => null の形に再構成する。
整数キーに対応する値は内容を検査されずにそのまま配列のキーとされる。したがってそれはリテラルでなくてはならず、配列やオブジェクトを渡してしまった場合は Warning が発生する。
(※ソースコードのハイライト箇所を参照)
また、配列キーが競合した場合は後方が前方を上書きする。
protected Object::_mergeVars  に実装
the source code[Hash::normalize](v2.2.1)

public static function normalize(array $data, $assoc = true) {
	$keys = array_keys($data);
	$count = count($keys);
	$numeric = true;

	if (!$assoc) {
		for ($i = 0; $i < $count; $i++) {
			if (!is_int($keys[$i])) {
				$numeric = false;
				break;
			}
		}
	}
	if (!$numeric || $assoc) {
		$newList = array();
		for ($i = 0; $i < $count; $i++) {
			if (is_int($keys[$i])) {
				$newList[$data[$keys[$i]]] = null;
			} else {
				$newList[$keys[$i]] = $data[$keys[$i]];
			}
		}
		$data = $newList;
	}
	return $data;
}

Sample

<?php
$merge = array('components', 'helpers');
$normalized = Hash::normalize($merge);
var_dump($normalized);

// この結果は
array (size=2)
  'components' => null
  'helpers' => null

numeric

>>API

array $data boolean $data の要素がすべて 数字であるかどうかを返す。
値を文字列演算子”.”で結合したものを php の ctype_digit で判定している。そのため、負の値や小数点が含まれていると false となる。
要素に配列やオブジェクトを含めると Notice Error が発生する。
Model, DboSource, Database/Postgres で使用
Sample

<?php
// 文字列演算子で結合した結果に小数点が含まれるため偽を返す
$data = array('1.0','2.0');
$result = Hash::numeric($data);    // false

// 文字列演算子で結合する際に小数点以下は失われるため真を返す
$data = array(1.0, 2.0);
$result = Hash::numeric($data);    // true

$data = array('1', '2');
$result = Hash::numeric($data);    // true

$data = array(1.0, 2.3);
$result = Hash::numeric($data);    // false

$data = array(array(1.0)); 
$result = Hash::numeric($data);    // false & Notice Error
pushDiff Set にて廃止

reduce

>>API

array $data
$path
$function
array  Hash::extract($data, $path) で指定した配列のデータに対して array_reduce( $function, $array ) した結果を返す。
[参照] Hash::apply, Hash::map

remove

>>API

array $data
$path
array 第1引数の配列に対して、第2引数で指定したパスのデータをアンセットする。
{n}と{s}が使用可能。
CookieComponent, Configure, CakeSession で使用
Sample

<?php
$data = array(
	'Post' => array(
		array(
			'content' => 'Post-1',
		),
		array(
			'content' => 'Post-2',
		),
	),
	'Widget' => array(
		array(
			'content' => 'Widget-1',
		),
	),
	array(
		'content' => 'data-content',
	),
);

$path = '{s}.0';

$inserted = Hash::remove( $data, $path );
?>
<pre><?php print_r( $inserted ); ?></pre>

この結果は

Array
(
    [Post] => Array
        (
            [1] => Array
                (
                    [content] => Post-2
                )
        )

    [Widget] => Array
        (
        )

    [0] => Array
        (
            [content] => data-content
        )

)
reverse Set にて廃止

sort

>>API

array $data
$path
$dir
$type=’regular’
array
the source code[Hash::sort](v.2.2.1)

/**
* Sorts an array by any value, determined by a Set-compatible path
*
* ### Sort directions
*
* - `asc` Sort ascending.
* - `desc` Sort descending.
*
* ## Sort types
*
* - `numeric` Sort by numeric value.
* - `regular` Sort by numeric value.
* - `string` Sort by numeric value.
* - `natural` Sort by natural order. Requires PHP 5.4 or greater.
*
* @param array $data An array of data to sort
* @param string $path A Set-compatible path to the array value
* @param string $dir See directions above.
* @param string $type See direction types above. Defaults to 'regular'.
* @return array Sorted array of data
* @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort
*/
public static function sort(array $data, $path, $dir, $type = 'regular') {
	$originalKeys = array_keys($data);
	$numeric = is_numeric(implode('', $originalKeys));
	if ($numeric) {
		$data = array_values($data);
	}
	$sortValues = self::extract($data, $path);
	$sortCount = count($sortValues);
	$dataCount = count($data);

	// Make sortValues match the data length, as some keys could be missing
	// the sorted value path.
	if ($sortCount < $dataCount) {
		$sortValues = array_pad($sortValues, $dataCount, null);
	}
	$result = self::_squash($sortValues);
	$keys = self::extract($result, '{n}.id');
	$values = self::extract($result, '{n}.value');

	$dir = strtolower($dir);
	$type = strtolower($type);
	if ($type == 'natural' && version_compare(PHP_VERSION, '5.4.0', '