復習
長い4日目において、コードのチャンク(塊)を性質が近い他のファイルに移動させることで、アプリケーションをリファクタリングすることに慣れました。モデルの修正も学んだので、アクションコードからデータに関連する共通メソッドを取り出すことができるようになりました。
開発はクリーンですが、多くの機能はまだ貧弱です。askeetサイトとユーザーの間に少しのインタラクティビティを持たせましょう。HTMLの基本は -ハイパーリンクに加えて -フォームです。
今日の目的はユーザーがログインとホームページの質問リストをページ分割できるようにすることです。開発の進行は速くなりますが、あなたが昨日のことを思い出すのに手助けとなるでしょう。
ログインフォーム
テストデータにユーザーがありますが、アプリケーションが認識する方法がありません。アプリケーションのすべてのページからログインできるようにしましょう。グローバルレイアウトの askeet/appas/frontend/templates/layout.php
を開いて、about
リンクの前に次の行を追加します:
<li><?php echo link_to('sign in', 'user/login') ?></li>
note
現在のレイアウトではこのリンクはWebデバッグツールバーの後ろに設置されています。見えるようにするために、'Sf'アイコンをクリックしてツールバーを折り畳んでください。
user
モジュールを作成しましょう。2日目にquestion
モジュールが生成されましたが、今回はsymfonyにモジュールスケルトンを作成するように頼みコードは私たち自身が書くことにします。
$ symfony init-module frontend user
note
スケルトンはデフォルトのindex
アクションとindexSuccess.php
テンプレートを含みます。必要がないので両方とも削除してください。
user/loginアクションを作成する
user/actions/action.class.php
ファイル(新しいaskeet/apps/frontend/modules/
ディレクトリの下)において、次の login
アクションを追加します:
public function executeLogin() { $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer()); return sfView::SUCCESS; }
アクションはリクエスト属性のリファラーを保存します。非表示フィールドが設置されているテンプレートに利用可能になるので、ログイン成功後にフォームのターゲットアクションがオリジナルのリファラーにリダイレクトできます。
sfView::SUCCESS
が返されるとloginSuccess.php
テンプレートにアクションの結果が渡されます。このステートメントはreturnステートメントを含まないアクションに含まれています。アクションのデフォルトテンプレートが actionnameSuccess.php
という名称になるのはそういうわけです。
アクションに取り組む前に、テンプレートを見てみましょう。
loginSuccess.phpテンプレートを作成する
Web上での人間とコンピュータのやりとりにはフォームが使用され、symfonyはフォームヘルパーのセットを提供することによってフォームの作成と管理を円滑にします。
askeet/apps/frontend/modules/user/templates/
ディレクトリにおいて、次の loginSuccess.php
テンプレートを作成してください:
<?php echo form_tag('user/login') ?> <fieldset> <div class="form-row"> <label for="nickname">nickname:</label> <?php echo input_tag('nickname', $sf_params->get('nickname')) ?> </div> <div class="form-row"> <label for="password">password:</label> <?php echo input_password_tag('password') ?> </div> </fieldset> <?php echo input_hidden_tag('referer', $sf_request->getAttribute('referer')) ?> <?php echo submit_tag('sign in') ?> </form>
このテンプレートはフォームヘルパーの最初の紹介です。これらのsymfonyの機能はフォームタグの書き込みを自動化することを手助けします。form_tag()
ヘルパーはデフォルトのPOSTのふるまいによってフォームを開き、渡されたアクションを指し示します。input_tag()
ヘルパーは最初の引数として渡される名前を基準とするid
属性を追加することで<input>
タグ(驚きでしょう)を自動的に作成します; デフォルトの値は2番目の引数から選択されます。symfony bookの関連する章でフォームヘルパーと生成されたHTMLコードを見ることができます。
ここでの本質的なことは、フォームが投稿されたとき(form_tag()
の引数)に呼び出されるアクションは表示に使用されるlogin
アクションと同じであることです。それではアクションに戻りましょう。
ログインフォーム投稿の取り扱い
先ほど書いたlogin
アクションを次のコードで置き換えてください:
public function executeLogin() { if ($this->getRequest()->getMethod() != sfRequest::POST) { // フォームを表示する $this->getRequest()->setAttribute('referer', $this->getRequest()->getReferer()); } else { // フォーム投稿を処理する $nickname = $this->getRequestParameter('nickname'); $c = new Criteria(); $c->add(UserPeer::NICKNAME, $nickname); $user = UserPeer::doSelectOne($c); // nicknameが存在するか? if ($user) { // passwordがOKか? if (true) { $this->getUser()->setAuthenticated(true); $this->getUser()->addCredential('subscriber'); $this->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber'); $this->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber'); // 最後のページにリダイレクトする return $this->redirect($this->getRequestParameter('referer', '@homepage')); } } } }
ログインアクションはログインフォームの表示と処理の両方に使われます。結果として、どちらのコンテキストで呼び出されたのかを知る必要があります。アクションがPOSTモードで呼び出されていないのであれば、リンクからリクエストされたからです。先ほど私たちが話した前者のケースです。リクエストがPOSTモードであるならば、アクションはフォームから呼び出され、操作する段階にあります。
アクションはリクエストパラメータからnickname
フィールドの値を得て、データベースにこのユーザーが存在するか User
テーブルをリクエストします。
それから、近い将来に、ユーザーに認証を与えるパスワードの制御機能が存在するようになります。今の時点で、このアクションが唯一行うことはセッション属性にid
とユーザーのnickname
を保存することです。結局のところ、アクションはリクエストパラメータとして渡された隠しreferer
フィールドのおかげでオリジナルリファラーにリダイレクトします。このフィールドが空の場合、デフォルトの値が代わりに使われます(@homepage
はquestion/list
のためのルーティングルールの名前です)。
この例における2種類の属性セットの違いに注目してください。request属性 ($this->getRequest()->setAttribute()
) はテンプレートのために保有され、回答がリファラーに送信されると同時に忘れます。session属性 ($this->getUser()->setAttribute()
)はユーザーセッションの寿命の間に保有され、将来、他のアクションはアクセスできるようになります。属性についてもっと知りたいのでしたら、symfony bookのパラメータホルダの章をご覧ください。
特権を与える
ユーザーがaskeetのWebサイトにログインすることができるのはよいことです。しかし、それだけではユーザーは面白くないです。ログインは新しい質問を投稿すること、質問に対する関心を示すこと、コメントを評価することが求められます。すべての他のアクションは記録されていないユーザーに対しても開かれています。
認証されたユーザーとして設定するためにはsfUser
オブジェクトの->setAuthenticated()
メソッドを呼び出す必要があります。このオブジェクトはプロファイルに従って洗練されたアクセス制限をするためにクレデンシャルメカニズム(->addCredential())
も提供します。symfony bookのユーザークレデンシャルの章で詳細にこのことが説明されています。
2行のコードの目的はそういうわけです:
$this->getContext()->getUser()->setAuthenticated(true); $this->getContext()->getUser()->addCredential('subscriber');
nicknameが承認されたとき、session属性に格納されたユーザーのデータだけでなくユーザーもサイトの制限された部分へのアクセス権が与えられます。明日は認証されたユーザーへのアプリケーションの部分のアクセスを制限をどのようにするのか見ます。
user/logoutアクションを追加する
->setAttribute()
メソッドについて最後のトリックです。最後の引数(上の例の subscriber
)は属性が保存される名前空間を定義します。名前空間は属性に任意の他の名前空間で既存の名前を許可するだけでなく1つのコマンドですべての属性をすぐに削除できます:
public function executeLogout() { $this->getUser()->setAuthenticated(false); $this->getUser()->clearCredentials(); $this->getUser()->getAttributeHolder()->removeNamespace('subscriber'); $this->redirect('@homepage'); }
名前空間を使うことで2つの属性を1つ1つ削除せずに済みます。一行以下のコードです。怠けるとはまさにこのことです!
レイアウトをアップデートする
レイアウトはユーザーがすでにログインしていても'login'リンクを表示したままです。すぐに直しましょう。askeet/apps/frontend/templates/layout.php
において、今日のチュートリアルの始めに追加した行を変更してください:
<?php if ($sf_user->isAuthenticated()): ?> <li><?php echo link_to('sign out', 'user/logout') ?></li> <li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li> <?php else: ?> <li><?php echo link_to('sign in/register', 'user/login') ?></li> <?php endif ?>
アプリケーションのページを表示し、'login'をクリックし、有効なニックネーム("anonymous"はトリックを行います)を入力し、バリデートすることで、すべてのテストする段階に来ました。ウィンドウのトップ上の'login'リンクが'sign out'に変わったら、作業内容はすべて正しいです。最終的には'login'リンクが再表示されるか確認するためにログアウトを試してください。
symfony bookのユーザーセッションの章 においてユーザーセッション属性の操作の情報をより多く見ることができます。
質問ページャ
大勢のsymfonyの熱狂的なユーザーがaskeetサイトに押し寄せます。ホームページに表示された質問リストがとても長く成長する可能性が非常に大きいです。遅いリクエストと過剰なスクロールを避けるために、質問リストをページ分割することが必要です。
symfonyはそのためのオブジェクト:sfPropelPager
を提供します。現在のページを表示するためのレコードだけがリクエストされるようにデータベースへのリクエストをカプセル化します。たとえば、10レコードを表示するためにページャが初期化されたとしたら、データベースへのリクエストの結果は10まで制限され、オフセットがページランクにマッチするように設定されます。
question/listアクションを修正する
3日目において、私たちはquestion
モジュールのlist
アクションがきわめて簡潔であることを見ました:
public function executeList () { $this->questions = QuestionPeer::doSelect(new Criteria()); }
配列の代わりにsfPropelPager
オブジェクトをテンプレートに渡すためにこのアクションを修正することにします。同時に、私たちは関心を寄せる人数によって質問を並べ替えることに取り組みます:
public function executeList () { $pager = new sfPropelPager('Question', 2); $c = new Criteria(); $c->addDescendingOrderByColumn(QuestionPeer::INTERESTED_USERS); $pager->setCriteria($c); $pager->setPage($this->getRequestParameter('page', 1)); $pager->setPeerMethod('doSelectJoinUser'); $pager->init(); $this->question_pager = $pager; }
sfPropelPager
オブジェクトの初期化はどのオブジェクトのクラスが含むか、ページに追加されるオブジェクトの最大数(この例では2)かを指定します。->setPage()
メソッドは現在のページを設定するリクエストパラメータを使用します。たとえば、このpage
パラメータが2
の値を持つとき、sfPropelPager
は3から5の結果を返します。デフォルトのpage
リクエストパラメータの値は1
で、このページャはデフォルトで1から2の値を返します。sfPropelPager
オブジェクトとそのメソッドに関するより詳しい情報はsymfony bookのページャの章で見ることができます。
カスタムパラメータを使用する
設定ファイルに定数を使うことは常によいアイディアです。たとえば、ページごとの結果数(この例では2
)は独自のアプリケーションの設定パラメータによって置き換えられる可能性があります。new sfPropelPager
の上記の行を変更してください:
... $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
アプリケーションのカスタム設定ファイル (askeet/apps/frontend/config/app.yml
)を開き、次のコードを追加してください:
all: pager: homepage_max: 2
ここでのpager
キーは名前空間として使われます。パラメータネームにおいても現れる理由です。symfony bookの設定の章 において、カスタム設定とカスタムパラメータの命名ルールについて詳しい情報があります。
listSuccess.phpテンプレートを修正する
listSuccess.php
において、ページャに保存された結果のリストをページが表示できるように
<?php foreach($questions as $question): ?>
を
<?php foreach($question_pager->getResults() as $question): ?>
に置き換えてください。
ページのナビゲーションを追加する
このテンプレートを追加するために少しの作業があります。それらはページナビゲーションです。今現在、テンプレートが行うことは最初の2つの質問を表示することですが、次のページへ移動する機能および前のページに移動する機能を追加した方がよいでしょう:
<div id="question_pager"> <?php if ($question_pager->haveToPaginate()): ?> <?php echo link_to('«', 'question/list?page=1') ?> <?php echo link_to('<', 'question/list?page='.$question_pager->getPreviousPage()) ?> <?php foreach ($question_pager->getLinks() as $page): ?> <?php echo link_to_unless($page == $question_pager->getPage(), $page, 'question/list?page='.$page) ?> <?php echo ($page != $question_pager->getCurrentMaxLink()) ? '-' : '' ?> <?php endforeach; ?> <?php echo link_to('>', 'question/list?page='.$question_pager->getNextPage()) ?> <?php echo link_to('»', 'question/list?page='.$question_pager->getLastPage()) ?> <?php endif; ?> </div>
このコードはsfPropelPager
オブジェクトのさまざまなメソッドを利用します。->haveToPaginate()
はリクエストへの結果の数字がページサイズを超えるときのみ true
を返します; ->getPreviousPage()
、->getNextPage()
と ->getLastPage()
は明らかな意味を持ちます; ->getLinks()
はページ番号の配列を提供します; そして->getCurrentMaxLink()
は最後のページ番号を返します。
この例は1つの手軽なsymfonyのリンクヘルパーも示します: 最初の引数がfalse
である場合にテストが与えられたのであれば、link_to_unless()
は通常のlink_to()
を出力し、そうではなければ、シンプルな<span>
で囲まれたリンク無しでテキストが出力されます。
ページャをテストしましたか? した方がいいでしょう。修正はあなた自身の目で検証しない限り終わりません。そのためには、3日目に作成されたテストデータファイルを開き、表示されるページナビゲーションのために新しい質問を追加してください。データインポートバッチを起動させ、ホームページを再びリクエストしてください。ほら。
次のページのルーティング・ルールを追加する
デフォルトでは、ページのURLは次のようになります:
http://askeet/frontend_dev.php/question/list/page/XX
これらのページを理解させるためにルーティングルールを利用しましょう:
http://askeet/frontend_dev.php/index/XX
apps/frontend/config/routing.yml
ファイルを開き、一番上に次のコードを追加してください:
popular_questions: url: /index/:page param: { module: question, action: list }
私たちがそれを目指している間に、ログインページのための他のルーティングルールを追加してください:
login: url: /login param: { module: user, action: login }
リファクタリング
モデル
question/list
アクションはモデルと緊密な関係にあるコードを実行します。このコードをモデルに移動させるのはそういうわけです。question/list
アクションを置き換えてください:
public function executeList () { $this->question_pager = QuestionPeer::getHomepagePager($this->getRequestParameter('page', 1)); }
...lib/model
のQuestionPeer.php
クラスに次のメソッドを追加してください:
public static function getHomepagePager($page) { $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max')); $c = new Criteria(); $c->addDescendingOrderByColumn(self::INTERESTED_USERS); $pager->setCriteria($c); $pager->setPage($page); $pager->setPeerMethod('doSelectJoinUser'); $pager->init(); return $pager; }
昨日書いたquestion/show
アクションに同じアイディアを応用します。特殊文字が剥ぎ取られたタイトルから質問を読み取るためにPropelオブジェクトをモデルに所属させます。そこでquestion/show
アクションを変更してください:
public function executeShow() { $this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title')); $this->forward404Unless($this->question); }
QuestionPeer.php
に追加してください:
public static function getQuestionFromTitle($title) { $c = new Criteria(); $c->add(QuestionPeer::STRIPPED_TITLE, $title); return self::doSelectOne($c); }
テンプレート
question/templates/listSuccess.php
に表示された質問リストはそのうちどこかで再利用されます。そこで、質問リストを表示するためにテンプレートコードを _list.php
フラグメントに加え、listSuccess.php
を単純に置き換えます:
<h1>popular questions</h1> <?php echo include_partial('list', array('question_pager' => $question_pager)) ?>
_list.php
フラグメントの内容はaskeetのSVNリポジトリで見ることができます。
それではまた明日
ログインフォームとリストページャは最近のほとんどすべてのWebアプリケーションで使用されています。 それらをsymfonyできわめて簡単に開発できることを今日理解しました。
今日はもう一度リファクタリングで終わりました。最初に大きな図面をデザインすることなく、少しずつあなたのアプリケーションを構築するために支払う代償です。
明日は、登録ユーザーがサイトの一部にアクセスする権限を制限するなど、ログインプロセスに取り組み続けます。また不正な投稿を避けるためにフォームのバリデーションを行います。
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.