18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2015-12-26

早いもので 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 の世界へ続く一本道なのです。

18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?