FacebookでCakePHPの日本用グループができました

正確には数ヶ月前からグループは出来てたんだけど、野良っぽい感じだった。
今は、CakePHPコアチームの人も参加してるし、名前もCakePHP Japanというグループ名になり、正式っぽくなった感じ。

https://www.facebook.com/groups/304490963004377/

日本のフォーラムだとcakephp.jpの掲示板があるけど、やはり普段から使ってるFacebookの方が見る頻度も書き込む頻度も違うので、出来ればFacebookグループにも参加してもらえるとうれしいです。

投稿内容は自由で、今は技術的な質問から、アンケート(logを取る方法など)、勉強会情報、などが投稿されてます。

CakePHPコアデベロッパーMarkStoryさんのインタビュー動画

CakePHP1.3からコアデベロッパーになったMarkStoryさんのインタビュー動画が公開されてます。

http://twit.tv/show/floss-weekly/244

動画はダウンロードできます。
また、音声のみでもダウンロードできるので、それをiPhoneに入れて聴いてました。

彼の人柄が良く分かる面白いものになってます。彼は芸術系のカレッジを出てイラストレータ -> Webデザイナー -> プログラマーになった人。そんなバックグラウンドにも触れられています。
英語のみの1時間のものですが、すごく面白かったので是非聴いてみてください。


今年のCakeFestはやはり日本が有力候補っぽいですね。この中でも語ってました。

Cake Beer TalkでCake1から2への移行Tips100を発表しました



まぁ、100個も紹介する時間は無く、31個まで。残りはどこかで。。。。

ちなみに表紙の写真は、自宅から徒歩10分ぐらいのところにある海。
福岡良いよ、福岡!


今回の会は茅場町コワーキングスペースCo-Edoで行いました。
会が始まる前からビールが投入され、質疑応答が活発なよい会になったと思います。
Co-Edo良いよ、Co-Edo!

HABTMの中間テーブルのモデルがAppModelになる問題再び

や、、、奴が帰ってきたぜ!

CakePHP1の頃に一度は解決した問題、また別の場所で勃発しました。。。
「HABTMの中間テーブルがAppModelオブジェクトになってしまう問題の対応」



今回は、HTBTMを持ってるモデルでfind()を実行して(recursive=1)、その後に別の場所で中間テーブルのモデルをClassRegistry::initで取得してメソッドを実行したら、そんなメソッドありませんというエラーがでて、何故かそれがAppModelクラスのインスタンスだったという流れ。
デバッグにはかなり時間がかかりましたが、原因を特定しました。

まずは結論から。

今回もHABTMのwithで指定し忘れてたのが原因だったので、前回と同様に、withキーに中間テーブルのクラス名の文字列を入れて対応完了。

<?php
class Division extends AppModel {
  var $hasAndBelongsToMany = array(
    'User' => array(
      'className' => 'User',
      'joinTable' => 'divisions_users',
      'foreignKey' => 'division_id',
      'associationForeignKey' => 'user_id',
      'unique' => true,
      'conditions' => '',
      'fields' => '',
      'with' => 'DivisionsUser', //ここが重要
      'order' => '',
      'limit' => '',
      'offset' => '',
      'finderQuery' => '',
      'deleteQuery' => '',
      'insertQuery' => ''
    )
  );
}

AppModelが使われる原因

一度モデルクラスのインスタンスを生成してしまうと、ClassRegistry::init()では再度生成せずにキャッシュとして持ってる最初のインスタンスを返します。つまり、最初にAppModelとしてインスタンスが作られてしまうと、ClassRegistry::removeObject()しない限りは目的のインスタンスが取得できなくなります。


CakePHP2でも、モデルのコンストラクタでアソシエーションを構築する際に、HABTMなどの設定情報を走査して、足りない情報を付与します(Model::_generateAssociation())。このときに、withに何もキーがないとDynamicWithにフラグを立てるので、その後自動でモデルが生成されるタイミング(Model::__isset())で、hasManyやbelogsToにそのテーブルの情報が無い場合は、HABTMを見に行って、中間テーブルの情報があればモデルを生成します。このときに、dynamicWithが指定されているとAppModelがnewされてしまいます。dynamicWithがfalseだったり、すでに事前にClassRegistry::init()で該当モデルのインスタンスを生成していればAppModelからインスタンスが生成されません。


なぜModel::__isset()かというと、CakePHP2から導入された遅延ローディングで、モデルのオブジェクトに始めてアクセスするまでモデルのインスタンスは生成されなくなりました。コントローラにいくら $uses = array(A, B, C);って書いても、$this->A->find()ってやる瞬間までAモデルのインスタンスは生成されないということです。



何を言ってるか分からないと思うので、下記のコードにコメントしたので、それを見て汲み取ってください。。。
今回は、中間テーブルのDivisionsUserモデルがどのように生成されるのかの流れを要点だけで解説。コードの不要な箇所はすべて省略してます。
DivisionsUserはhasAndBelongsToManyのjoinTableで定義されるものです。

