Hatena::ブログ(Diary)

三等兵

2009-10-04

CakePHPでのフォーム処理とデータの保存

簡単すぎて素通りする部分をピックアップしなければならないほど悲しきシリーズ。フォームで入力したデータをデータベースに保存するという。

そう、あるんです。もうサンプルとしてあるんですよ。なんで別にどうでもいいんだわさ。

ここを参考にすれば簡単。

http://book.cakephp.org/ja/view/219/Blog


フォームはマニュアルみればほぼわかるとはおもいますが。

http://book.cakephp.org/ja/view/182/Form

とりあえずデータを保存するまで。


create(string $model = null, array $options = array())

第一引数がモデル名で、第二引数がオプションで属性とかも。

<?php
echo $form->create('Recipe'); ?>

# <form id="RecipeAddForm" method="post" action="/recipes/add">

ってなるみたい。formの終了タグは、

<?php
echo $form->end();

#</form>

だそうで。これら使わなくても普通にかけばいいじゃんと思うんだけど、cakeらしくつくったほうがいいかな。

あとそれぞれタイプの使い方は、

http://book.cakephp.org/ja/view/204/Form-Element-Specific-Methods

で確認。



今回使うフォームは、

<?php
echo $form->create('Test', array('action' => 'index'));
echo $form->textarea('text', array('cols' => '60', 'rows' => '3'));
echo $form->end('投稿');

こちら。

気づいたんですが第二引数は主にhtmlタグだと属性指定をするようですね。たとえばlinkタグだとtargetとかもきっとarray('target' => 'blank')とかになるんでしょうね。面倒だから未確認だけど。

それで、

<?php
echo $form->create('Test', array('action' => 'index'));

のactionですが、デフォルトだとaddになっちゃうので、indexに指定してます。


<?php
echo $form->textarea('text', array('cols' => '60', 'rows' => '3'));

ここ重要ですね。タイプはtextareaになってますけど、たぶんtextとかでも同じだと思いますが、第一引数の部分はデータベースのフィールドの名前と一緒じゃないと保存されません。

この場合textってフィールドないとデータベースには保存されません。


当たり前だって?いやいやいや。


ここに当たり前がわからなかった御仁がいるんだよ!うさぎ御仁だよ!

うさぎ御仁「ハロー


はい。ちなみにデータベースは、

CREATE TABLE tests (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    text TEXT
);

こんな感じ。コントローラには、

http://book.cakephp.org/ja/view/335/Create-a-Posts-Controller

にあるようにsaveメソッド使っちゃいます。コピペ。

こんだけで保存できちゃうんですねー。楽。ちなみにバリデーションは今回何もやってません。マニュアルと本見た限りでは簡単そうだなーという印象だけど。


今回試したソース

view

<?php
echo $form->create('Test', array('action' => 'index'));
echo $form->textarea('text', array('cols' => '60', 'rows' => '3'));
echo $form->end('投稿');

controller

<?php
class TestsController extends AppController {
  public $uses = array('Test');

  function index(){
    if (!empty($this->data)) {
      if ($this->Test->save($this->data)) {
        $this->redirect('/tests');
      }
    }
  }
}


model

<?php
class Test extends AppModel {
}

sql

CREATE TABLE tests (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    text TEXT
);

データベース名はなんでも。初心者ってのはこういうこともわからないもんだから、涙を流さずに前を向いて、景色をにじませる涙なんて蒸発だ!出てきた瞬間じゅわわわ〜だ!できるよ!


こうして、うさぎ御仁は今日も行く。もふもふ終り。

2009-09-16

cakePHP書籍初心者おすすめ本

amazonよりご到着。珍しくJPXだった。またCDも頼んでいたけれど、予約商品だったので別々に配送されました。結局今日で一緒だったのに。ちなみに佐川メール便でした。

というわけで書籍。たぶんこの選択がベストだと思われ。

CakePHP ポケットリファレンス (Pocket Reference) (単行本(ソフトカバー))
4774135038

これがあればなんとかなりそうな気がしました。cakeやるんだったらこれ一冊もってると便利つーか、初心者には必須で良い。少し古いので変更部分を把握さえすれば大丈夫そう。あと謎なオプションも説明してくれているのでやっふぉーいです。


