CakePHPのHashクラスをJavaScriptに移植した「cake-hash.js」を作ってみた
最近フロントエンドが楽しくて、なかなかPHP
を書いていませんでした。
ふと、以前にCakePHP
で作ったWebアプリを見なおしてみると、Hash
クラス便利だったなぁ〜と思いました。
どんな感じの実装になってるのか、ソースを読んでみるとJavaScript
にも簡単に持ってこれそうだったので、全てのコードをES6
で書いて移植してみました。
IE9
以上、その他はモダンブラウザ、Node
上で動作します。IE8
以下は無かったこととします。
概要
そもそもCakePHP
のHash
クラスをご存知無い方のために。
CakePHP
では、モデルから取得するデータ、その他設定データなどが連想配列で構造化されています。そうすると、どんどんデータが複雑な構造になりがちです。
そして、そんな複雑化したデータを簡単に管理する為に、Hash
クラスが活躍しています。
(上記は2.x
系での知識で、3.x
系からはエンティティが出来たので多少楽になったかもです…!)
ちなみに、最近のフロントエンドでも同じで、どんどんデータ構造が複雑化してきているような気がします。
移植版の「cake-hash.js
」でも、本家とほとんど同じ使い方ができます。
以下は簡単なサンプルです。
import CakeHash from "cake-hash"
let users = [
{id: 1, name: "mark"},
{id: 2, name: "jane"},
{id: 3, name: "sally"},
{id: 4, name: "jose"}
];
let result = CakeHash.extract(users, "{n}.id");
console.log(result); // [1, 2, 3, 4]
{n}
の様な特殊なパス構文もそのまま使用できます。
パス構文について
上記で少し触れましたが、柔軟なパスの指定が可能となっています。
以下の構文は各メソッドで使用できます。
式の種類
以下の様な式を使用できます。
{n}
– 数値キーを意味します。{s}
– 文字列キーを意味します。Foo
– 完全に同じ値だった場合のみ一致します。
属性値での絞り込み
式に加えて、属性値での絞り込みが行えます。
[id]
– 記述されたキーと一致する要素に絞り込みます。[id=2]
–id
が2
となっている要素に絞り込みます。[id!=2]
–id
が2
ではない要素に絞り込みます。[id>2]
–id
が2
より大きい要素に絞り込みます。[id>=2]
–id
が2
以上の要素に絞り込みます。[id<2]
–id
が2
より小さい値に絞り込みます。[id<=2]
–id
が2
以下の要素に絞り込みます。[text=/.../]
– 正規表現...
とマッチする値をもった要素に絞り込みます。
セパレータ文字のエスケープについて
本家CakePHP
のHash
クラスと同様に、cake-hash.js
でも配列やオブジェクトを掘っていくのにドットシンタックスを使用します。
キーの中にドットが含まれている場合でも、ガンガン掘り進められるようにするためには、ドットをバックスラッシュ(\
)でエスケープします。
const data = {
"index.html": {
css: {
"style.css": "* {box-sizing: border-box}"
}
}
};
let result = CakeHash.get(data, "index\\.html.css.style\\.css");
console.log(result); // * {box-sizing: border-box}
この点は本家に無いオリジナルな挙動になっています。
インストール
npm
からのインストールに対応しています。
$ npm install cake-hash
リポジトリから直接ファイルを持ってきて、<script>
で読み込んでも使えます。
<script src="./cake-hash.min.js"></script>
リポジトリは以下です。
使い方
JavaScript
では、既にUnderscore.jsやlodashの様な強力なライブラリがあるので、そちらで代替できるものは移植していません。
サポートしているメソッドの一覧です。
get
extract
insert
remove
combine
check
flatten
expand
map
reduce
以下、メソッド毎の使い方をちょいちょい省きながらご紹介です!
README
にも書いていますが念のため。
get(data, path, [defaultValue = null])
data
: array | object
path
: string
defaultValue
: mixed
return
: mixed
get()
は後述のextract()
のシンプル版です。{n}
や{s}
の様なマッチャをサポートしませんが、その分早く要素へアクセスできます。
let users = [
{id: 1, name: "mark"},
{id: 2, name: "jane"},
{id: 3, name: "sally"},
{id: 4, name: "jose"}
];
let result = CakeHash.get(users, "2.name");
console.log(result); // "sally"
result = CakeHash.get(users, "hoge.fuga", "default!!");
console.log(result); // default!!
extract(data, path)
data
: array | object
path
: string
return
: mixed
extract()
は全てのパス構文とマッチャをサポートします。複雑なデータの取得に有用です!サンプルは最初に書いたものと同じものです。
let users = [
{id: 1, name: "mark"},
{id: 2, name: "jane"},
{id: 3, name: "sally"},
{id: 4, name: "jose"}
];
let result = CakeHash.extract(users, "{n}.id");
console.log(result); // [1, 2, 3, 4]
insert(data, path, [value = null])
data
: array | object
path
: string
value
: mixed
return
: mixed
data
をpath
の定義に沿って配列(又はオブジェクト)に挿入します。
let data = {
pages: {name: "page"}
};
let result = CakeHash.insert(data, "files", {name: "file"});
console.log(result);
/*
{
pages: {name: "page"},
files: {name: "file"}
}
*/
{n}
や{s}
などののパス構文を使用することで、複数の箇所に値を挿入できます。
users = CakeHash.insert(users, "{n}.new", "value");
以下のようにして、属性値を使った絞り込みも可能です。
let data = [
{up: true, item: {id: 1, title: "first"}},
{item: {id: 2, title: "second"}},
{item: {id: 3, title: "third"}},
{up: true, item: {id: 4, title: "fourth"}},
{item: {id: 5, title: "fifth"}}
];
let result = CakeHash.insert(data, "{n}[up].item[id=4].new", 9);
console.log(result);
/*
[
{up: true, item: {id: 1, title: "first"}},
{item: {id: 2, title: "second"}},
{item: {id: 3, title: "third"}},
{up: true, item: {id: 4, title: "fourth", new: 9}},
{item: {id: 5, title: "fifth"}}
]
*/
remove(data, path)
data
: array | object
path
: string
return
: mixed
配列、またはオブジェクトの中から、path
に一致する要素を削除します。
let data = {
pages: {name: "page"},
files: {name: "file"}
};
let result = CakeHash.remove(data, "files");
console.log(result);
/*
{
pages: {name: "page"}
}
*/
パス構文やマッチャの指定も可能です。
let data = [
{clear: true, item: {id: 1, title: "first"}},
{item: {id: 2, title: "second"}},
{item: {id: 3, title: "third"}},
{clear: true, item: {id: 4, title: "fourth"}},
{item: {id: 5, title: "fifth"}}
];
let result = CakeHash.remove(data, "{n}[clear].item[id=4]");
console.log(result);
/*
[
{clear: true, item: {id: 1, title: "first"}},
{item: {id: 2, title: "second"}},
{item: {id: 3, title: "third"}},
{clear: true},
{item: {id: 5, title: "fifth"}}
]
*/
combine(data, keyPath, [valuePath = null, groupPath = null])
data
: array | object
keyPath
: string
valuePath
: string
groupPath
: string
return
: array | object
keyPath
のパスをキー、valuePath
のパスを値として使い、配列、又はオブジェクトを作ります。
groupPath
が指定された場合は、そのパスに従って生成したものをグループ化します。
本家のHash
クラスでは、値のフォーマットができますが、cake-hash.js
では未対応です。
let data = [
{
user: {
id: 2,
group_id: 1,
data: {
user: "mariano.iglesias",
name: "Mariano Iglesias"
}
}
},
{
user: {
id: 14,
group_id: 2,
data: {
user: "phpnut",
name: "Larry E. Masters"
}
}
}
];
result = CakeHash.combine(data, "{n}.user.id", "{n}.user.data.name");
console.log(result);
/*
[2: "Mariano Iglesias", 14: "Larry E. Masters"]
*/
result = CakeHash.combine(data, "{n}.user.id", "{n}.user.data.name", "{n}.user.group_id");
console.log(result);
/*
[
1: {
2: "Mariano Iglesias"
},
2: {
14: "Larry E. Masters"
}
]
*/
check(data, path)
data
: array | object
path
: string
return
: boolean
指定したパスがセットされているかチェックします。
let data = {
"My Index 1": {
first: {
second: {
third: {
fourth: "Heavy. Nesting."
}
}
}
}
};
result = CakeHash.check(data, "My Index 1.first.second");
console.log(result); // true
result = CakeHash.check(data, "My Index 1.first.second.third");
console.log(result); // true
result = CakeHash.check(data, "My Index 1.first.second.third.fourth");
console.log(result); // true
result = CakeHash.check(data, "My Index 1.first.seconds.third.fourth");
console.log(result); // false
// 分かりづらいですが、"seconds"にしています!
flatten(data, separator = “.”)
data
: array | object
separator
: string
return
: array | object
多次元配列、オブジェクトを一次元なフラットな構造にします。
let data = [
{
post: {id: 1, title: "First Post"},
author: {id: 1, user: "Kyle"}
},
{
post: {id: 2, title: "Second Post"},
author: {id: 3, user: "Crystal"}
}
];
let result = CakeHash.flatten(data);
console.log(result);
/*
{
"0.post.id" : 1,
"0.post.title" : "First Post",
"0.author.id" : 1,
"0.author.user": "Kyle",
"1.post.id" : 2,
"1.post.title" : "Second Post",
"1.author.id" : 3,
"1.author.user": "Crystal"
}
*/
expand(data, separator = “.”)
data
: array | object
separator
: string
return
: array | object
flatten()
でフラットにした構造を展開します。
let data = {
"0.post.id" : 1,
"0.post.title" : "First Post",
"0.author.id" : 1,
"0.author.user": "Kyle",
"1.post.id" : 2,
"1.post.title" : "Second Post",
"1.author.id" : 3,
"1.author.user": "Crystal"
};
let result = CakeHash.expand(data);
console.log(result);
/*
[
{
post: {id: 1, title: "First Post"},
author: {id: 1, user: "Kyle"}
},
{
post: {id: 2, title: "Second Post"},
author: {id: 3, user: "Crystal"}
}
]
*/
map(data, path, callback)
data
: array | object
path
: string
callback
: function
return
: array | object
指定したパスで返ってくる要素に対して、コールバックを適用して新しい配列を作ります。
let data = [
{user: {id: 1, name: "Adam"}},
{user: {id: 2, name: "Clyde"}},
{user: {id: 3, name: "Cyril"}},
{user: {id: 4, name: "Thomas"}},
{user: {id: 5, name: "William"}}
];
let result = CakeHash.map(data, "{n}.user.id", (id) => id * 2);
console.log(result); // [2, 4, 6, 8, 10]
reduce(data, path, callback)
data
: array | object
path
: string
callback
: function
return
: array | object
指定したパスで返ってくる要素で、隣り合う2つの要素に対して同時にコールバックを適用して、単一の値にします。
let data = [
{user: {id: 1, name: "Adam"}},
{user: {id: 2, name: "Clyde"}},
{user: {id: 3, name: "Cyril"}},
{user: {id: 4, name: "Thomas"}},
{user: {id: 5, name: "William"}}
];
let result = CakeHash.reduce(data, "{n}.user.id", (one, two) => one + two);
console.log(result); // 15
おわりに
あまり本筋には関係ありませんが、初めてちゃんとテストを書いて、レッドな状態から実装して、グリーンに持っていくみたいなTDD
な感じで開発を進められました!
power-assert
さまさまでした。
また、ES6
で書いてブラウザ、Node
両対応にするためにrollup
を使ってみました。
browserify
でバンドルしたファイルに比べて、すごいクリーンなコードが吐かれて感動しました。
こちらの使い方は今度記事に書いてみようと思います。
バグや機能についてのツッコミなどありましたら教えて下さいませ。。