カテゴリー : CakePHP

CakePHP 今さらですがClassRegistryクラスのメモ

暫くぶりに CakePHP でアプリケーションを作ろうかといろいろ構想練っている。その中でユーザーに提供したシステムを更新する際、、自動アップデート機能を盛り込みたいなぁ、と思って以前ブックマークしたsdozonoさんの記事「CakePHPのワンクリックアップデート用ソースコード」を見ていたら、「ClassRegistry」なるクラスがあった。

そこで早速検索して、忘れても直ぐに参照できるよういつものようにブログにメモ。

コントローラで複数のモデルを参照、利用する際、プロパティ $uses に参照するモデル名の配列を与える。

$uses = array('Post', 'User')

ところでコントローラのアクションは、プログラムの実行に先立って $uses に登録されたモデル全てを読み込むので、モデルのメソッドを利用しないアクションでは効率が悪くなる。そこで、利用するアクションのみモデルをロードする、ClassRegistry を利用する。

$Post = ClassRegistry::init('Post');
$posts = $Post->find('all');

又は

$posts = ClassRegistry::init('Post')->find('all')

既に読み込まれていれば、ビューで利用できる。

$Category = ClassRegistry::getObject('Category');
$categories = $Post->find('list');

App::import() でも処理できるが、データベースの接続先が $default に固定されてしまい、ユニットテストで利用できないので ClassRegistry の利用をするといいらしい。

参考ページ
cakephperさん:色々なモデルからデータを読み込んでViewにセットする
bennyleeさん:ClassRegistryの備忘録
foldrrさん:CakePHP モデルの読み込みは App::import ではなく ClassRegistry::init で
hiromi2424さん:ClassRegistry徹底解剖

(CakePHP Ver.1.3.9)

追記1

ページに表示するメインナビゲーションやサイドバーに表示するコンポーネントなど、ページの主文と関係ないデータを表示したいときどうしたらいいだろうか?

解決方法の1つとして、Elements から ClassRegistry を利用してモデルのメソッドを呼び出すといいだろう。

MVCの構成から、「View から Model を呼び出すべきでない」、という考えもあるのであまり深くは突っ込まない。

追記2

デバッグ時にデータベースのアクセスログを表示する方法、すっかり忘れていた。テンプレートに

<?php echo $this->element('sql_dump') ?>

を追加する。

CakePHP 1.3 でいくつか便利になった事

ビューに埋め込む JavaScript

JavaScript を利用するページが限られている場合、layout に記述すると必要ないページまでロードするので、それは避けたい。そこでコントローラ/アクションに係るビューファイルに記述するのだが、ライブラリの読み込みは head 部に置きたいし、その他のソースもできれば head 部に記述したい。その方法について、「jQueryのAjaxでSelect入替」でやり方を記述していた。

1.2 ではコントローラで予め JavaScript ヘルパーを指定しておく必要があったが、1.3 になって Javascript ヘルパーは非推奨になった。で、代わりに Html ヘルパーで事足りるようになったようだ。

以下、head 部に jQuery の読み込みとイベントプログラムを挿入するための、ビュー上での記述の例です。ただし、layout ファイルでは「echo $scripts_for_layout」を記述しておく必要がある。

<?php
$html->script(array('jquery-1.4.2.min'), false);
$html->scriptBlock(<<<JS
$(document).ready(function() {
	$("button").click(function() {
		return confirm('転送します');
	});
});
JS
, array('inline'=>false));
?>

CakePHP paginatorのnumber()でspanタグをカレント以外から外す

CakePHPのページリンク機能が大変便利であることは、使用している多くの人が認めるところだと思う。自分も利用させていただいてる。利用するのに少し困るのは、毎回デザインへの合わせ込みをするのだが、たまに苦労することだ。

さて今回のデザインは、ページリンクをCSSの「border」を利用して枠で囲んでいる。paginatorヘルパーを利用して表示すると

0630paginator

のように、ページ番号のリンク表示が2重の枠で表示された。paginatorヘルパーが出力したhtmlを見ると、

<span class="disabled prev_page"><< Prev</span>
<span class="current">1</span>
<span><a href="/users/index/page:2">2</a></span>
<a href="/users/index/page:2">Next >><a>

となっている。CSSでborderをspanタグとaタグの両方に対して指定しており、aタグでリンクされたhtmlがspanタグで囲まれているため、上のように表示されてしまった。

ところで、デフォルトのdivタグからspanタグへの変更は、「paginatorでページリンクを1行にまとめて表示するには」でメモしてあります。また、ページ番号の区切りの「|」は、

$paginator->numbers(array('separator'=>false));

とすれば出力されなくなる。

さて修正だが、リンクの付いたページ番号のところでspanタグが出力されないようにすれば、どうやら解決しそうだ。

paginatorヘルパーの利用で、出力しないようにするパラメータがないかと探ってみたが、どうも情報は見当たらない。そこで今回は、「cake/libs/view/helpers/paginator.php」にパッチを当てることにした。

当該ソースプログラムの541行目

$out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options));

$out .= $this->link($i, array('page' => $i), $options);

とすることで、

0630repaginator

と表示するようになった。なお、同ソースの507行目、517行目、521行目も同様に修正しておけばページ数が増えたとき、幸せになれる(お決まりですが自己責任でお願いします)。

(CakePHP 1.2.3.8166)