CakePHP1.2ガイドブック (単行本(ソフトカバー))
4839932468

上の本プラスこれあったらこれでよいと思った。ぱらぱらっとみただけですが丁寧です。セキュリティのことも書かれてありました。まだみてないのですが大丈夫でしょう。日本のcakePHPの中心的存在な方々がかいてらっしゃるので。少なくとも良いかどうかなんてみてもわからんもの私には。


CakePHPによる実践Webアプリケーション開発 (単行本(ソフトカバー))
4839930651

これは前に購入したものですが初心者が最初に買うもんじゃない。その応用力というか、機能の使いどころとか学ぶのにいいんじゃないかな。ただScaffoldは最後まで使っていますよね。カスタマイズしただけのような気がする。ちょっとそこはよろしくないなあと思うんだけど、どうなんでしょう?いいのかな。あとphpがある程度できないとイライラしてたぶん意味ない。


とりあえず国内で流通している本で初心者によさげなのは、CakePHP ポケットリファレンスCakePHP1.2ガイドブックが今のところ一番良い選択だと思われます。たぶん。初心者の私がいうのだからきっと大丈夫さ。

2009-09-11

model::find()メソッドの先にはお花畑が広がっていました

あっはっは〜つかまえてごらんボルジョワフゥ〜


ソースコードを読みます

難しい。非常にむつかしい。これはわからない。findっていうのはタイプが多くて、その上オプションもありよく使い方がみえない。というわけで読んで見たけど、やっぱりわからない。

マニュアル

http://book.cakephp.org/ja/view/73/Retrieving-Your-Data

ソースコード

http://api.cakephp.org/view_source/model/#line-1935

<?php
function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
if (!is_string($conditions) || (is_string($conditions) && !array_key_exists($conditions, $this->_findMethods))) {
$type = 'first';
$query = array_merge(compact('conditions', 'fields', 'order', 'recursive'), array('limit' => 1));
} else {
list($type, $query) = array($conditions, $fields);
}

最初から気になった部分。メソッドが、

function find($conditions = null, $fields = array(), $order = null, $recursive = null)

ってなってるけど、マニュアルでは、

function find($type, $params)

となっている。少し悩んだけど$fieldsの引数が、廃止となったfindAllとかの書き方を対応してのことかなと思われ。


実際の使い方はソースコードにサンプルあったけど、

Eg: find('all', array(
    'conditions' => array('name' => 'Thomas Anderson'),
    'fields' => array('name', 'email'),
    'order' => 'field3 DESC',
    'recursive' => 2,
    'group' => 'type'));

こんな感じでやるのが今のfindの正しい使い方。findメソッドの引数が4つあるのは気にせず、

find($type, $params)

の方で利用したほうがいいでしょう。$typeは、all / first / count / neighbors / list / threadedから。$paramsは、マニュアルで確認。


引数部分が長くなったけど次のif。||はor、&&はand、!はnotの意味と一緒。優先順位は忘れたけどたぶん記号が上。

で、$conditionsが文字列でないか、または文字列であるか、かつ、$conditionsが_findMethods配列のうちのキーとしてなければture、というもの。言葉にしにくいというか、ちょっと複雑な言い方になっちゃいましたね。

_findMethodsはこれ。

<?php
var $_findMethods = array(
'all' => true, 'first' => true, 'count' => true,
'neighbors' => true, 'list' => true, 'threaded' => true
);

簡単にいうと、$conditonsが(all / first / count / neighbors / list / threaded)のうちのどれにもあてはまらないならtureで、あてはまったらfalseになるってこと。!でnotを使ってる部分に注意。いつも思うけど見づらいよねビックリマーク。

array_key_existsはその文字列が、配列のキーとして存在するかどうか調べるといったもの。


tureは第1引数にallやlistが無いときの処理。find($type, $params)でのfind利用をする場合tureとなりませんので無視すればおっけ。

