単なる結合だけから始めればいいのに、なんでソートとか書いて騙すようなわかりにくいことするんだろうか?
内容としては、最初から記事を取得するのではなく、まずカテゴリリストを取得し、その後各カテゴリ毎に記事を取得していって表示する、という内容になります。
まずコントローラを、記事リストの取得ではなく、カテゴリリストの取得を行うように変更します。
apps/frontend/modules/job/actions/actions.class.php
1
2
3
|
public function executeIndex(sfWebRequest $request){
$this->categories = Doctrine::getTable('JobeetCategory')->getWithJobs();
}
|
JobeetCategoryTableモデルクラスに、有効なジョブを含むカテゴリリストの取得を行うメソッドを追加します。
lib/model/doctrine/JobeetCategoryTable.class.php
1
2
3
4
5
|
public function getWithJobs(){
$q = $this->createQuery('c')
->leftJoin('c.JobeetJobs j')->where('j.expires_at > NOW() ');
return $q->execute();
}
|
JobeetCategoryテーブルに対し、JobeetJobテーブルをLEFT JOINするという内容です。
スキーマでリレーションを設定してあるテーブルの場合、このようにjoin系のメソッドで簡単に結合を行えます。
最後の条件はチュートリアルではdate()とか使ってますがSQL任せでいいじゃん。
この時点で、テンプレートから$categoriesでカテゴリリストを取得することができます。
簡単にテンプレートを書いて確認してみます。
apps/frontend/modules/job/templates/indexSuccess.php
1
2
3
4
|
foreach($categories as $category){
print($category->name."<br />");
}
|
Design
Programming
2行が表示されました。
この2カテゴリのみが、現在有効な求人を持っているからです。
実行されたSQLは以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
SELECT
j.id AS j__id, j.name AS j__name, j.created_at AS j__created_at
, j.updated_at AS j__updated_at, j2.id AS j2__id
, j2.category_id AS j2__category_id, j2.type AS j2__type
, j2.company AS j2__company, j2.logo AS j2__logo
, j2.url AS j2__url, j2.position AS j2__position
, j2.location AS j2__location, j2.description AS j2__description
, j2.how_to_apply AS j2__how_to_apply, j2.token AS j2__token
, j2.is_public AS j2__is_public, j2.is_activated AS j2__is_activated
, j2.email AS j2__email, j2.expires_at AS j2__expires_at
, j2.created_at AS j2__created_at, j2.updated_at AS j2__updated_at
FROM jobeet_category j
LEFT JOIN jobeet_job j2 ON j.id = j2.category_id
WHERE j2.expires_at > NOW()
|
さて、ここからがSymfonyの便利な点ですが、SQLの時点で呼ばれてない内容にも、リレーションが設定されている限り進んでいくことができます。
テンプレートをちょっと変更してみます。
apps/frontend/modules/job/templates/indexSuccess.php
1
2
3
4
5
6
7
|
foreach($categories as $category){
$jobeetjobs=$category->getJobeetJobs();
foreach($jobeetjobs as $jobeetjob){
print($jobeetjob->token.'<br />');
}
}
|
取得されたjobeet_categoryテーブルから、そこに含まれるjobeet_jobテーブルの内容を引っ張ってきて全部表示する、という内容です。
各モデルには、get+相手のモデル名で、リレーションのあるテーブルの中身を全部取得するというメソッドが予め定義されています。
JobeetCategoryモデルには他の3テーブルから直接リレーションが張ってあるのですが、スキーマの書き方によって呼び出し方が少しだけ違います。
relationsが単数形のJobeetCategoryで定義されているjobeet_jobsやjobeet_category_affiliateはgetJobeetJobs()、getJobeetCategoryAffiliate()で呼ぶことができ、複数形のJobeetCategoriesで定義されているjobeet_affiliateテーブルはgetJobeetAffiliates()で呼ぶことができます。
今回はありませんが、テーブルa→b→cなんて関連があった場合でも、$a->getB()->getC()といったふうにすることができるので非常に便利です。
上で使用したgetJobeetJobs()メソッドは、関連するカラムを全て引っ張ってくるという内容ですので、たとえばexpired_atが切れているようなものがあったとしても気にせず呼び出してしまいます。
job_extreme_sensio
job_sensio_labs
job_expired
の3行が表示されることになりま………………されないぞ?
job_expiredが表示されません。
本来getJobeetJobs()の際は、リレーション以外の条件を気にしないはずなのですが、ログを見てみると、getJobeetJobs()の際に新たなSQLが発行されず、上記SQLの実行結果をそのまま使い回していました。
そりゃ表示されるわけ無いな。
しかしこの動作では、チュートリアルのように以下のJobeetCategory::getActiveJobs()メソッドを作成する意味が全く無いのですが。
まあいいや、気を取り直して。
本来はget+モデル名のメソッドを呼び出した場合、リレーション以外の条件を全く気にせずにカラムを拾ってきてしまいます。
上記のgetJobeetJobs()で拾わなかったのは偶然です。たぶん。きっと。おそらく。
ということでexpired_at切れを確実に排除するために、getJobeetJobs()のかわりにgetActiveJobs()メソッドを作成することにします。
前JobeetJobTableクラスにgetActiveJobs()を作成しましたが、今回はJobeetCategory::getActiveJobs()です。
二者の違いは、前者はカテゴリを気にせず、後者はカテゴリを気にするということです。
チュートリアルではいきなり分けてますが、わかりにくいのでとりあえずJobeetCategoryに全部書いてしまうことにします。
lib/model/doctrine/JobeetCategory.class.php
1
2
3
4
5
6
7
8
|
public function getActiveJobs(){
$q = Doctrine_Query::create()
->from('JobeetJob j')
->addWhere('j.category_id = ?', $this->getId())
->addWhere('j.expires_at > NOW()')
->addOrderBy('j.expires_at DESC');
return $q->execute();
}
|
$this->getId()の$thisは一見何処からやってくるのかさっぱりなのですが、きちんとテンプレートでループ中の$category.idを取得してきてくれます。
メソッドを作成したのでテンプレートに反映します。
apps/frontend/modules/job/templates/indexSuccess.php
1
2
3
4
5
6
7
|
foreach($categories as $category){
$jobeetjobs=$category->getActiveJobs();
foreach($jobeetjobs as $jobeetjob){
print($jobeetjob->token.'<br />');
}
}
|
job_extreme_sensio
job_sensio_labs
の2件がやっぱり表示されました。
こちらの場合はSQLを使い回しているわけではなく、ループのたびにJobeetCategory::getActiveJobs()が呼ばれ、SQLが実行されています。
今回はカテゴリが2件しかなかったので追加発行されるSQLも2件なのですが、今後カテゴリが1000個に増えたよ、みたいなことになれば発行回数がえらいことになってしまいます。
なのでこの設計はいまいちよろしくないのですが、とりあえずまあいいや。
どうでもいいのですがDoctrineのマニュアルではWHERE句追加のメソッドはandWhereです。
確かにor系メソッドとの対比上andが正しいのでしょうが、よく使われるaddも使用できるようになっています。
さて、jobeet_jobテーブルに関する処理内容がJobeetCategoryに書かれているという事態は好ましくありません。
ということで適切なクラスにメソッドを分けてしまいましょう。
まずJobeetCategory。
lib/model/doctrine/JobeetCategory.class.php
1
2
3
4
5
6
|
public function getActiveJobs(){
$q = Doctrine_Query::create()
->from('JobeetJob j')
->where('j.category_id = ?', $this->getId());
return Doctrine::getTable('JobeetJob')->getActiveJobs($q);
}
|
この$qの持ち回しがポイントです。
呼ばれる側のJobeetJobTable::getActiveJobs()を修正。
lib/model/doctrine/JobeetJobTable.class.php
1
2
3
4
5
6
7
8
9
|
public function getActiveJobs(Doctrine_Query $q = null){
if (is_null($q)) {
$q = Doctrine_Query::create()
->from('JobeetJob j');
}
$q->andWhere('j.expires_at > NOW()')
->addOrderBy('j.expires_at DESC');
return $q->execute();
}
|
引数があればそのクエリにWHERE句を付け足し、無ければ単純にJobeetJobモデルを作成して返すということになります。
単に先ほどのJobeetCategory::getActiveJobs()を二つに分けただけなので、動作結果も全く変わりません。
しかし、このTableが付くか付かないかのモデルの使い分けがよくわからん。
Symfonyの記事一覧
6日目までのチュートリアルにはapp.ymlの書き方が見つからなかったので簡単に解説。
config_handler.ymlまでやり始めるとよくわからないのでとりあえずはパス。
まずapp.ymlが何かっていうと、自作プログラムでありがちなconfig.phpとかconst.phpみたいなものです。
動的に変更されることはあまりないけど、サイト全体で共有しておきたい設定なんかを格納しておきます。
YAML形式で書くことで、PHPで書くより素人にもわかりやすく、XMLみたいにタグがいっぱいでめんどくさいといったこともない簡単な設定ファイルにすることができます。
app.ymlを置ける場所は複数あります。
config/app.yml
apps/frontend/config/app.yml
前者がプロジェクト全体から呼び出せるもので、後者はアプリケーション単位の定義となります。
使用する際には両者がマージされ、両者で同じ名前がある場合は範囲の狭いアプリケーション単位のほうが優先されます。
後者が置けるのだから当然apps/frontend/modules/***/config/app.ymlでモジュール単位の設定もできるだろうと思ったら何故かできませんでした。
次にYAMLの書き方。
6日目のチュートリアルでは下記のようになっています。
1
2
|
all:
active_days: 30
|
トップレベルにはall、prod、test、devの4種類を置くことができます。
allは全環境、prodは本番環境、devは開発環境でのみ設定される内容になります。
testはあんまり使わないのでまあいいや。
要素は半角スペース2つでインデントしたあとに記入します。
:のあとに半角スペースを最低1個置いたあと値を記入すると、それが要素の値となります。
上記の場合はactive_daysの値が30ということになります。
複数のトップレベルに同じ要素を記入した場合、範囲が狭い方が優先されます。
従って最終的な書き方は以下のようになります。
1
2
3
4
5
6
7
|
all:
hoge: hoge_all
prod:
name: 本番環境
dev:
name: 開発環境
hoge: hoge_dev
|
sfConfig::get()で実際に値を取得することができます。
app.ymlから内容を取得する場合は、引数のプレフィックスに必ず'app_'が付きます。
開発環境で実行した場合、
sfConfig::get('app_hoge') → hoge_dev
sfConfig::get('app_name') → 開発環境
本番環境で実行した場合、
sfConfig::get('app_hoge') → hoge_all
sfConfig::get('app_name') → 本番環境
というふうになります。
基本的には、allに全ての設定を記入し、prodで本番時のみ参照したいデータを記述するということになるでしょう。
さて、YAMLを階層構造にすることができます。
半角スペースを2つ増やすたびに一段階深くなります。
カテゴリ毎に情報を纏めたたりする場合に便利なのですが……ここにひとつ落とし穴があります。
1
2
3
4
5
6
7
8
|
all:
data: data_zero
one:
data: data_one
two:
data: data_two
three:
data: data_three
|
このようなYAMLがあるとして、階層構造のデータを取得するときは'_'で区切ることで次の階層に進むことができます。
'data_zero'を取得したいならsfConfig::get('app_data')です。
'data_one'を取得したい場合はsfConfig::get('app_one_data')となります。
ここまではいいのですが、では次の'data_two'を取得したければsfConfig::get('app_one_two_data')でいいかと思えば駄目です。
3段階目以降は何故か直接sfConfig::get()することができません。
sfConfig::get('app_one_two')とすることで、それ以下の階層が連想配列で入ってきます。
'data_three'は、$data_two=sfConfig::get('app_one_two');$data_three=$data_two['three']['data'];とする必要があります。
当初これに気がつかずに「値が取得できねー」とさんざっぱら悩みました。
リファレンスに微塵も書かれてないというのはどういうことなのだろうな。
Symfonyの記事一覧
まずDoctrineの検索条件から。
Doctrine::getTable('JobeetJob')->createQuery('a')->execute();
とするとjobeet_jobテーブルの内容が全て取得されます。
getTableでテーブル名を指定し、createQueryでエイリアス名を指定します。
このエイリアス名は作成されるSQLにも出力を扱う際にも全く関係が無く、Doctrine内でカラムやテーブルを扱う際にのみ利用されます。
最後にexecuteで作成したクエリを実行し、取得します。
今回はWHERE句やLIMIT句等を使用していないため、jobeet_jobテーブルの内容が全件そのまま取得されます。
それでは今からクエリを書き換えていきますが、チュートリアルは丸ごと全部書き換えてしまっているのでいきなりハードルが高くてわかりにくいです。
とりあえずさっきのクエリにひとつ条件を書き足してみます。
Doctrine::getTable('JobeetJob')->createQuery('a')->where('a.id = ?',1)->execute();
whereは文字通りWHERE句を指定します。
第一引数に比較する内容、第二引数に具体的な値を記述します。
上記の場合'?'の部分が第二引数の1に相当します。
where('JobeetJob.id = ?',1)と書くとおかしな結果になります。
createQuery('a')で、以後JobeetJobを'a'として扱うと決めたので、その後最初のjobeet_jobテーブルを扱う際は'a'と書く必要があるのです。
上記の結果として、jobeet_job.id=1のカラムだけが取得されることになります。
今回は1ですが、ここにユーザ入力値を突っ込むことで自動的にSQLインジェクション対策になります。
固定値の場合であればプリペアドステートメントにする必要もないので、where('a.id = 1')と書くこともできます。
上記をチュートリアル形式に書き換えてみます。
$q = Doctrine_Query::create()->from('JobeetJob a')->where('a.id = ?',1);
$this->jobeet_job_list = $q->execute();
最初に呼び出すクラスがDoctrineクラスからDoctrine_Queryに変わっていますが、実行できることはあまり変わりません。
というか違いがよくわからん。
Doctrine_Query::fromではDoctrine::getTableとcreateQueryが行っていたテーブル名とエイリアス名の指定を一気に行います。
以後はwhereやexecuteを全く同じように書くことができます。
当然ですが$qを介さず、
$this->jobeet_job_list =Doctrine_Query::create()->from('JobeetJob j')->where('j.id = ?',1)->execute();
と一行で書くこともできます。
結局Doctrine_Query::create()とDoctrine::getTable()って何が違うんだ。
ようやくチュートリアルに追いついた。
30日以内に投稿された求人だけ拾ってみましょう。
apps/frontend/modules/job/actions/actions.class.php
1
2
3
4
|
$q = Doctrine_Query::create()
->from('JobeetJob j')
->where('j.created_at > ?', date('Y-m-d H:i:s', time() - 86400 * 30));
$this->jobeet_job_list = $q->execute();
|
単にwhereメソッドが変わっただけですね。
idのときは=だけでしたが、不等号や関数を記述することもできます。
しかし、30日以内とかなら
$q->where('j.created_at > NOW()-INTERVAL 30 DAY ');
のほうが早い気が。
実際に実行されたSQLはブラウザ右上のドラム缶みたいなマークで確認することができますので、見てみるとよいでしょう。
次にモデルを変更してみます。
前slugifyとかを書いたJobeetJob.class.phpです。
lib/model/doctrine/JobeetJob.class.php
1
2
3
4
5
6
7
|
public function save(Doctrine_Connection $conn = null){
if ($this->isNew() && !$this->getExpiresAt()){
$now = $this->getCreatedAt() ? strtotime($this->getCreatedAt()) : time();
$this->setExpiresAt(date('Y-m-d H:i:s', $now + 86400 * 30));
}
return parent::save($conn);
}
|
モデルクラスのsaveメソッドをオーバーライドすればセーブ時の動作を変化させることができるそうです。
そういった流れというか挙動は一体何処を調べればわかるのでしょうか?
あと、よくわからんのだが、シリアライズってこんなところで使う言葉なのか?
$this->isNew()は一度も保存されていないときにtrueを返すと思われます。
Doctrineの各モデルクラスはsfDoctrineRecordを継承しており、sfDoctrineRecord::isNew()に実体があります。
>(boolean) isNew ()
>Function require by symfony >= 1.2 admin generators
どういう意味だ。
こういうのの存在ってチュートリアルが無ければ気付きようが無い気がするんだがどうしてるんだろう?
全体としては、現在のオブジェクトが、一度もセーブしたことが無く、expires_atが定義されていない場合、expires_atを指定して親のsaveメソッドを呼ぶということになります。
2回目以降のセーブ、もしくは既にexpires_atが入っている場合はそのまま親のsaveメソッドが呼ばれます。
せっかくexpires_atを登録できるようにしたので、呼び出す場合もexpires_atを利用するようにしましょう。
apps/frontend/modules/job/actions/actions.class.php
1
2
3
4
|
$q = Doctrine_Query::create()
->from('JobeetJob j')
->where('j.expires_at > ?', date('Y-m-d H:i:s', time()));
$this->jobeet_job_list = $q->execute();
|
expires_atが現在より先のものだけ拾ってくるようになります。
これもやっぱり
$q->where('j.expires_at > NOW()');
のほうが早いんですがね。
このDQL変更によって、expires_atがNULLであるレコードは拾えなくなってしまいます。
新たなデータを例によってフィクスチャを利用して登録してみます。
しかし正直、どうもこの機能が便利とはとても思えないんですが。
データベースをいじって最初に投入したデータを変更していた場合、このチュートリアル通りにするとそこらへんの変更が全部消え去ってしまいます。
data/fixtures/jobs.ymlに、該当のYAMLのexpired_job:を追加します。
そしてコマンドを実行。
>php symfony doctrine:data-load
今回追加するこのexpired_job:にはexpires_atが指定されていないため、本来ならconfig/doctrine/schema.ymlの
JobeetJob:expires_at: { type: timestamp, notnull: true }
条件に引っかかってしまいます。
ところが上でJobeetJob.class.phpでsaveメソッドをオーバーライドしているため、expires_atに何も入れない場合、自動的にexpires_atを設定した上でセーブしてくれます。
expired_job.expires_atは、JobeetJob::save()メソッドに従いcreated_atの30日後に設定された上で保存されていました。
ついでに元あったデータは、直接DBをいじくって変更していたものがあったりした場合でも全削除されます。
このコマンドが効くのは開発用データベースだけではあるのですが、それでもテスト用データがたくさん詰まった状態なんかでうっかりコマンドを打ってしまうと大惨事。
現在のデータベースの内容をYAMLに書き出すコマンドは無いのか?
カスタムコンフィギュレーションは単にapp.ymlに設定を分離するというだけの話です。
apps/frontend/config/app.ymlにチュートリアルをコピペ。
この後コントローラからでもモデルからでもビューからでも、
sfConfig::get('app_active_days')
でapp.ymlに書かれている値を引っ張ってくることができるようになります。
JobeetJob::saveメソッドの該当部分を以下のように書き換えます。
$this->setExpiresAt(date('Y-m-d H:i:s', $now + 86400 * sfConfig::get('app_active_days')));
やっぱり掲載から60日は表示されるようにしたい、といったときに直接ロジックを書き換えることなく、設定ファイルを書き換えるだけで済むようになります。
最後にリファクタリング。
これまでDoctrine_Query::where('j.expires_at > NOW()')とかのロジックをコントローラに書いていました。
expires_atみたいなデータベースに直接関連する内容はコントローラに書かずに、全てモデルに押しやってしまいます。
コントローラを書き換えます。
apps/frontend/modules/job/actions/actions.class.php
1
2
3
|
public function executeIndex(sfWebRequest $request){
$this->jobeet_job_list = Doctrine::getTable('JobeetJob')->getActiveJobs();
}
|
JobeetJobTable::getActiveJobs()を書いたからには作らないといけません。
lib/model/doctrine/JobeetJobTable.class.php
1
2
3
4
5
|
public function getActiveJobs(){
$q = $this->createQuery('j')
->where('j.expires_at > ?', date('Y-m-d H:i:s', time()));
return $q->execute();
}
|
まあ見ての通り、これまでコントローラに書いてあった処理をJobeetJobTableモデルに持っていっただけです。
$thisになっていますが、別に
return Doctrine_Query::create()->from('JobeetJob j')
->where('j.expires_at > ?', date('Y-m-d H:i:s', time()))->execute();
とか書いても動きます。
コントローラでDoctrine::getTable('JobeetJob')を実行した場合、内部ではJobeetJobTableクラスが呼ばれています。
これまでのようにコントローラに検索ロジックを書いていた場合、複数の箇所で使用したくなったらその都度同じ処理を書かねばなりませんでした。
JobeetJobTableクラスにメソッドを記述することで、全てのプロジェクトからこのメソッドを使用できるようになります。
Symfonyの記事一覧
前回の続き。
ここまではリンクの書き方とrouting.ymlで完結していました。
ここまでは単にURLとルーティングをマッチングさせているだけなのでまあわからないこともないです。
この後突然ルートクラスという単語が登場し、全然意味がわからなくなってしまいます。
個人的には、下記のように一カ所を変更したいと思う度に至る所を修正しなければならない作りが正しいとはとても思えないんですがどうなんですかね。
まずsfRequestRoute。
こちらは通常ルーティングで使われるはずのsfRouteに、リクエストメソッドによる分岐機能を追加したものになります。
同じURLにリクエストが来た場合でも、<a>や<form method="GET">で飛んできた場合はsf_method:[get]に合致し、<form method="POST">で飛んできた場合はsf_method:[post]に合致する、というふうに分岐を行うことができます。
routing.ymlのjob_show_user:url:を変更。
1
2
3
4
5
6
7
|
job_show_user:
url: /job/:company/:location/:id/:position
class: sfRequestRoute
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
|
class: sfRequestRouteとrequirements:sf_method: [get]が増えました。
これで
http://symfony.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
というURLに対し、リクエストメソッドがGETである場合のみにjob_show_userにマッチするようになります。
<form method="POST">で来た場合はルートが見つからないためエラーになります。
こちらは比較的わかりやすい。
次にsfDoctrineRoute。
routing.ymlのjob_show_user:url:をsfRequestRouteからsfDoctrineRouteに変更。
1
2
3
4
5
6
7
8
|
job_show_user:
url: /job/:company/:location/:id/:position
class: sfDoctrineRoute
options: { model: JobeetJob, type: object }
param: { module: job, action: show }
requirements:
id: \d+
sf_method: [get]
|
apps/frontend/modules/job/templates/indexSuccess.phpを変更。
url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))
class:sfDoctrineRouteでルーティングにsfDoctrineRouteを指定し、model:JobeetJobでルートに関係するモデルクラスを指定し、type:objectでこのルートに関係するオブジェクトを定義します。
何を言ってるんだこいつは?
全く全然さっぱりちっとも意味がわかりませんが、何故かリンクは表示されました。
/frontend_dev.php/job/Sensio+Labs/Paris%2C+France/1/Web+Developer
というようなURLになります。
なんで?
どこがどう繋がってこんなことになってるの?
さらにカスタマイズします。
URLに+とか%2Cといった文字が出てくるのはあまりよろしくないのでURLをslugifyします。slugifyって何よ?
lib/model/doctrine/JobeetJob.class.phpにgetSlug系メソッドを追加。
// lib/model/doctrine/JobeetJob.class.php
1
2
3
4
5
6
7
8
9
|
public function getCompanySlug(){
return Jobeet::slugify($this->getCompany());
}
public function getPositionSlug(){
return Jobeet::slugify($this->getPosition());
}
public function getLocationSlug(){
return Jobeet::slugify($this->getLocation());
}
|
lib/Jobeet.class.phpを作成してチュートリアルからJobeetクラスをコピペ。
// lib/Jobeet.class.php
1
2
3
4
5
6
7
8
9
|
class Jobeet{
static public function slugify($text){
// 文字ではないもしくは数値ではないものすべてを-に置き換える
$text = preg_replace('/\W+/', '-', $text);
// トリムして小文字に変換する
$text = strtolower(trim($text, '-'));
return $text;
}
}
|
routing.ymlのjob_show_user:url:を変更。
url: /job/:company_slug/:location_slug/:id/:position_slug
jobActions::executeShowを変更。
$this->job = $this->getRoute()->getObject();
http://symfony.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
リンクが正しくなりました。めでたしめでたし。
JobeetJob::getCompanySlugは何処からどうやって呼ばれてるんだ?
lib/Jobeet.class.phpはいったい何処で此処に置くことを決定しているんだ?
getRouteって何?getObjectって何?
全然わかりません。
なんでリンクするだけのためにこんな馬鹿みたいなことをしないといけないのでしょうか。
あと日本語は文字化けします。
ルートコレクションという機能もあります。
基本的なフォームの閲覧から作成、編集までのひとまとまりのルーティングを纏めてキャッチしてくれます。
apps/frontend/config/routing.ymlに以下を追加します。
1
2
3
|
job:
class: sfDoctrineRouteCollection
options: { model: JobeetJob }
|
なんか色々なルートを纏めて作成できるそうです。
皆さんわかりましたか?
私にはさっぱりわかりません。
最後にデフォルトルートを削除します。
デフォルトルートを削除した場合、定義したルート以外には辿り着けなくなります。
変なURLによるアクセスを簡単に排除することが出来ます。
逆に、今後モジュールを追加したとしても、routing.ymlを編集しない限りアクセスできなくなります。
Symfonyの記事一覧
http://symfony.localhost/frontend_dev.php/job/show/id/3
というURLで何故frontendアプリケーションのjobActions::executeShow()が実行されるのか、というお話。
まずテンプレート上で
url_for('a/b?c=d&e=f')
と書くと、url_for関数が勝手に
<a href="frontend_dev.php/a/b/c/d/e/f">といったふうにリンクを書き換えます。
indexという文字が現れたときとかの例外はありますが、今のところは特に気にする必要はありません。
リンク先をクリックした場合、まず第一引数であるaというモジュールを探し、その中のaActions::executeB()を実行しようとします。
三番目以降の引数はリクエストパラメータとなります。
まあこれが基本なのですが、そのルーティングの設定はapps/frontend/config/routing.ymlというファイルで決定されています。
これを変更することで、一部のリクエストだけ特別な処理を行ったりすることができます。
先にマッチしたルートが実行されるので、設定の順番は狭い順にします。
とりあえずデフォルトの設定を見てみます。
最初の段落、
1
2
3
|
homepage:
url: /
param: { module: default, action: index }
|
/はindex.phpとかfrontend_dev.php/といった、引数が一個もない状態で来た場合にマッチします。
この場合、defaultモジュールのindexアクションが実行されます。
defaultモジュールは作成していませんが、Symfonyで最初から用意されているモジュールで、index以外にもerror404やdisabled、secureといった基本的なページが用意されています。
前回あったforward404Unless()は、内部的にdefaultモジュールのerror404アクションを呼び出しています。
homepage:は単なるラベルなので適当に変更してもかまいません。
ただ後述の@のためにわかりやすい名前にしておきましょう。
二段落目は
1
2
3
|
default_index:
url: /:module
param: { action: index }
|
これはモジュール名だけやってきてアクション名がなかった場合、デフォルトのアクション名はindexにするよ、というだけの内容です。
たとえば
param: { action: hoge }
に変更して
http://symfony.localhost/frontend_dev.php/job
にアクセスすると、jobモジュール内にhogeアクションが見つからないよ、というエラーになります。
http://symfony.localhost/frontend_dev.php/job/index
とアクション名まで書いた場合はきちんと表示されますが、これは最後の段落です。
1
2
|
default:
url: /:module/:action/*
|
モジュール名とアクション名が入っていれば、その後どのようなパラメータが入っていてもこの段落に当てはまります。
具体的な指定は何もありませんが、すなわちモジュール名とアクション名をそのまま何も変更しないということです。
ではルートをカスタマイズしてみましょう。
まずはチュートリアルの通りに
1
2
3
|
homepage:
url: /
param: { module: job, action: index }
|
と上書きします。
これで、引数が無い場合にはjobモジュールのindexアクションが実行されるということになり、
http://symfony.localhost/frontend_dev.php
http://symfony.localhost/frontend_dev.php/job/index
この両者が同じになります。
更にルーティングを追加。
1
2
3
4
|
job_show_user:
url: /job/:company/:location/:id/:position
param: { module: job, action: show }
requirements: {id: \d+ }
|
default:の/:module/:action/*が先に来るとそちらに引っかかってしまうので、default:より先に記述する必要があります。
そして以下のURLでアクセスします。
http://symfony.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
urlに:が付く場合は該当するパラメータの内容を表し、付かない場合はURLそのものを表します。
url: /job/:company/:location/:id/:position
の場合、
/job/はそのものが必須であり、
http://symfony.localhost/frontend_dev.php/job/sensio-labs/paris-france/1/web-developer
のようなURLには引っかかりますが、
http://symfony.localhost/frontend_dev.php/jobs/sensio-labs/paris-france/1/web-developer
などのURLにはひっかからないということを示します。
/:companyは、その位置に該当するパラメータということで、今回の場合は"sensio-labs"を表します。
このパラメータは、アクション内で$request->getParameter('company')として受け取ることができます。
どのような場合に使うかというと、URLをわかりやすくすることができます。
とりあえず今思いついたものとしては静的ページを配置する場合などに使えるかもしれません。
1
2
3
|
static_page:
url: /static/:name
param: { module: index, action: static }
|
としてindexActions::executeStaticで$this->setTemplate($request->getParameter('name'))とか。
requirementsエントリはリクエストパラメータの絞り込みです。
id: \d+なので、:idの位置に来る値が数値であることを要求します。
当てはまらなかった場合はstatic_page:のルーティングには該当しないということになります。
@を使用したリンクを作成してみます。
url_for('@job_show_user?id=1&position=pos&company=comp&location=loc');
とすると、URLの形式がjob_show_user:のURL形式である、/job/:company/:location/:id/:positionに合致するように変形されます。
@を使わない形式ではurl_forに配列を渡せるのに@を使用するとできないのは何故なんだろう。
長くなったので続きは後日。
これ一時間とか絶対無理だよな。
Symfonyの記事一覧
一日目にfrontendアプリケーションを作成しましたが、このアプリケーションの具体的な実装はapps/frontendディレクトリに書いていくことになります。
まずapps/frontend/templates/layout.phpを見てみます。
このファイルは、index.phpおよびfrontend_dev.phpを呼び出したときに必ず呼び出されるテンプレートになります。
全体的なレイアウトをここで設定し、後ほど書いていく個別テンプレートは全部$sf_contentに入る、という形になります。
チュートリアルのfrontend/templates/layout.phpをコピペします。
また、CSSと画像ファイルをそれぞれweb/css/とweb/images/に設置します。
http://symfony.localhost/frontend_dev.php/job
を開いてみると、いきなりグラフィカルな画面になっています。
なに?解説が手抜き?気にするな。
さて、ここからようやくSymfonyの気持ち悪い機能を繙いていくことになります。
テンプレの各所にinclude_stylesheets()とかurl_for('job/index')といった見慣れない関数が鎮座しています。
これらはSymfonyで定義される色々な機能を呼び出すヘルパーです。
で、最初にも書きましたがSymfonyにはこういった機能の一覧/検索がないんですよね。
おかげで延々パッケージやチュートリアル、リファレンスを探し回される羽目になります。
include_stylesheets()はapps/frontend/config/view.ymlからstylesheetsハッシュを拾ってきて<link rel>タグにして表示します。
include_javascripts()は同様にjavascriptsハッシュを<script>タグで表示します。
上書きする前に存在したinclude_http_metas()やinclude_title()等を含め、これらのインクルード関数はAssetヘルパーに属しているのですが、そのことを先に知っていないとAPIリファンレンスで調べることができないという。
apps/frontend/config/view.ymlはフロントエンドを経由する全てのテンプレートに適用されますが、一部のモジュールのみ、一部のビューのみに設定を行うこともできます。
apps/frontend/modules/job/config/view.ymlを設定することでjobモジュールに、さらにその中でindexSuccess:とビューを指定することでビュー単位に指定できます。
アクションを触ってみます。
http://symfony.localhost/frontend_dev.php/job
はhttp://symfony.localhost/frontend_dev.php/job/indexと解釈されます。
そしてweb/frontend_dev.phpからapps/frontend/modules/job/actions/actions.class.phpが呼び出され、その中のexecuteIndexアクションが実行されることになります。
アクションで実行されている内容は実質わずか一行です。
$this->jobeet_job_list = Doctrine::getTable('JobeetJob')->createQuery('a')->execute();
簡単に言うとDoctrineを使用してJobeetJobを呼び出し、全レコードを取得して$this->jobeet_job_listに投入する、という内容です。
さて、executeIndex()はテンプレートへの変数のセットとかテンプレートの呼び出しといったことを一切行っていません。
どうなっているのでしょうか。
何も書かなかった場合、アクション名と同じテンプレートが自動的に呼び出されます。
今回のexecuteIndex()の場合は、テンプレートとしてapps/frontend/modules/job/templates/indexSuccess.phpが適用されます。
さらにその結果がapps/frontend/templates/layout.phpの$sf_contentに入ります。
で、apps/frontend/modules/job/templates/indexSuccess.phpを見てみると、いきなり
foreach ($jobeet_job_list as $jobeet_job)
とか書いてあります。
実は、アクションで$this->hogeに入れた内容はテンプレートからいきなり$hogeで呼ぶことができるのです。
なんとなく怖い。
$jobeet_job_listにはDoctrineの巨大なオブジェクトが入っていますが、イテレータを実装しているので配列として扱うことができます。
各行を表す$jobeet_jobに対し、$jobeet_job->getId()または$jobeet_job->idでjobeet_job.idを取得することができます。
Doctrineの便利なところは$jobeet_job->JobeetCategory->getName()とリレーションを辿っていけるところですが今回はそれは無いようです。
現在はどう見ても不要なカラムまで全部表示しているので、必要な部分だけを表示するようにします。
まあ例によってチュートリアルのapps/frontend/modules/job/templates/indexSuccess.phpをコピペするだけですが。
詳細画面に移動するとURLが次のようになります。
http://symfony.localhost/frontend_dev.php/job/show/id/2
jobアクションのexecuteShowが実行されます。
$request->getParameter('id')で/id/2の値、即ち2が取得されます。
Doctrine::getTable('JobeetJob')->find(2);
とすると主キーが2であるカラムを一件拾ってきます。
非常にわかりにくいので、
Doctrine::getTable('JobeetJob')->findOneById(2);
と引いてくるカラムと件数を明記した方がいいでしょう。
返ってくる値はどちらも同じです。
このfindBy系のメソッドは例によってスキーマから自動作成されており、findByEmailでjobeet_job.emailが一致したのを全件検索、findOneByTypeでjobeet_job.typeが一致する先頭一件を取得といった書き方ができます。
executeShowアクションから使用されるテンプレートはapps/frontend/modules/job/templates/showSuccess.phpです。
デフォルトでは全てのカラムが表示されていますので、これまたチュートリアルをコピペしましょう。
何故かテンプレート内のjobeet_jobオブジェクト名が$jobに変わっているので、アクションもそちらに合わせて変更します。
テンプレートの頭にuse_helper('Text')というのが入っていますが、これを記述することでヘルパーの一種であるTextHelperが使用できるようになります。
AssetHelperは何も書かずに使えたのに何故?
さあ?
use_helperを書かずに使用できるのはAssetHelper、CacheHelper、EscapingHelper、HelperHelper、PartialHelper、TagHelper、UrlHelperです。
何この不平等。
ていうかここらへんのことAPIリファレンスの何処にも書かれていないように見えるんですが。
次にスロットの説明が出てきます。
スロットはPartialHelperに含まれるのでuse_helper宣言をせずにいきなり使用できます。
スロットは出力制御みたいなもので、単に変数に値を代入するものです。
1
2
3
|
slot('title');
print('あいうえお');
end_slot();
|
とすると、slot()からend_slot()までで挟まれた部分が全て変数titleに入ります。
1
|
include_slot('title');
|
とすることでその内容を表示することができます。
次に404です。
アクション内で$this->forward404Unless($hoge)とした場合、$hoge==falseであれば404ページを表示します。
変なリクエストとかを簡単にはじけるので便利。
そのうち404ページのカスタマイズ方法も出てくるかと思いますが現状不明。
最後にリクエストとレスポンスメソッド一覧表が出てきますが、何故こんな大事なものをチュートリアルの片隅に載せるんだ。
sfWebResponseの使い方は此処には書いてありませんが、getResponseメソッドで呼ぶことができます。
$this->getResponse()->setCookie('hoge','fuga');
といったふうに使用します。
>本日のチュートリアルの最初の方でview.ymlとテンプレートの両方でスタイルシートやJavaScriptを管理するやり方を見ました。
>結局2つのテクニックともレスポンスオブジェクトのaddStylesheet()とaddJavascript()メソッドを使います。
唐突に出てきていきなりなんのこっちゃ?という感じですが、最初の方で出てきたinclude_javascripts()はその中で
$this->getResponse()->addJavascript()
的なことをやっているよ、という意味です。
中を見る限りなんかちょっと違うようなかんじだったのですがまあいいか。
Symfonyの記事一覧
CakePHP1.2.5のフォームヘルパーの全メソッドと引数を解説。
Paginatorと違いヘルプが充実しているので要らないような気もしますが、と思ったら未完なところが多かった。
create ( [ $model [ , $options ] ] ) | |
---|---|
<form>タグを作成する | |
引数 | 解説 |
$model | モデル名を指定。 デフォルトは現在のモデル。 配列を渡すとモデル名ではなく$optionsとして解釈される。 |
$options['type'] | デフォルトは'post'で要素は'post','get','put','delete','file' 'get'以外は<form method>が'POST'になり、'_method'=$options['type']がhiddenで渡される。 'file'はenctype="multipart/form-data"が追加される。 |
$options['action'] | 渡す先のアクション名を変更する。デフォルトは'add'。 |
$options['url'] | 渡す先のコントローラ名から先を全て変更する。 |
$options['default'] | falseにするとonsubmitでreturn false;される。 |
end ( [ $options ] ) | |
---|---|
</form>タグを作成する | |
引数 | 解説 |
$options | 文字列であればその値で<input type="submit">を作成し、その後に</form>を作成する。 配列ならば各キーと値が<input type="submit">に引き渡される。 無指定なら</form>のみ。 |
secure ( $fields ) | |
---|---|
セキュリティトークンをhiddenで発行する。 Securityコンポーネント使用時に使用可能 |
|
引数 | 解説 |
$fields | 暗号化するフィールド名の配列。デフォルトは全入力欄の名前。 有効であればend()時に自動的に発行されるので通常指定する必要はない。 |
input ( $fieldName [ , $options ] ) | |
---|---|
入力フォームを作成する。 フォームの種別は入力値によって自動的に(or手動で)変わる。 |
|
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][fieldName]" />といった値になる。 入力欄の前にもフィールド名が表示される。 |
$options['type'] | フォームの種別を指定する。 値はtext,textarea,select,radio,password,datetime,date,time,checkbox,hidden,file等。 指定されていない場合、モデルにフィールド名と同じ名前のカラムがあればその型から決められる。 見つからない場合、$options['options']、$options['length']等を参照してそれっぽい型に決められる。 |
$options['default'] | 最初から入力されている値を指定。 入力値が存在する場合はそちらが優先される。 |
$options['value'] |
最初から入力されている値を指定。 常時入力値より優先される。 |
$options['selected'] | $options['type']='select','datetime'等のときに指定可能。 デフォルトで選択されるキーを指定。 入力値より優先される。 $options['default']と$options['selected']両方指定している場合はselectedが優先。 |
$options['escape'] | falseにすると$options['value']をエスケープせずに表示する。 |
$options['before'] | フィールド名の前に表示する内容があれば記述。 |
$options['between'] | フィールド名と<input>タグの間に表示する内容があれば記述。 |
$options['after'] | <input>タグの後に表示する内容があれば記述。 |
$options['separator'] | $options['type']='radio'のときに指定可能。 各ラジオボタンの間に表示する内容があれば指定。 |
$options['options'] | <option>の中身とする配列を指定。 指定すると<select><option>になる。 |
$options['multiple'] | $options['type']='select'のときに指定可能。 $options['multiple']='multiple'かtrueなら<select multiple="multiple" >になる。 $options['multiple']='checkbox'なら$options['options']をチェックボックスとして出力する。 |
$options['maxlength'] | 指定可能なときにmaxlength属性を指定する。 |
$options['div'] | 全体を括る<div>の属性を指定する。 文字列であれば<div class="入力値">となる。 連想配列であれば各キーと値が属性にセットされる。 falseにすると<div>自体が出力されない。 |
$options['label'] | <label>で挟まれる文字、即ち$fieldNameの表記のみを変更したいときに指定する。 <label for>の値ではない。 連想配列であれば各キーと値が<label>の属性にセットされる。文字自体は'text'で指定。 falseにすると<label>自体が出力されない。 |
$options['legend'] | $options['type']='radio'のときに指定可能。 <fieldset>内の<legend>の値を指定する。 falseにすると<fieldset>自体が出力されない。 |
$options['id'] | <input>のidの値、及び<label for>の値を変更する。 |
$options['error'] | バリデーションエラーがある場合に表示するエラーメッセージを変更する。 falseにすると非表示になる。 |
$options['cols'] $options['rows'] |
テキストエリアのrowsとcolsを指定。 指定すると<textarea>になる。 |
$options['empty'] | $options['type']='select','datetime'等のときに指定可能。 <option>の最上段に表示される値を指定する。 trueなら空白になる。 マニュアルでは<input>についても書かれているが、<input>には無影響。 http://book.cakephp.org/ja/view/201/options-empty |
$options['dateFormat'] | $options['type']='datetime','date'のときに指定可能。 引数はdateTime()の$dateFormatと同じ。 |
$options['timeFormat'] | $options['type']='datetime','time'のときに指定可能。 引数はdateTime()の$timeFormatと同じ。 |
$options['minYear'] $options['maxYear'] |
$options['type']='datetime','date'のときに指定可能。 年の選択肢に表示する最大値、最小値を指定。 デフォルトは現在±20年。 |
$options['interval'] |
$options['type']='datetime','time'のときに指定可能。 分の選択肢を指定した間隔毎に表示する。 |
submit ( [ $caption [ , $options ] ] ) | |
---|---|
submitボタンを表示する | |
引数 | 解説 |
$caption | submitボタンに表示する文字列。 '://'が含まれる、あるいは画像ファイルであれば<input type="image">になる。 |
$options['div'] | 全体を括る<div>の属性を指定する。 文字列であれば<div class="入力値">となる。 連想配列であれば各キーと値が属性にセットされる。 falseにすると<div>自体が出力されない。 |
button ( [ $title [ , $options ] ] ) | |
---|---|
buttonボタンを表示する | |
引数 | 解説 |
$title | ボタンに表示する文字列。 |
$options['type'] | <input type="button">の値を上書きする。 意味があるのは'reset','submit'くらい。 |
$options['title'] | $titleの値を上書きする。 |
$options['name'] | ? |
text ( $fieldName [ , $options ] ) | |
---|---|
<input type="text">を作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | <input>の各要素を指定。 |
password ( $fieldName [ , $options ] ) | |
---|---|
<input type="password">を作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | <input>の各要素を指定。 |
hidden ( $fieldName [ , $options ] ) | |
---|---|
<input type="hidden">を作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | <input>の各要素を指定。 |
textarea ( $fieldName [ , $options ] ) | |
---|---|
<textarea>を作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options['value'] | <textarea>内に表示する値を指定する。 |
checkbox ( $fieldName [ , $options ] ) | |
---|---|
<input type="checkbox">を作成する 同時に送信確認のためのhiddenフォームを作成する |
|
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | <input>の各要素を指定。 |
radio ( $fieldName [ , $options [ , $attributes ] ] ) | |
---|---|
<input type="radio">を作成する 同時に送信確認のためのhiddenフォームを作成する |
|
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | ラジオボタンに設定する値を指定。 送信するvalueをキー、表示する文字列を値とした配列で指定する。 |
$attributes['default'] | デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$attributes['value'] | デフォルトで選択される値を指定。 入力値より優先される。 |
$attributes['legend'] | <fieldset>内の<legend>の値を指定する。 falseにすると<fieldset>自体が出力されない。 |
$attributes['separator'] | 各ラジオボタンの間に表示する内容があれば指定。 |
$attributes['label'] | なんかチェックしてるけど意味がないみたい? |
select ( $fieldName [ , $options , [ $selected [ , $attributes [ , $showEmpty ] ] ] ] ) | |
---|---|
<select><option>を作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options | <option>に設定する内容を指定。 送信するvalueをキー、表示する文字列を値とした配列で指定する。 |
$selected | デフォルトで選択される値を指定。 入力値より優先される。 $attributes['multiple']を有効化した場合は配列で指定すると複数選択できる。 |
$attributes['default'] | $selected=nullの場合のみ有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$attributes['value'] | $selected=nullの場合のみ有効。 デフォルトで選択される値を指定。 入力値より優先される。 |
$attributes['multiple'] | trueまたはmultipleにすると<select multiple="multiple" >になる。 'checkbox'にするとチェックボックスになる。 |
$attributes['escape'] | falseにすると$optionsの値をエスケープしない。 |
$attributes['showParents'] | ? |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 input()はデフォルトが非表示だが、こちらはデフォルトで空欄表示。 |
year ( $fieldName [ , $minYear [ , $maxYear [ , $selected [ , $attributes [ , $showEmpty ] ] ] ] ] ) | |
---|---|
年選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['year']" id="ModelName入力値Year">になる。 |
$minYear | 選択可能な最初の年を指定。 デフォルトは現在-20年。 |
$maxYear | 選択可能な最後の年を指定。 デフォルトは現在+20年。 |
$selected | デフォルトで選択される値を指定。 指定は西暦年もしくはstrtotimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
month ( $fieldName [ , $selected [ , $attributes [ , $showEmpty ] ] ] ) | |
---|---|
月選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['month']" id="ModelName入力値Month">になる。 |
$selected | デフォルトで選択される値を指定。 指定は月もしくはstrtotimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$attributes['monthNames'] | デフォルトは英語表記だが、falseにすると数値になる。 任意の値を設定はできない。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
day ( $fieldName [ , $selected [ , $attributes [ , $showEmpty ] ] ] ) | |
---|---|
日付選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['day']" id="ModelName入力値Day">になる。 |
$selected | デフォルトで選択される値を指定。 指定は日付もしくはstrtotimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
hour ( $fieldName [ , $format24Hours [ , $selected [ , $attributes [ , $showEmpty ] ] ] ] ) | |
---|---|
時間選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['hour']" id="ModelName入力値Hour">になる。 |
$format24Hours | trueにすると24時間制になる。 デフォルトは12時間制。 |
$selected | デフォルトで選択される値を指定。 指定は時間もしくはstrtotimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
minute ( $fieldName [ , $selected [ , $attributes [ , $showEmpty ] ] ] ) | |
---|---|
分選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['minute']" id="ModelName入力値Minute">になる。 |
$selected | デフォルトで選択される値を指定。 指定は分もしくはstrtotimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['interval'] | 表示する間隔を指定。 デフォルトは毎分。 開始は0分で変更不能。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
meridian ( $fieldName [ , $selected [ , $attributes [ , $showEmpty ] ] ] ) | |
---|---|
AM/PM選択フォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <select name="data[ModelName][入力値]['minute']" id="ModelName入力値Minute">になる。 |
$selected | デフォルトで選択される値を指定。 指定は'am','pm'。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['default'] $attributes['value'] |
$selected=nullの場合に有効。 デフォルトで選択される値を指定。 入力値が存在する場合はそちらが優先される。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
dataTime ( $fieldName [ , $dateFormat [ , $timeFormat [ , $selected [ , $attributes [ , $showEmpty ] ] ] ] ] ) | |
---|---|
日時選択フォームを作成する。 上記year(),month(),day(),hour(),minute(),meridian()を纏めて作成する。 |
|
引数 | 解説 |
$fieldName | フィールド名。 |
$dateFormat | 年月日の表示内容、表示順番をYMDで指定。 |
$timeFormat | 'NONE'で非表示になる。 24で24時間制になり、AM/PM選択フォームが消える。 デフォルトは12時間制。 |
$selected | デフォルトで選択される値を指定。 指定はstrftimeの解釈できる値。 入力値より優先される。 nullだと初期値無しで、入力値を優先する。 |
$attributes['id'] | <select id>の値を"ModelNamefieldNameYear"から"idYear"等に変更する。 |
$attributes['separator'] | 年/月/日の選択肢の間に表示する文字列を指定。 個別の設定はできない。 |
$attributes['monthNames'] | デフォルトは英語表記だが、falseにすると数値になる。 任意の値を設定はできない。 |
$options['minYear'] $options['maxYear'] |
年の選択肢に表示する最大値、最小値を指定。 |
$attributes['minuteInterval'] $attributes['interval'] |
分の表示間隔を指定。 minuteIntervalが優先。 |
$attributes['default'] $attributes['value'] |
バグる。 |
$showEmpty | <option>の最上段に表示される値を指定。 falseにすると選択肢自体が消える。 |
inputs ( [ $fields [ , $blacklist ] ] ) | |
---|---|
モデルに沿った入力フォームを作成する | |
引数 | 解説 |
$fields | 表示するフィールドの配列。 省略またはnullの時は全カラムを表示。 |
$fields['fieldset'] | 指定されていれば<fieldset>のclass属性に設定される。 |
$fields['legend'] | 指定されていれば<legend>に表示される。 |
$blacklist | 指定したフィールドを表示しない。 $fieldsとどう違うのかはよくわからない。 |
file ( $fieldName [ , $options ] ) | |
---|---|
ファイルのアップロードフォームを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 <input name="data[ModelName][入力値]" id="ModelName入力値">になる。 |
$options['secure'] | ? |
label ( [ $fieldName [ , $text [ , $attributes ] ] ] ) | |
---|---|
ラベルを作成する | |
引数 | 解説 |
$fieldName | フィールド名。 省略またはnullの時は最後に作成したフォームに対するラベルが作成される。 |
$text | ラベルを貼るテキスト。 |
$attributes | 特になし |
isFieldError ( $field ) | |
---|---|
フィールドにバリデーションエラーが存在するかチェックする | |
引数 | 解説 |
$field | フィールド名。 |
error ( $field [ , $text [ , $options ] ] ) | |
---|---|
フィールドのバリデーションエラーの内容を表示する | |
引数 | 解説 |
$field | フィールド名。 |
$text | バリデーションエラーがある場合に表示するエラーメッセージを変更する。 falseだとデフォルトになる。 空白や未出力にはできない。 |
$options['wrap'] | エラーメッセージは通常<div>で囲まれるが、タグを変更したい場合に指定。 falseにするとタグを使用しない。 |
$options['escape'] | falseにするとエラーメッセージをエスケープしない。 |
$optionsまたは$attributesとして特に設定のない配列が来た場合、多くのメソッドにおいて<form>や<input>の属性にそのまま出力される。逆に'name'や'id'等を指定すると、デフォルトの値が破壊されてしまうので注意。
$selectedだと常時入力値より優先されてしまうので、デフォルト値を与えつつ入力値を保存するには$options['default']を使わないといけないとか、設計思想がよくわからない。
あと気になったものとして、__generateOptions('day')には日付の開始日、終了日を変更できるようなことが書いてあるのに、呼び出し側のday()が対応してないので2月でも常に1~31日が表示されてしまうとか、text()やpassword()の$optionsに'type'=>'hidden'とかを無理矢理上書きするとおかしなことになるとか、<option>に$selected=trueとかすると全ての<option>にselected="selected"がくっつくとか、なんか全体的に微妙な出来。
CakePHPの記事一覧
Formヘルパーというものがあるので検索フォームを簡単に作成できます。
cake_sample/formに作成してみます。
formアクションはとりあえず何もしない。
app/controllers/cake_sample_controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/*
CakeSampleコントローラ
*/
class CakeSampleController extends AppController{
//自分の名前
var $name = 'CakeSample';
//CakeSample/form
public function form() {
//入れる
$this->set('cake_sample', $this->paginate('CakeSample'));
}
}
|
ビューは$formを適当に呼び出すだけで簡単に入力欄ができあがります。
/app/views/cake_sample/form.ctp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//フォームオプション
$options=array();
$options['action']='form';
$options['type']='text';
//フォーム作成
$a=array();
$a[]=$form->create($options);
$a[]=$form->input('id',$options);
$a[]=$form->input('title',$options);
$a[]=$form->end('検索');
foreach($a as $val){
print($val);
}
//検索結果表示
if(isset($cake_sample)){
var_dump($cake_sample);
}
|
注意点として、初期状態だと新規登録を行うことに特化されたフォームになります。
フォームの名前としてCakeSampleモデル、即ちcake_samplesテーブルにあるカラム名を使うと、勝手にそこからデータ型を取得し、それに沿ったタグを出力します。
たとえばcake_samples.createdはdatetimeとして作成したので、年月日時分を選択させる<select>で表示されます。
主キーであるcake_samples.idなど、デフォルトではhiddenになってしまいます。
検索を行いたい場合こういった機能は邪魔なので、第二引数のオプションで出力形式を規定しなければなりません。
$options['type']で、形式を指定することができます。
また、$options['action']で、送信先のアクション名を指定しています。
デフォルトの送信先アクションは'add'になっているので、formアクションに戻ってくるようにします。
$formを使用すると各タグの名前が、
<input name="data[CakeSample][hoge]" />
といった変な値になってしまいます。
なんか気持ち悪いですが、コントローラ側で$this->dataで一括して受け取ることができます。
というわけで、その受け取った値を利用して検索する処理をformアクションに追加。
/app/controllers/cake_sample_controller.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
/*
CakeSampleコントローラ
*/
class CakeSampleController extends AppController{
//自分の名前
var $name = 'CakeSample';
//CakeSample/form
public function form() {
//引数があれば検索条件に入れる
$params=array();
if($this->data['CakeSample']['id']){
$params['conditions']['CakeSample.id']
=$this->data['CakeSample']['id'];
}
if($this->data['CakeSample']['title']){
$params['conditions']['CakeSample.title LIKE']
= '%'.$this->data['CakeSample']['title'].'%';
}
//念のためlimit
$params['limit']=10;
//検索
$ret=$this->CakeSample->find('all',$params);
//表示
$this->set('cake_sample', $ret);
}
}
|
これでidをイコール、titleをLIKEで検索できるようになりました。
ただ、せっかく作ったはいいのですが検索条件作成のあたりがどうにもCakePHPの作法に則ってないように見えてしょうがないんだがまあいいか。
さて、検索フォームができたのはいいのですがここで不思議現象が。
画面遷移後に入力値が保存されています。
えっと……なんで?
見ての通りコントローラでもテンプレートでも何もしていません。
激しく気持ち悪い。
CakePHP1.2.4.8284のページャーの全メソッドと引数を解説。
公式マニュアルには$paginatorの解説が全く載っていません。
マニュアルでは「$paginator->numbersで前後10件ずつ表示したい」「1ページ目にリンクしたい」すら解決できません。
他に解説しているところもほとんど見あたりませんでした。
仕方ないのでソースから解読。
よくわかんないところは想像で適当に補っています。
特に$optionsの中身とかはさっぱりわからないので間違っている可能性大。
first ( [ $first [ , $options ] ] ) | |
---|---|
1ページ目へのリンクを作成 | |
引数 | 解説 |
$first | リンクに表示する文字 |
$options | 要素はtag,after,model,separator,url,update |
last ( [ $last [ , $options ] ] ) | |
---|---|
最終ページへのリンクを作成 | |
引数 | 解説 |
$last | リンクに表示する文字 |
$options | 要素はtag,before,model,separator,url,update |
prev ( [ $title [ , $options [ , $disabledTitle [ , $disabledOptions ] ] ] ] ) | |
---|---|
前のページにリンク | |
引数 | 解説 |
$title | リンクに表示する文字 |
$options | 要素はurl,step,escape,model,tag,model,update ただしtagはバグで?効かない |
$disabledTitle | 前のページが存在しない場合に表示する文字列 |
$disabledOptions | 前のページが存在しない場合に適用するオプション 要素は$optionsと同じ。 こちらはtag有効。 |
next ( [ $title [ , $options [ , $disabledTitle [ , $disabledOptions ] ] ] ] ) | |
---|---|
次のページにリンク | |
引数 | 解説 |
$title | リンクに表示する文字 |
$options | 要素はurl,step,escape,model,tag,model,update ただしtagはバグで?効かない |
$disabledTitle | 次のページが存在しない場合に表示する文字列 |
$disabledOptions | 次のページが存在しない場合に適用するオプション 要素は$optionsと同じ。 こちらはtag有効。 |
numbers ( [ $options ] ) | |
---|---|
前後数ページへのリンクを表示 | |
引数 | 解説 |
$options | 要素はtag,before,after,first,last,separator,model,modulus,url,update またはtrue(before,after,first,lastを指定したことになる) |
sort ( $title [ , $key [ , $options ] ] ) | |
---|---|
ソート | |
引数 | 解説 |
$title | リンクに表示する文字列。 ただし$keyが無い場合、$titleが$keyに入れられ、$titleはキャメルケースされて表示される。 |
$key | ソートするカラム名。 |
$options | 要素はurl,model,update |
url ( [ $options [ , $asArray [ , [ $model ] ] ] ) | |
---|---|
指定されたURLを返す | |
引数 | 解説 |
$options | 要素はpage,sort,direction,order。 デフォルトは現在のURL。 |
$asArray | 返り値を配列として返すか否かのフラグ。 falseの場合、そのまま完全なURLが返ってくる。 trueの場合、link()の第二引数として使用する配列形式で返す。 |
$model | 別のモデルを使用する場合に指定。 |
link ( $title [ , $url [ , $options ] ] ) | |
---|---|
任意のリンクを作成して返す | |
引数 | 解説 |
$title | リンクに表示する文字列。 |
$url | URLに出力するキーと値の配列。 現在のURLの値とマージされる。 |
$options | 要素はmodel,url,update |
使用例 | $paginator->link('リンク',array('a'=>'b')); → 現在のアドレスに/a:b/がプラスされたURLへのリンクが作成される。 |
currrent ( [ $model ] ) | |
---|---|
現在のページ数を返す | |
引数 | 解説 |
$model | デフォルト以外のモデルをチェックしたい場合に指定。 |
params ( [ $model ] ) | |
---|---|
ページャーの関連パラメータを取得 | |
引数 | 解説 |
$model | デフォルト以外のモデルをチェックしたい場合に指定。 |
options ( [ $options ] ) | |
---|---|
ページャーの関連パラメータを設定 | |
引数 | 解説 |
$options | params()で参照できる情報を上書きできる。 モデル名から含めた配列で指定が必要。 ビューからでは指定できない内容が多い。 |
使用例 | $paginator->options(array('Hoge'=>array('page'=>10))) → 現在のページを10ページということにする |
counter ( [ $options ] ) | |
---|---|
現在の件数やページ数などを返す | |
引数 | 解説 |
$options | 要素はmodel,format,separator ただし引数を文字列で与えた場合、$options['format']として扱われる。 |
$options['format'] | $options['format']は特殊なので此処で解説。 無指定、'pages'の場合、「3 of 50」(50ページ中3ページ目の意味) 'range'の場合、「41 - 60 of 999」(999件中41-60件目の意味) それ以外の場合、入力した中から以下の文字列が置換されて返ってきます。 %page%→現在のページ数 %pages%→全ページ数 %current%→1ページあたりの件数 %count%→全件数 %start%→現在のページの開始件数 %end%→現在のページの終了件数 |
使用例 | $paginator->counter(array('format'=>'range')) → 「41 - 60 of 999」 $paginator->counter('全%pages%ページ中%page%ページ目を表示') → 「全50ページ中10ページ目を表示」 |
hasPage ( $model , [ $page ] ) | |
---|---|
該当のページが存在するかチェック | |
引数 | 解説 |
$model | ページが存在するかチェックしたいモデル名。 ただし数値を入れた場合、ここが第二引数の$page扱いになる。 |
$page | チェックしたいページ数。 |
使用例 | $paginator->hasPage('Hoge',10) → Hogeモデルのページャに10ページ目が存在するかチェック $paginator->hasPage(10) → 現在のモデルのページャに10ページ目が存在するかチェック |
hasPrev ( [ $model ] ) | |
---|---|
前のページが存在するかチェック | |
引数 | 解説 |
$model | チェックしたいモデル名。 デフォルトは現在のモデル。 |
hasNext ( [ $model ] ) | |
---|---|
次のページが存在するかチェック | |
引数 | 解説 |
$model | チェックしたいモデル名。 デフォルトは現在のモデル。 |
defaultModel ( ) | |
---|---|
デフォルトのモデル名を取得 | |
引数 | 解説 |
なし | 基本的に内部用 |
sortKey ( [ $model [ , $options ] ] ) | |
---|---|
ソートのORDER BYの値を取得。passedArgs['sort']に近い | |
引数 | 解説 |
$model | デフォルト以外のモデルを使用する場合に指定。 |
$options | sort,order,defaults |
sortDir ( [ $model [ , $options ] ] ) | |
---|---|
ソートの順番(ASC,DESC)を取得。passedArgs['direction']に近い | |
引数 | 解説 |
$model | デフォルト以外のモデルを使用する場合に指定。 |
$options | order,defaults,direction |
要素 | 解説 |
---|---|
tag | ページャーは通常<span>で括られるが、それを変更する。 |
separator | リンクが複数出力される場合、間を分けるセパレータ。 |
before | リンクの前に入れる文字列。 |
after | リンクの後ろに入れる文字列。 |
first | 先頭へのリンクを表す文字列。 数値を入れた場合、一件目から該当件数を表示する。 |
last | 最後へのリンクを表す文字列。 数値を入れた場合、後ろから該当件数を表示する。 |
model | デフォルトのモデルではなく、別のモデルのページャを使用したい場合にモデル名を入れる。 |
modulus | 前後を合わせて何ページ表示するか。 前3ページ、後6ページといった指定は出来ない。 |
url | 配列で指定。 リンクのアドレスに指定した値が追加されます。 |
step | 前後step数先のページへのリンクを作成する。 |
update | デフォルトは'Html'で、取れる値は'Html'か'Ajax'。 どちらの形式のリンクを作成するか。 |
sort,order,defaults,direction | ? '/(?:\w+\.)?(\w+)/'って何だ? |
で、見ていて色々とバグっぽい挙動を見つけました。
$paginator->first(5)みたいに数値を渡すと、2~5では与えた文字に1ページ目へのリンクとなり、6以降を渡すと$options['first']=5を渡したのと同じ挙動になります。
$options['params']を$paginator->numbers()に渡すとページャが死んじゃいますし、$options['options']を$paginator->first()や$paginator->last()に渡すとやっぱり死にます。
一部のメソッド内で$optionsをectract()してるので、おかしくなるのは大抵そのせいです。
このectract()って全く意味がない使い方に見えるんだが、何故こんなことしているのだろう?
あと基本的に括りは<span>ですが、何故かfirst()とlast()だけ<div>なのは何故。
つうか、そもそも$paginatorって使いにくいよね。
未だPEAR::Pagerに勝るものはないと思う。
CakePHPの記事一覧
びっくりするほど使い辛いなDoctrine。
とりあえず
SELECT a.id AS id ,( select count(*) from b where b.a_id=a.id group by a.id ) AS count FROM a
の発行方法がわからない。
確かに簡単なSELECTやINSERTなんかはDoctrineを使えば一瞬でできるので便利といえば便利なのですが、リレーションのついてないテーブルをくっつけたり変に集約したサブクエリみたいな入り組んだものを作ったりしようとすると途端に訳が解らなくなります。
まあドキュメントをほじくり返せばこういったDQLを作成する方法も何処かに書いてあったりするようですが、よくわからん。
というわけでDoctrineで任意のSQLを発行する方法です。
getconnection.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//DBコネクション
$q=Doctrine::getConnectionByTableName('a');
//任意のSQL
$sql='SHOW TABLES';
//実行
$statement=$q->execute($sql);
//取得
$ret=$statement->fetchAll(PDO::FETCH_ASSOC);
|
PropelにはPropel::getConnection()という直接コネクションを取得できるメソッドがあるのですが、Doctrineには何故か存在しないようです。
Doctrine::getConnectionByTableName()は一見ATable extends Doctrine_Tableあたりを通じてコネクションを取得するメソッドのように見えますが、実際には引数を使っていないようで、適当な文字列を入れるだけでコネクションが取得できたりします。
コネクションに使用されるモジュールは環境によって変わると思いますが、PDOが入っていればPDOになると思いますので、あとは普通にPDOのメソッドを実行するだけです。
まあ掟破りな方法かもしれませんが、Doctrineが訳の解らない作りになっているのが悪い。
Symfonyの記事一覧