読者です 読者をやめる 読者になる 読者になる

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

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

Ginqでカッコ良く配列を操作する

PHP Ginq

出向先にいる優秀なフリーランスプログラマに教えてもらったGinqという面白いライブラリのご紹介です。

GinqはLINQにインスパイアされたPHPのライブラリです。

LINQを知らないという人は次をどうぞ。

統合言語クエリ - Wikipedia

Ginqのインストール

  • GitHubからソースをダウンロードします。

github.com

  • srcフォルダを適当なところに配置します。
  • Ginqを使用したいスクリプトの中でsrc/Ginq/Ginq.phpをインクルードします。
<?php
require_once 'src/Ginq/Ginq.php';

Ginqの勉強方法

基本的な使い方はGitHub - akanehara/ginq: `LINQ to Object` inspired DSL for PHPに記載されています。

ただそれだけだと不十分なので、個人的にオススメなのはtest/GinqTest.phpのテストケースに目を通すことです。
GinqTest.phpには短いコードでGinqメソッドの使用した例とその結果が数多く記述されています。

Ginqの使用例①

<?php

// この配列から、、、
$list = array(
	array('id' => 1,  'type' => 1, 'score' => 1952),
	array('id' => 2,  'type' => 1, 'score' => 23523),
	array('id' => 3,  'type' => 1, 'score' => 9832),
	array('id' => 4,  'type' => 2, 'score' => 85322),
	array('id' => 5,  'type' => 2, 'score' => 9149),
	array('id' => 6,  'type' => 2, 'score' => 33),
	array('id' => 7,  'type' => 3, 'score' => 185),
	array('id' => 8,  'type' => 3, 'score' => 25981),
	array('id' => 9,  'type' => 3, 'score' => 456),
	array('id' => 10, 'type' => 3, 'score' => 78881),
);

// 次のようにtype=2を除くtypeの中で最大のsocreを持つ要素を抽出したい。
$maxScoreList = array(
	array('id' => 2,  'type' => 1, 'score' => 23523),
	array('id' => 10, 'type' => 3, 'score' => 78881),
);

「グループ単位で、あるルールに従って最大のレコードを抽出する」ためのサンプルを紹介します。

まずはPHPのみで書いてみました。

<?php

$maxScoreList = array();
foreach ($list as $item) {
	$type = $item['type'];
	// type=2の要素を除外する。
	if ($type == 2) {
		continue;
	}
	// $typeが一度登場している場合はscoreを比較する。
	if (isset($maxScoreList[$type])) {
		// scoreが最大の要素で置換する。
		if ($maxScoreList[$type]['score'] < $item['score']) {
			$maxScoreList[$type] = $item;
		}
	// $typeが初登場の場合は戻り値の配列に追加する。
	} else {
		$maxScoreList[$type] = $item;
	}
}
// インデックスを採番し直す。
$maxScoreList = array_values($maxScoreList);

もう少しコンパクトに書くこともできますが、可読性を考えたらこれぐらいに留めておいた方がいいかな?
最後のarray_valuesは歯抜けになった配列のインデックスを0からの連番にするためによくやる手法です。

次はGinqを使った例です。

<?php

$maxScoreList = Ginq::from($list)
	// type=2の要素を除外する。
	->where(function ($v) { return $v['type'] != 2; })
	// typeが同じ要素をグルーピングする。
	->groupBy(function ($v) { return $v['type']; })
	// $grにはグルーピングした要素の塊が渡ってくる(例えばtype=1の要素だけとか)
	->select(function ($gr) { 
		// socreが最大の要素を取得する。
		return $gr->maxWith(function ($v1, $v2) { return $v1['score'] - $v2['score']; });
	})
	// インデックスを採番し直す。
	->renum()
	->toArray();

ループを使っていないのがカッコイイと思うのは私だけかな(;;´(ェ)`)
Unixのパイプのように、データの形がメソッドチェーンで実行する度に変わる様子がイメージできるので、自分はこっちの書き方の方が好きです。
あとメソッドがSQLのキーワードと同じ名前になっていることが多いので、SQLを知っているならメソッドがやっていることを直感的に理解しやすいです。

Ginqの使用例②

<?php

// curlでYahoo!のレスポンスヘッダの情報を取得する。
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,            'http://www.yahoo.co.jp');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET,        true);
curl_setopt($ch, CURLOPT_HEADER,         true);
curl_setopt($ch, CURLOPT_NOBODY,         true);
$result = curl_exec($ch);
curl_close($ch);

// $resultにレスポンスヘッダの文字列が入っているのでそれを配列に変換する。
// 具体的には次のようなレスポンスヘッダの文字列を、、、
$result = "HTTP/1.1 200 OK
Server: nginx
Date: Thu, 30 Jun 2016 03:21:03 GMT
Content-Type: text/html; charset=UTF-8
・・・
";

// array(ヘッダ名 => ヘッダ値)の配列に変換したい。
$headerList = array(
	'Server'       => 'nginx',
	'Date'         => 'Thu, 30 Jun 2016 03:21:03 GMT',
	'Content-Type' => 'text/html; charset=UTF-8',
);

次は前の例よりも少し実践的な内容で、Webページのレスポンスヘッダの文字列をパースして連想配列に変換するサンプルを紹介します。

まずはPHPのみの場合です。

<?php

// 改行文字列を配列に変換する。
$resultItemList = explode("\n", trim($result));
$headerList = array();
foreach ($resultItemList as $resultItem) {
	// 「ヘッダ名: ヘッダ値」の形の文字列を、ヘッダ名とヘッダ値に分割する。
	list($name, $value) = explode(':', trim($resultItem), 2);
	// ヘッダ値が存在したら戻り値配列に追加する。
	if (isset($value)) {
		$headerList[$name] = $value;
	}
}

explode関数の第三引数の2は、分割数を最大で2にするという意味です。
コロンが複数あった場合でも最初に見つかったコロンで文字列が2分割されます。

次はGinqを使用した例です。

<?php

$headerList = Ginq::from(explode("\n", $result))
	// 「ヘッダ名: ヘッダ値」の形の文字列を、array(ヘッダ名、ヘッダ値)に変換する。
	->select(function($v) { return explode(':', trim($v), 2); })
	// :をデリミタとして2分割できていないものは除外する。
	->where(function($v) { return count($v) == 2; })
	// array(ヘッダ名、ヘッダ値)をarray(ヘッダ名 => ヘッダ値)に変換する。
	->select(function($v) { return trim($v[1]); }, function($v) { return $v[0]; })
	->toArray();

(´(ェ)`)・・・
もしかしたら、これはPHPだけのほうがわかりやすいかも(汗
ただし、もう少し複雑な加工処理が入るとGinqに軍配があがるかもしれません。

パフォーマンスについて

今の現場では、数百万以上のユーザがいるソシャゲのAPIでGinqを使用しています。
Ginqはよく遅いと言われていますが、体感的に遅いと思ったことは一度もありません。
(扱う配列のサイズがそれほど大きくないというのもありますが、、、)
PHP7で高速化も見込めるのでパフォーマンスはそれほど気にする必要がないのかなぁという印象です。