CakePHP2 でモダンに美しく find() する

早いもので CakePHP3 がリリースされてから半年以上が経過しました。

革新的な人は、一足先に CakePHP3 の世界に飛び込んで、時には躓きながらも、すでに多くのノウハウを蓄積していることでしょう。保守的な人は、あるいはいまだに CakePHP1 の世界に留まっているかもしれませんね。
あなたが中庸な人であれば、CakePHP2 の世界に居ながらも、ためらいがちに新しい扉の向こう側を覗いているかもしれません――私のように。

今回、ご紹介する [StackableFinder](https://github.com/chinpei215/cakephp-stackable-finder) は、そんなあなたにぴったりのプラグインで、CakePHP3 の新機能 Query Builder の CakePHP2 における実装です。CakePHP2 のソースコードに目を通していた折りに、同機能をほんの少しのコード量で実装できることに気付いて、早速作成してみました。

この記事では [Cookbook 3.x](http://book.cakephp.org/3.0/en/orm/query-builder.html) の例にならいながら、StackableFinder の使い方をご披露します。CakePHP2 の `find()` がモダンに美しく生まれ変わります。

## 基本的な使い方

プラグインをインストールすると、StackableFinder を以下の方法で呼び出せるようになります。

```php
// ArticlesController.php 内

$query = $this->Article->q();
```

CakePHP3 と同じように、メソッドチェーンでクエリを準備し、`Iterator` により実行することができます。

```php
$query = $this->Article
  ->q()
    ->select(['id', 'name'])
    ->where(['id !=' => 1])
    ->order(['created' => 'DESC']);

foreach ($query as $article) {
    debug($article['Article']['created']);
}
```

または `exec()` を呼び出すことで、`foreach` を用いずにクエリの結果を得ることができます。

```php
$articles = $this->Article
  ->q()
    ->where(['id >' => 1])
    ->exec();
```

ちなみに、これは CakePHP3 であれば `all()` を呼ぶ場面ですが、CakePHP2 には `Collection` クラスが存在せず、互換性のあるオブジェクトを返せないために実装していません。
なお、`Collection` の代わりに配列を返す `toArray()` については実装しており、こちらは `exec()` とほぼ同じ動きをしますが、以下の例において挙動が異なります。

```php
$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 でも実装しています。

```php
$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`

完全な使用例は次のようになります。

```php
$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 では、サブクエリを次のように書きます。

```php
$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](https://github.com/dereuromark/) 氏から "nice idea" との嬉しい評をいただきました。

```php
$article = $this->Article
  ->q()
    ->find('approved')
    ->find('popular')
    ->find('latest')
    ->first();
```

カスタムファインダの作成方法には一切の変更はありません。すでに作成済みのカスタムファインダがあれば、自由にスタックすることができます。
たとえば、`find('latest')` は以下のような実装になっているかもしれません。

```php
// 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 はこれを弁別します。

```php
$user = $this->User
  ->q()
    ->findByUsername('joebob')
    ->exec();

// または

$user = $this->User
  ->q()
    ->findAllByUsername('joebob')
    ->first();
```

また CakePHP2.8 でコアが更新され、ダイナミックカスタムファインダが使用できるようになります。
StackableFinder でも CakePHP2.8 を使用すると以下の呼び出しが可能になります。

```php
$query = $this->User
  ->q()
    ->findTrollsByUsername('bro');
```

なお、現時点においては CakePHP3 ではこうしたダイナミックファインダをメソッドチェーンの途中からは呼ぶことができませんが、StackableFinder ではいつでも呼び出すことができます。

```php
$query = $this->Article
  ->q()
    ->find('latest')
    ->findAllByPublished(true);
```

StackableFinder の機能説明は以上になります。
最後まで記事をお読みいただきありがとうございます。そして、プラグインをお使いになった皆様からのフィードバックをお待ちしております。

## インストール手順

### プラグインの取得
Composer からインストールすることができます。

```php
{
	"require": {
		"chinpei215/cakephp-stackable-finder": "^0.2"
	}
}
```

もちろん、GitHub の [StackableFinder](https://github.com/chinpei215/cakephp-stackable-finder) レポジトリから git clone する、あるいは zip ファイルをダウンロードすることもできます。

### プラグインの有効化
`app/Config/bootstrap.php` 内で StackableFinder プラグインを有効化してください。

```php
CakePlugin::load('StackableFinder');
```

### ビヘイビアの有効化
モデルで `StackableFinder.StackableFinder` ビヘイビアを有効化してください。`contain()` を使用する場合には、`Containable` も有効化する必要があります。

```php
public $actsAs = [
  'Containable',
  'StackableFinder.StackableFinder',
];
```

インストールはこれで完了です。

早速 `q()` メソッドを呼び出してみてください。古き良き CakePHP2 の世界に新しい道が開きます――そして、それはきっと CakePHP3 の世界へ続く一本道なのです。