でも一応みときますと、tureの場合$typeはfirstになります。マニュアルでもデフォがfirstとかいてますしね。$queryにはそれぞれの引数を、compactで配列にして、さらにarray_mergeで配列と配列をマージしてます。配列が合体する、というイメージ。

falseの場合。 

list関数使ってます。これよくわからないんだよなあ。結局$conditionsが$typeになり、$fieldsが$queryになります。

次。

<?php
$db =& ConnectionManager::getDataSource($this->useDbConfig);
$this->findQueryType = $type;
$this->id = $this->getID();

ConnectionManager::getDataSource()メソッドなんですが、このへんはものすごくややこしかった。よくでてきますねこれ。データベースのデータを取得しているんだと思いますが。


みてみたけど複雑だたーよ。mysqlやpostgresを選別している部分もあったけどよくわからなかった。で、findQueryTypeには、$typeが代入されてますね。これはallとかlistとかあれ。

それからgetIDですが。コードみたところ現在のidを取得しているってことかな。配列であるならキーをのぞいている。

idってのはたぶんプライマリキー。これはユニークなキーですよね。ちょっとうろ覚えなんですが。ユニークなキーであるからして、それぞれの行をひとまとまりとして判断できる、という感じでしょうか。

次。

<?php
$query = array_merge(
array(
'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null,
'offset' => null, 'order' => null, 'page' => null, 'group' => null, 'callbacks' => true
),
(array)$query
);

これは見たまんんまですね。array_maerge関数は配列の合体というか、上書きというイメージでもいいでしょう。$queryはもともとconditionsやfileldsなどをキーとした値が入っていて、それをマージでnullの配列に上書きという感じ。型キャストは念のためかな?

<?php
$query = array('conditions' => 'hoge', 'fields' => 'fuga');

$query = array_merge(
array(
'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null,
'offset' => null, 'order' => null, 'page' => null, 'group' => null, 'callbacks' => true
),
(array)$query
);

//dump
var_dump($query);

で挙動がよくわかります。

次。

<?php
if ($type != 'all') {
if ($this->_findMethods[$type] === true) {
$query = $this->{'_find' . ucfirst($type)}('before', $query);
}
}

条件式は、2つもがっつくと、もし$typeがallでなく、そのうえもしfindMethodsのキー$typeがtureなら、という具合。だからall以外のfirst / count / neighbors / list / threadedであったら、ということでしょうね。

tureの場合、$queryにはそれぞれのメソッドの結果が入るというわけなんですが、さすがにそれを読むのはめんどくさいというわけでスルー。


$query = $this->{'_find' . ucfirst($type)}('before', $query);

ここでタイプ別のメソッドを実行することになります。たとえば$typeがlistだったとすれば、

$query = $this->_findList('before', $query);

ってなるようにできてますね。それからfindメソッドより下のソースをみてみてください。引数のbeforeってのが結構重要で、afterも後から出てきます。

それぞれのメソッドでbeforeとafterが設定されていますが。ようは前処理と後処理、ってことだと思います。でもやってることがよくわかんないのだわ。


たぶんそれぞれのタイプ別に$queryをいじったり、場合によってはそっちでデータを取得したりしている、とみえなくもないんだけど。何のためなのかわからない。

ここから先は$queryの前処理された場合になりますので複雑ですね。タイプがallの場合はそのまんまです。とりあえず次。

<?php
if (!is_numeric($query['page']) || intval($query['page']) < 1) {
$query['page'] = 1;
}
if ($query['page'] > 1 && !empty($query['limit'])) {
$query['offset'] = ($query['page'] - 1) * $query['limit'];
}
if ($query['order'] === null && $this->order !== null) {
$query['order'] = $this->order;
}
$query['order'] = array($query['order']);

$query['page']が数字じゃないか、または1以下なら、$query['page']に1を代入。

$query['page']が1より大きく、かつ$query['limit']が空でなければ。詳細めんどいのでtrue実行。

$query['order']がnullで、かつorderがnullじゃなければtrue。orderはクラス変数。

そして最後は二次元配列。

結局やっていることはわかっても、何のために何をやってるのかがわかりませぬ。そのためにはもっといろいろのぞかなければいけないけど、気力がなくなってきた。

ので次。

