cakephperの日記(CakePHP, Laravel, PHP)


継続的WebセキュリティテストサービスVAddyを始めました!

2013-08-23

CakePHPのfind()で取得したデータが全てstring型になるのを、DBのカラムの型に合わせてint型で値を取得する方法(mysql)


CakePHP2からはPDOを使ってDBアクセスするようになりました。PDO(mysql)では、デフォルト設定でデータをfetchするとint型のカラムでもstring型として結果が返ってきます。CakePHPもこの影響を受けており、jsonデータなどに変換する際や、型を厳密に扱いたい場合に影響がます。(CakePHP1では、PDOを使っていませんがintカラムはstringで返ってきます)


この問題を解決するには、PHP5.3以上の環境でPDOのPDO::ATTR_EMULATE_PREPARESをオフにすれば良いです。PDOがmysqlndドライバを利用することが前提なのですが、PHP5.4からはデフォルトでmysqlndドライバが利用されるので大丈夫です。今回はPHP5.4の環境で検証しました。

PHP5.3ではPDOがmysqlndドライバを利用するためにコンパイルオプションを指定するか、パッケージインストールphp-mysqlndを入れるなどが必要です。

参考 http://stackoverflow.com/questions/13159518/how-to-enable-mysqlnd-for-php


mysqlndについてや、PDOとの関係などは下記の資料がまとまっています。

http://blog.layer8.sh/ja/2013/03/12/php-mysql-pdo-mysqlnd/


さて、CakePHP2でPDOのフラグオプションにPDO::ATTR_EMULATE_PREPARES = falseを指定すれば良いのですが、mysqlデータソースを見るとアプリケーション側から指定する仕組みがないため、コントローラの中などでPDOインスタンスを取得してセットすることにしましょう。

例えば、jsonを返すコントローラのアクションの中で下記のようにすればfindの結果は型が正しい状態で返ってきます。

<?php
$pdo = $this->Post->getDatasource()->getConnection();
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,FALSE);
var_dump($this->Post->find());

やっていることは、Postモデルを使って(モデルは何でも良い)、getConnection()でPDOインスタンスを取得します。

あとはPDOクラスのsetAttribute()メソッドでPDO::ATTR_EMULATE_PREPARESにfalseを指定しています。


もしアプリケーション全体でこのオプションを有効にしたい場合は、AppModelやAppControllerで上記の指定をしておくか、CakePHPMySQLデータソースを継承して、connect()メソッドオーバライドして制御するなどすれば対応できるでしょう。


追記

akiyan先生からのご指摘で、タイトルが紛らわしいと気付いたので変更しました。ありがとうございました。

2013-02-25

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



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

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

福岡良いよ、福岡


今回の会は茅場町コワーキングスペースCo-Edoで行いました。

会が始まる前からビールが投入され、質疑応答が活発なよい会になったと思います。

Co-Edo良いよ、Co-Edo!

2013-02-21

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で生成されて、基本的なモデルのメソッドが使えるということ。

それぐらいかな。。。。

2013-02-11

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前のバリデーションチェックが少し楽になると思います。

f:id:cakephper:20130211223324p:image


ソースコードはまだ読んでませんが、どうも動きを見ると、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)); 
?>

2013-02-08

CakePHP2実践入門が増刷されました!

CakePHPをバリバリ使ってるエンジニアで書き上げた「CakePHP2実践入門」、おかげさまで増刷(第2刷)となりました!

CakePHPをある程度使ってる方にも有用で、例えばソーシャル連携とか、セキュリティとか、テストとか、CakeEmailとか、私もCakePHP2の開発をする上で手放せない本になりました。やはり現場で活躍している人が書いてると役立ちますね。

自分が書いたチートシートも自分の役に立ってて、がんばって書き上げてよかったなと思ってます。


f:id:cakephper:20121010121615j:image:w360


http://www.amazon.co.jp/dp/4774153249/