<?php
//lib/Cake/Model/Model.php

	public function __construct($id = false, $table = null, $ds = null) {
		$this->_createLinks(); //ここでリレーションモデルの生成
	}

	protected function _createLinks() {
					//アソシエーション生成
					$this->_generateAssociation($type, $assoc);
	}

	protected function _generateAssociation($type, $assocKey) {
		foreach ($this->_associationKeys[$type] as $key) {
			//hasManyやhasAndBelongsToManyなどをループで見ていく
			//そのプロパティの定義で、定義が無いものはデフォルトの定義値を入れていく
			if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {

				//'with'に何も値がないと、中間テーブルのテーブル名からモデル名を作成(DivisionsUser)
				// ついでにdynamicWithのフラグをon(てめー!)
				switch ($key) {
					case 'with':
						$data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
						$dynamicWith = true;
					break;
				}
			}

			//dynamicWithのキーをtrueにしてしまう(てめー!)
			if ($dynamicWith) {
				$this->{$type}[$assocKey]['dynamicWith'] = true;
			}

		}
	}


	public function __isset($name) { //$nameはクラス名が今回の流れでは入ります(DivisionsUser)
		$className = false;

		foreach ($this->_associations as $type) {   //$typeにはhasManyやhasAndBelongsToManyなどの文字列が入ります
			if (isset($name, $this->{$type}[$name])) { 
				//ここでhasManyなどにDivisionsUserがあればループ終了して、通常通りモデル生成
				$className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className'];
				break;

			} elseif ($type == 'hasAndBelongsToMany') {
				//hasManyなどに定義がなければ、hasAndBelongsToManyを走査
				foreach ($this->{$type} as $k => $relation) {
					// withキーはコンストラクタで中間テーブルのクラス名(DivisionsUser)が入っている
					if (is_array($relation['with'])) {
						if (key($relation['with']) === $name) {
							$className = $name;
						}

					if ($className) {
						$assocKey = $k;
						//ここでコンストラクタでセットされたdynamicWithが評価される
						$dynamic = !empty($relation['dynamicWith']); 
						break(2);
					}
				}
			}
		}

		if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) {
			//まだ該当モデル(DivisionsUser)が存在せず、dynamicキーがtrueなら、AppModelをnewする(てめー!)
			$this->{$className} = new AppModel(array(
				'name' => $className,
				'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'],
				'ds' => $this->useDbConfig
			));
		} else {
			//dynamicがfalseなら、ClassRegistryを使ってモデルを生成する(AppModelにはならない)
			$this->_constructLinkedModel($name, $className, $plugin);
		}

	}

ちなみに、このdynamicWithの何がメリットかと考えると、中間テーブルのモデルファイルを作っておかなくてもモデルのインスタンスがAppModelで生成されて、基本的なモデルのメソッドが使えるということ。
それぐらいかな。。。。

[CakePHP2] SQL文を貼り付けると、CakePHPのコードを生成するサービス


infinitas-cms.org というCakePHPCMSを開発している @dogmatic69 さんが、すごいものを開発してました。
なんと、SQL文をペーストすると、CakePHPのコードを生成するものです。

http://dogmatic69.com/sql-to-cakephp-find-converter


コードは3パターン出力されます。オプションを選ぶと、ClassRegistry::init経由での呼び出しパターンとか切り替え可能です。

  • findのオプションで指定する方法 find('all', $params)みたいなパターン
  • Modelのコードを出力。カスタムfindメソッドを生成するパターン
  • Paginationコンポーネントで指定するパターン

バーチャルフィールドなども考慮して出力するという素晴らしい仕様!


たとえば、下記のようなSQLを貼り付けると

select id, SUM(your_field) from foos where id = 1 and bar = 'fuga' order by buz limit 1,1;

モデルファイルのコードを生成して表示してくれます。

<?php
class Foo extends AppModel {
/**
 * Virtual fields
 * 
 * @var array
 */
	public $virtualFields = array(
		'sum_your_field' => 'SUM(your_field)',
	);

/**
 * Custom find methods
 *
 * @var array
 */
	public $findMethods = array(
		'special' => true
	);

/**
 * Special find
 *
 * @param string $state The state of the find (before or after)
 * @param array $query the query conditions
 * @param array $results the results
 *
 * @return array
 */
	protected function _findSpecial($state, $query, $results = array()) {
		if ($state == 'before') {
			$query['fields'] = array_merge((array)$query['fields'], array(
				'Foo.id',
				'Foo.sum_your_field',
			));
			$query['conditions'] = array_merge((array)$query['conditions'], array(
				'id' => '1',
				'bar' => 'fuga',
			));
			$query['order'] = array_merge((array)$query['order'], array(
				'buz' => 'asc',
			));
			$query['limit'] = 1;
			$query['offset'] = '1';

			return $query;
		}
		
		return current($results);
	}
}

まだ不完全なところもあるかもしれませんが、それなりに使える便利なサービスになりそうです。


# 本末転倒のような気がしないでもないですが。。。。

CakePHP2.3からinputタグにhtml5のrequired属性がつくようになった

CakePHP2.3から、モデルのバリデーション定義で必須項目にしているフィールドには、Viewのinputタグにrequrired属性が追加されるようになりました。
つまり、下記のようなinputタグが出力されるということです(一番最後のrequired=の箇所)

<input name="data[Contact][name]" maxlength="50" type="text" id="ContactName" required="required"/>


これがあると、最近のブラウザではsubmit前に下記のような表示が出てPOSTできないようになります(firefoxの例)
これがあれば、Javascriptを使ったPost前のバリデーションチェックが少し楽になると思います。


ソースコードはまだ読んでませんが、どうも動きを見ると、allowEmptyがfalseの場合にこの動作になるみたい(デフォルトはfalse)。allowEmpty=trueにしないとこの属性は消えません。requiredオプションを見るのかと思ったけど違った。ちょっとややこしいなこれは。


[追記]
これを回避する方法はいくつかあって、下記のCookbookにまとまっています
http://book.cakephp.org/2.0/en/core-libraries/helpers/form.html#FormHelper::input
ひとつの方法として、Formタグにnovalidateをセットするやりかた。

<?php
echo $this->Form->create('Model', array('novalidate' => true)); 
?>