<?php
if ($query['callbacks'] === true || $query['callbacks'] === 'before') {
  $return = $this->Behaviors->trigger($this, 'beforeFind', array($query), array(
  'break' => true, 'breakOn' => false, 'modParams' => true
  ));
  $query = (is_array($return)) ? $return : $query;

  if ($return === false) {
    return null;
  }

  $return = $this->beforeFind($query);
  $query = (is_array($return)) ? $return : $query;

  if ($return === false) {
    return null;
  }
}

わかりにくいと思ったのでインデントつけてます。

最初の条件式は$query['callbacks']がtrue、またはbeforeならtrue。このコールバックの値は、true、false、before、afterが入るのみのようです。おそらくtrueならbeforeとafterのコールバック実行するんじゃないかな。falseは何もしなくて、あとはbeforeのみかafterのみかということに。

trueの場合はこのメソッドで一番長いコード内容になってます。

まずBehaviors::trigger()メソッドつかってますね。結果を$returnに代入しています。でも何をやっているのかはわからないです。

その後の三項演算子部分。$returnが配列だったら、$returnを$queryに代入。配列でなければ、$queryをそのまま使う、という感じ。$returnというのは、先ほどのBehaviors::trigger()メソッドの結果を代入した変数。

trigger()メソッドの説明は、

Dispatches a behavior callback on all attached behavior objects

となってます。「すべてのビヘイビアオブジェクトに結びついたビエイビアコールバックを送り出すよー」的な訳でいいでしょうか。

しかしまー、内容がイメージできないわ。ごめんね保留。


で、次のifは$returnがfalseならnull返して、終り。find失敗ってことかな。

そしてbeforeFindメソッドを使って$returnに代入している部分。

それでbeforeFindなんですが、たぶんappのモデル部分でユーザが定義したメソッドを使っていることになりますね。model.phpには、

<?php
function beforeFind($queryData) {
return true;

しかないので、基本はappのモデル部分でオーバーライドされたbeforeFindメソッドの結果を返している、というのことでしょうか。何もモデルに定義してなかったら単にtrueを返すだけですが。

その後また$returnが配列かどうかチェックしてますね。そしてfalseだったらまたnullを返すと。


この辺はちょっとわかんないです。trigger()メソッドが何やってるのかわかればよかったんですが、ちょっときついなー。長くなるし。

というわけで次といきたいところだが。

<?php
$results = $db->read($this, $query);
$this->resetAssociations();
$this->findQueryType = null;

if ($query['callbacks'] === true || $query['callbacks'] === 'after') {
$results = $this->__filterResults($results);
}

if ($type === 'all') {
return $results;
} else {
if ($this->_findMethods[$type] === true) {
return $this->{'_find' . ucfirst($type)}('after', $query, $results);
}
}
}

挫折。ここは内容は理解できるけど、やってることが理解できない。そもそも途中からわかっていないよね。とりあえず時間おこう。


わからなかった部分

find($type, $params)の$paramsに使うそれぞれのキーの意味がわからない

http://book.cakephp.org/ja/view/73/Retrieving-Your-Data

にサンプルがあるとはいえ、よくわからない。これは大きい。pageは?limitは?offset?ここがよくわからないから何やってるかあやふやだった。主に使うキーって何なんだろう?

今思うとapiもしっかりみとけばなとは思うけどわからんなやっぱ。

http://api.cakephp.org/class/model#method-Modelfind


どこでデータを検索しているのかわからない

その$paramsのキーをもとにあれこれしてると思うんだけど、そのあれこれがわからない。firstにしてもcountにしてもlistにしても、それがいまいちつかめなかった。データベースのデータ取得は、

$db =& ConnectionManager::getDataSource($this->useDbConfig);

ここだと思うんですよね。それともこれって接続しただけ?mysql_connect()しただけでしょうか。みた限りではmysql_select_dbまでいってるんじゃないかって思ったけど。もしかして間違えたか。

それでとにかく検索です。私はmysql_query()しかしらないんですが、そういう系のあったかな。もしかしたら、

$results = $db->read($this, $query);

ここじゃないかなとも思ったけど、model::read()みても何もないんですよね。場所が違うンでしょうか?

