早いもので CakePHP3 がリリースされてから半年以上が経過しました。
革新的な人は、一足先に CakePHP3 の世界に飛び込んで、時には躓きながらも、すでに多くのノウハウを蓄積していることでしょう。保守的な人は、あるいはいまだに CakePHP1 の世界に留まっているかもしれませんね。
あなたが中庸な人であれば、CakePHP2 の世界に居ながらも、ためらいがちに新しい扉の向こう側を覗いているかもしれません――私のように。
今回、ご紹介する StackableFinder は、そんなあなたにぴったりのプラグインで、CakePHP3 の新機能 Query Builder の CakePHP2 における実装です。CakePHP2 のソースコードに目を通していた折りに、同機能をほんの少しのコード量で実装できることに気付いて、早速作成してみました。
この記事では Cookbook 3.x の例にならいながら、StackableFinder の使い方をご披露します。CakePHP2 の find()
がモダンに美しく生まれ変わります。
基本的な使い方
プラグインをインストールすると、StackableFinder を以下の方法で呼び出せるようになります。
// ArticlesController.php 内
$query = $this->Article->q();
CakePHP3 と同じように、メソッドチェーンでクエリを準備し、Iterator
により実行することができます。
$query = $this->Article
->q()
->select(['id', 'name'])
->where(['id !=' => 1])
->order(['created' => 'DESC']);
foreach ($query as $article) {
debug($article['Article']['created']);
}
または exec()
を呼び出すことで、foreach
を用いずにクエリの結果を得ることができます。
$articles = $this->Article
->q()
->where(['id >' => 1])
->exec();
ちなみに、これは CakePHP3 であれば all()
を呼ぶ場面ですが、CakePHP2 には Collection
クラスが存在せず、互換性のあるオブジェクトを返せないために実装していません。
なお、Collection
の代わりに配列を返す toArray()
については実装しており、こちらは exec()
とほぼ同じ動きをしますが、以下の例において挙動が異なります。
$query = $this->Article
->q()
->find('count');
debug( $query->exec() ); // 100 など
debug( $query->toArray() ); // array( 100 ) など
ファインダの中には find('list')
や find('threaded')
のように取得結果を加工するものがありますが、CakePHP3 ではそれらの返値は Collection
のような集合になり、スカラやエンティティを直接返すことはありません。この一貫性から外れる find('first')
や find('count')
は廃止されたため、CakePHP3 ではこの問題に遭遇することはありません。
なお、CakePHP3 では代わりに count()
や first()
など、find()
以外のメソッドでこれを行いますが、こちらについては、StackableFinder でも実装しています。
$article = $this->Article
->q()
->where(['id' => 1])
->first();
debug($article['Article']['title']);
first()
は find('first')->exec()
と、count()
は find('count')->exec()
とそれぞれ全く同一の結果を返します。
クエリ作成
現状、StackableFinder では以下のクエリ作成メソッドを実装しています。
select
/ join
/ contain
/ where
/ group
/ order
/ limit
/ offset
/ page
完全な使用例は次のようになります。
$this->Article
->q()
->select(['Article.user_id', 'COUNT(*)'])
->join([
[
'type' => 'INNER',
'table' => 'users',
'alias' => 'User',
'conditions' => 'User.id = Article.user_id',
],
])
->contain('Comment')
->where(['Article.published' => 'Y'])
->group('Article.user_id')
->order(['Article.user_id' => 'ASC'])
->limit(15)
->offset(0)
->page(1)
なお、join()
は現在、上のような古い形式(二次元配列、一次元のキーが数値)のみをサポートしています。CakePHP3 と互換性こそありますが、一次元配列による指定や、文字列キーでのエイリアス指定はまだサポートしていません。
サブクエリ
StackableFinder では、サブクエリを次のように書きます。
$matchingComment = $this->Article->Comment
->q()
->select(['article_id'])
->where(['comment LIKE' => '%CakePHP%'])
->group(['article_id']);
$query = $this->Article
->q()
->where(['id IN ?' => [$matchingComment]]);
メインクエリでの WHERE 句が、CakePHP3 とは少しだけ異なります。
CakePHP3 では ['id' => $matchingComment]
と簡素に記述する場面ですが、CakePHP2 ではコアの実装の都合により同じ形式での記述ができません。
まず、左辺について id IN ?
のような IN
句の明示的な指定と ?
によるサブクエリの埋め込み位置の指定が必要になっています。そして、右辺については [$matchingComment]
のように、StackableFinder を配列 []
で括る必要があります。
カスタムファインダ
CakePHP3 の強力な機能に、ファインダのスタック(積み重ね)があります。
StackableFinder はこの機能をサポートしています。実はこの機能を CakePHP2 でも使いたくてプラグインの開発を計画したのが始まりです。プラグイン名はその証です。
ちなみに、この計画についてはコアチームメンバーの dereuromark 氏から "nice idea" との嬉しい評をいただきました。
$article = $this->Article
->q()
->find('approved')
->find('popular')
->find('latest')
->first();
カスタムファインダの作成方法には一切の変更はありません。すでに作成済みのカスタムファインダがあれば、自由にスタックすることができます。
たとえば、find('latest')
は以下のような実装になっているかもしれません。
// App::uses('CakeTime', 'Utility');
public $findMethods = ['latest' => true];
protected function _findLatest($state, $query, $results = []) {
if ($state === 'before') {
$query['conditions'][$this->alias . '.created >='] = CakeTime::toServer('-1 week');
$query['order'] = [$this->alias . '.created' => 'DESC'];
return $query;
}
foreach ($results as &$result) {
$ref =& $result[$this->alias];
$ref['ago'] = CakeTime::timeAgoInWords($ref['created']);
}
return $results;
}
StackableFinder は、こうしたカスタムファインダを組み合わせることができます。
上記のスタックの例は、承認済みの人気記事の内、最近に投稿された1件を取得するでしょう。
CakePHP2 ではこれが困難で、必要であればこれらを一括して行う _findApprovedPopularLatestFirst()
を別途作成しなければなりませんでした。
StackableFinder は上記の呼び出しを可能にします。
ダイナミックファインダ
findBy*()
および findAllBy*()
によるダイナミックファインダもサポートしています。
なお、CakePHP3 では find('first')
の廃止にともない、両者に違いがなくなりましたが、StackableFinder はこれを弁別します。
$user = $this->User
->q()
->findByUsername('joebob')
->exec();
// または
$user = $this->User
->q()
->findAllByUsername('joebob')
->first();
また CakePHP2.8 でコアが更新され、ダイナミックカスタムファインダが使用できるようになります。
StackableFinder でも CakePHP2.8 を使用すると以下の呼び出しが可能になります。
$query = $this->User
->q()
->findTrollsByUsername('bro');
なお、現時点においては CakePHP3 ではこうしたダイナミックファインダをメソッドチェーンの途中からは呼ぶことができませんが、StackableFinder ではいつでも呼び出すことができます。
$query = $this->Article
->q()
->find('latest')
->findAllByPublished(true);
StackableFinder の機能説明は以上になります。
最後まで記事をお読みいただきありがとうございます。そして、プラグインをお使いになった皆様からのフィードバックをお待ちしております。
インストール手順
プラグインの取得
Composer からインストールすることができます。
{
"require": {
"chinpei215/cakephp-stackable-finder": "^0.2"
}
}
もちろん、GitHub の StackableFinder レポジトリから git clone する、あるいは zip ファイルをダウンロードすることもできます。
プラグインの有効化
app/Config/bootstrap.php
内で StackableFinder プラグインを有効化してください。
CakePlugin::load('StackableFinder');
ビヘイビアの有効化
モデルで StackableFinder.StackableFinder
ビヘイビアを有効化してください。contain()
を使用する場合には、Containable
も有効化する必要があります。
public $actsAs = [
'Containable',
'StackableFinder.StackableFinder',
];
インストールはこれで完了です。
早速 q()
メソッドを呼び出してみてください。古き良き CakePHP2 の世界に新しい道が開きます――そして、それはきっと CakePHP3 の世界へ続く一本道なのです。