追加情報:

上のネタはWebアプリ用のフリーテンプレートを利用させていただきました。
MOONGIFTさん:http://www.moongift.jp/2009/06/web_app_theme/#more-16257
制作者Andreaさん:http://gravityblast.com/

CakePHP Formヘルパーを利用して複数レコードの一括編集と保存

データの編集で、同じモデルの複数レコードを一括で編集表示し保存する、ような処理を行ないたいことがよくある。レコード1件1件をその都度編集、保存する手間を省きたい訳だ。

CakePHPのFormヘルパーは、inputエレメントをlabelエレメントやdivエレメントを付けて出力してくれるので、 fieldsetエレメントやlegendエレメントを利用して編集ブロックとしてまとめるなど、CSSを利用して統一したデザインにすることができる。 デザインについては、sdozonoさんの以下の記事が大変参考になった。

FormHelperとCSS

Formヘルパーでは、createメソッドにモデル名を設定すれば、後はinputメソッドにテーブルのカラム名を指定し、最後にendメソッド を置いて<form>ブロックの出来上がりである。サーバーにデータがsubmitされた際のactionで は、$this->dataにモデル名とカラム名の配列で入力を取得できるため、そのままモデルのvalidateを利用できるし、saveメソッ ドで保存もできる。

であるわけだが、<input>にid属性を与えるので、あくまでもデータレコード1件では使い勝手が非常に良いのだが、さて同じモデルの複数レコードを一覧で編集したいときはどうすればいいのだろうか?

以下のような使い方ができたので、次回参考のため記録する。

ユーザーのIDと名前の一括編集登録ビューの作成、および保存するアクションを例にする。

1.一括表示編集のアクション
先ずはデータの取得部分、保存については以降で検討する。

function multiedit() {
    $data = $this->User->find('all');
    $this->set(compact('data'));
}
2.一括表示編集のビュー
CakePHPで実際に吐き出されたコードを参照すると良い。<input>でname属性を「data[User][0][name]」のように、モデル名の後ろがインクリメントされた番号になるようにするのがキモである。

multiedit.ctp

<?php
    echo $form->create('User', array('action'=>'multiedit'));
    foreach($data['User'] as $key => $user) {
        echo $form->input("{$key}.id", array('type'=>'hidden', 'value'=>$user['id']));
        echo $form->input("{$key}.name", array('value'=>$user['name']));
        echo $form->input("{$key}.shimei", array('value'=>$user['shimei']));
    }
    echo $form->end("保存");
?>
3.アクションでのデータ保存の部分
$this->dataでアクセスできる配列データは、モデルのfindメソッドで複数件数のデータを取得した時と同様な扱い方ができる。アクションに保存する命令を加える。

function multiedit() {
    if (!empty($this->data)) {
        foreach ($this->data['User'] as $data) {
            $this->User->save($data);
        }
        $this->redirect('index');
    }
    $data = $this->User->find('all');
    $this->set(compact('data'));
}

今までは、と言うと、ちょっと強引なやり方をしていたようだ。

(CakePHP 1.2.3.8166)

CakePHP recursive=2で不要な関係データの取り除き方

CakePHPの関係データ取得は、SQL文を自分で作らなくていいので重宝だ。で、「recursive=2」でテーブルの情報を検索した。検索 結果に必要な関係データがあるのは良いとして、不要なデータも当然付いてくるので、これを取り除いた検索結果を取得できないかな?というのを試してみた。

例としてテーブル、usergroups、users、posts、commentsとする。それぞれ

  • usergroups hasMany users
  • users hasMany posts/comments、users belongsTo usergroups
  • posts belongsTo users、posts hasMany comments
  • comments belongsTo posts/users

図で表すとこんな感じだろうか。

0303cakeassoc

ここで、一件のpostsデータを検索する際、ユーザの所属グループデータも一緒に求めたいとして「recursive=2」とした時、取得されるデータはどうなるだろうか?

$this->Post->recursive = 2;
$this->Post->findById($postid);

array(
[Post] => array(
    [id]
    [user_id]
    :
)
[User] => array(
    [id]
    [usergroup_id]
    :
    [Usergroup] => array(
        [id]
        :
    )
)
[Comment] => array(
    [0] => array(
        [id]
        [post_id]
        [user_id]
        :
        [Post] => array(
            [id]
            :
        )
        [User] => array(
            [id]
            :
        )
    :
    [1] => array(
    :
))

のようにデータが取得されると思う(試したのはこれらテーブルと異なるので、違ったらごめんなさい)。

目的とした「Usergroup」データの他に、「Comment」データに「Post」と「User」のデータも一緒に出力される。さて、本題で ある。ここで不要な「Post」と「User」データの取り除き方だ。実は、「unbindModel()」は長いので「recursive」で何とかな らないかと、「$this->Post->Comment->recursive = -1」とか、「$this->Post->Comment->belongsTo[‘User’][‘recursive’] = -1」とか試してみたのだが、うまく行かなかった。やっぱり、「unbindModel()」を使って解決することになった。

$this->Post->recursive = 2;
$this->Post->Comment->unbindModel(array(
    'belongsTo' => array('Post', 'User')));
$this->Post->findById($postid);

「Comment」データでユーザ名を取り込みたいときは、上の「belongsTo」で「User」を除けば良い。「recursive」は大変便利な機能だと改めて思った。