それと$db->read()って何でしょう?$db->query()とかも使えるみたいだけど、なんで->でアクセスできるのですか。$db->nameなんてのもあった。


どこに影響しているのかわからないコード
$this->findQueryType = $type;
$this->id = $this->getID();

他にもあったのですが。無駄な部分はないと思うので他の部分で影響しているのだと思うのですが、どこか検討がつかない。



このようにわからないことが多く、これらの影響が大きかったと思います。


終り

難しく本当に見ただけだった。たぶん間違いだらけ。なんとかfindを使えるようになりたくてみてみたけど、最低query使ってる部分がわからないというのは痛い。目の前にあるのに、肝心の部分が見えない。

それからキーのconditionsやpageの影響もわからないし。んー、何かのcakeサンプルでデータ入れまくってfindしてみるのが良いのかな。とにもかくにも疲れた。

とりあえず変なところあったら教えてください。

2009-09-04

CakePHPで2億4千万のvar_dump

その、お勉強用ということです。クリーンな状態でやりたいですよね。まっさらな感じで。そう、津軽海峡冬景色、みたいな…。


意味は無い、みたいな。


dumpしたいがためのCakePHP

それだけ。いろいろ確認したいときのために。


app\controllers\tests_controller.php作成
<?php
class TestsController extends AppController{
  public $uses = array('Test');

  function index(){
  }
}

アクセスは、ttp://sample.com/cake/tests/

でおっけ。cakeの部分は名前が違うなら変えて。


app\models\test.php作成
<?php
class Test extends AppModel{
  public $useTable = false;
}

データベース使わなくても良いように。使うときは外せばおけ。


app\views\tests\index.ctp

viewフォルダにtestsフォルダを作り、index.ctpも作成。

index.ctpにはdump用の関数を作っておくと便利。

<?php
//
d($var);

function d($dump){
  echo "<pre>";
  var_dump($dump);
  echo "</pre><br />";
}

$varにコントローラから受取る変数名を入れれば、preタグで整えられた結果がでます。


トップディレクトリに.htaccessが無い場合

最新のcakePHPだともしかしたらないかもしれない。それだとルーティングあれこれしなきゃいけなくてよくわからないので、.htaccessを設置した方が楽。

<IfModule mod_rewrite.c>
   RewriteEngine on
   RewriteRule    ^$ app/webroot/    [L]
   RewriteRule    (.*) app/webroot/$1 [L]
</IfModule>
core.php編集

たぶんそのままだとsaltがどうのこうのというエラーが出てくるかもしれないので、

Configure::write('Security.salt', 'ewrsthyjkjhtgredgrhtjjhygtrfd');

の部分で第2引数の部分を適当に変える。


津軽海峡冬景色は永遠に

2億4千万回var_dumpして一人前。

2009-09-02

controllerのset()メソッド

どうやって渡しているのかきになる。フォームでの受取りしかしらないバカなもんでね。ちょっとお勉強したいのさ。

ソース

http://api.cakephp.org/view_source/controller/#line-656


うっとうしい分析

set(string $var, mixed $value)

第1引数が変数名で、第2引数が渡す値。文字列でも配列でも良い。

$oneが$varで、$valueが$twoにあたる。

controller.php
<?php
function set($one, $two = null) {
  $data = array();
  if (is_array($one)) {
    if (is_array($two)) {
      $data = array_combine($one, $two);
    } else {
      $data = $one;
    }
  } else {
    $data = array($one => $two);
  }

$data初期化で配列。もし、$oneが配列ならtrue以下実行。そうでないなら$oneの文字列をキーとして値を$twoにし、$dataに代入。後者の場合、$one

$oneが配列で$twoも配列なら、array_conbine()関数でコンバインしちゃう。この関数ってどちらとも要素数一緒じゃなきゃいけない。それで$oneが配列だけど$twoが配列では無い場合、$dataには$oneが入る。

パターン1:$oneと$twoが配列ならユーコンバインしちゃいなよ、で$dataの配列

パターン2:$oneは配列だけど$twoは配列じゃない、で$dataの配列

パターン3:$oneが配列ではないのなら、で$oneがキーで$twoが値となった$dataの配列

がここでできあがるわけですねー。

次。

controller.php
<?php
  foreach ($data as $name => $value) {
    if ($name === 'title') {
      $this->pageTitle = $value;
    } else {
      if ($two === null && is_array($one)) {
        $this->viewVars[Inflector::variable($name)] = $value;
      } else {
        $this->viewVars[$name] = $value;
      }
    }
  }
}

