TOYOKUMO Tech Blog

トヨクモ株式会社の開発本部のブログです。

kintone REST API向けのPHPのクエリビルダを作りました

開発本部の齊藤です。kintone REST API向けのPHPのクエリビルダkintone-query-builderを作成しました。 レコードの取得 (GET) にある、「レコードの一括取得(クエリで条件を指定)」のパラメータqueryのためのクエリビルダーです。 ソースコードはMITライセンスで公開してありますので、kintone連携製品の開発者の方はよかったら使ってみてください。 packagistに登録してありますので、composer require toyokumo/kintone-query-builderで使うことができます。

ユースケース

サイボウズスタートアップスでは、kViewerで実際に使用しています。既存のパラメータqueryを文字列連結で地道に組み立てるモジュールを、kintone-query-builderで書き直しました。リファクタリングの結果として、より可読性の高いコードにすることができました。改変してありますが、クエリビルダー導入前のコードと、導入後のコードを比較してみたいと思います。

比較

以前のコード

<?php
foreach ($filters as $filter) {

    $with = $filter['conj'] ?? 'and';
    if (substr($query, -1) === '(') {
        $with = '';
    }
    $query .= " {$with} " . $this->makeQuery($filter);
}

この if(substr($query,-1)==='(')は今まで貯めてきたクエリがあるかどうかを判定していますが、こういった地道な文字列操作をなくすことができました。

<?php
foreach ($filters as $filter) {

    if ($with === 'and') {
        $builder->andWhere($subBuilder);
    } else {
        // $with=='or'
        $builder->orWhere($subBuilder);
    }
}

また、kintone apiの仕様では、time = NOW()のような構文で関数を使うことができるのですが、クエリビルダを使用しない場合、右辺の値が関数かどうかを調べるコードが必要になります。

<?php
        if (
            preg_match(
                '/^(NOW\(\))|(TODAY\(\))|.../',
                $filter['val']
            )
        ) {
            $val = $filter['val'];
        } else {
            $val = "\"{$filter['val']}\"";
        }

この処理はクエリビルダがやってくれるので、すべて消すことができました。

例(レコードの全取得)

kintone apiの仕様として、1回のリクエストにつき500件のレコードしか取得できない、という制約があります。kintone query builderを使用して特定の上限を満たすレコードを全取得するコードは以下になります。

<?php
$builder = (new KintoneQueryBuilder())->where(...);
$records = $api->fetch($builder.build());
// do something
$offset = 0;
$records_max = 500;
while(!\empty($records)) {
    // do something
    $offset+=$records_max;
    $records = $api->fetch($builder->offset($offset)->build());
}

offsetの指定が違うだけで、条件指定の部分が同じクエリを複数発行することになりますが、$builderを使いまわすことができています。

詳しい使い方

READMEを引用します。

<?php
use KintoneQueryBuilder\KintoneQueryBuilder;
use KintoneQueryBuilder\KintoneQueryExpr;
// example
// すべての演算子(=, !=, like, not like, <, >, <=, >=, in, not in)が使えます
(new KintoneQueryBuilder())->where('name', '=', 'hoge')->build();
// => 'name = "hoge"'
(new KintoneQueryBuilder())
    ->where('favorite', 'in', ['apple', 'banana', 'orange'])
    ->build();
// => 'favorite in ("apple","banana","orange")'
(new KintoneQueryBuilder())
    ->where('age', '>', 10)
    ->andWhere('name', 'like', 'banana') // かわりにwhereと書くことができます(where = andWhere)
    ->andWhere('name', '!=', 'banana')
    ->build();
// => 'age > 10 and name like "banana" and name != "banana"'
(new KintoneQueryBuilder())
    ->where('age', '>', 20)
    ->orderBy('$id', 'desc')
    ->limit(50)
    ->build();
// => 'age > 20 order by $id desc limit 50'
(new KintoneQueryBuilder()) // ネストしたクエリには、KintoneQueryExprを$builder->whereの引数として渡してください。
->where(
    (new KintoneQueryExpr())
        ->where('a', '<', 1)
        ->andWhere('b', '<', 1)
)->orWhere(
    (new KintoneQueryExpr())
        ->where('c', '<', 1)
        ->andWhere('d', '<', 1)
)->build();
// => '(a < 1 and b < 1) or (c < 1 and d < 1)'
(new KintoneQueryBuilder())->where('x', '=','ho"ge')->build()
// ダブルクオートはエスケープされます
// => 'x = "ho\"ge"'

今後も継続的にメンテナンスしていきます。また、今後JavaとJavascriptへポートする予定です。バグがありましたら、GitHubリポジトリのIssueで報告していただけるとありがたいです。

与太話

kintoneのapiの仕様が明確に書かれていないところがあり、テスト環境でクエリを投げてapiと対話しながら、仕様を明確にしていく作業が必要でした。 ダブルクオートのエスケープが必要であること、order byを複数のカラムについてかける方法(ordrer by x desc, y asc などとする)、like検索の仕様など、いくつか気になる点がありました。 ドキュメント のコメント欄を漁る必要があり調べるのが手間だったので、kintone本体の開発者の方には、コメントの内容を必要であればドキュメントにも追加していただきたいです。