foreach。$dataで$nameをキー、$valueを値。

if文は、もし$nameがtitleという文字列なら、$this->pageTitleに代入。

pageTitleの説明はこれ。

http://book.cakephp.org/ja/view/54/Page-related-Attributes-layout-and-pageTitle

ページタイトルはちゃんとpageTitleに入れてくれるのね。

それで、もしtitleというキーがでなくて、$twoがnullで$oneが配列ならば。つまり、set()メソッド使ったときに引数が$oneの配列だけだった場合は。$viewVarsという配列にInflectorクラスのvariableメソッドを使い、引数に$nameでその返り値をキーとし、代入は$value。

そうでなければ、外部のメソッドを参照せずに$viewVarsに$nameをキーとして$valueを代入。


えーまたまたInflectorクラスのvaroableメソッドを緊急にみなければいけなくなりました。

<?php
function variable($string) {
  $string = Inflector::camelize(Inflector::underscore($string));
  $replace = strtolower(substr($string, 0, 1));
  return preg_replace('/\\w/', $replace, $string, 1);
}

まーた参照。関係ないけどこれって::じゃないとだめなのでしょうか。->ではダメ?

Inflector.php
<?php
function camelize($lowerCaseAndUnderscoredWord) {
  return str_replace(" ", "", ucwords(str_replace("_", " ", $lowerCaseAndUnderscoredWord)));
}

//

function underscore($camelCasedWord) {
  return strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelCasedWord));
}

camelizeメソッドは、ucwords関数は文字列の最初の小文字を大文字に。んで、$lower〜のうちにアンダースコアがあるなら半角スペースして、さらに外側のstr_replaceで半角スペースを除去。

underscoreメソッドは、preg_replace関数で$camel〜のうちに/(?<=\\w)([A-Z])/'にマッチする部分を、'_\\1'に置換。アンダースコアをつけるってことかな?で、strtolowerで全部小文字にしてます。


というわけで、variableメソッドは、まず$stringをunderscoreメソッドでマッチすればアンダースコアつけて、camelizeメソッドでアンダースコアがあったらそれを半角スペースに直し、単語のトップを大文字にしてさらに半角スペースくっつけて自己代入。

で、$replaceはsubstr関数を使い$stringの最初を取得、そしてそれをstrtolower関数で小文字に変換して$replaceに代入。

returnの部分は$stringで大文字になっている部分を$replaceに変換。引数1なんで最初です。この辺はよくわからないと思うので、

http://book.cakephp.org/ja/view/572/Class-methods

をみるといいですね。結局キーの名前をクラスメソッドっぽくしている。ただ、どういう場合にここに流れ着くのか、っていうのがわからないなあ。


まー簡略化すると、

パターン1:$nameが'title'なら、$this->pageTitleに$valueの値を代入

パターン2:$twoがnullで、$oneが配列なら$viewVarsにキーの名前をクラスメソッド?っぽくして、$valueの値を代入

パターン3:パターン2に当てはまらない場合は、$viewVarsに$nameをキーに、$valueの値を代入

で、これらを1つ1つ検査して繰り返していくということですねー。


最終的にどうやって渡しているかというと、$pageTitleと$viewVarsに値が代入されているわけで。それで、

$data = "データだよーん";
set('var', $data);

ってやると、ビューでは$varを使うことでデータだよーん、て表示できるわけですが、なんでこうなるかわからん。


あー、もしかしてどこかで、

extract($viewVars);

みたいなことやってんのかな。おそらく。これ以上はわからん。

結局ただの変数でわたしてるってことでしょう。そりゃそうだよね。まーでもやっぱり値の受け渡しってのは配列が最適みたい。