review askeet Day 5
ログインフォームを作る
まずは、フォームへのリンクを作る。
場所: askeet2/apps/frontend/templates/layout.php
<li><?php echo link_to('サインイン','user/login') ?></li>
投稿は日本語が受け付けられませんが、表示類は極力日本語にしてみます。
Userモジュールを作る
symfony init-module frontend user
これでaskeet2/apps/frontend/modules/userができます。
モジュールスケルトンというらしいです。
要らないモノもできちゃうので削除します。
svn rm apps/frontend/modules/user/templates/indexSuccess.php
次に、アクションを書きます。
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
<?php /** * user actions. * * @package askeet * @subpackage user * @author Your name here * @version SVN: $Id: actions.class.php 2692 2006-11-15 21:03:55Z fabien $ */ class userActions extends sfActions { public function executeLogin() { $this->getRequest()->setAttribute('referer',$this->getRequest()->getReferer()); return sfView::SUCCESS; } }
ついでにexecuteIndexは削除しておく。
ログインフォームのテンプレートを作る。
場所: askeet2/apps/frontend/modules/user/templates/loginSuccess.php
<?php echo form_tag('user/login') ?> <fieldset> <div class="form-row"> <label for="nickname">ニックネーム:</label> <?php echo input_tag('nickname',$sf_params->get('nickname')) ?> </div> <div class="form-row"> <label for="password">パスワード:</label> <?php echo input_password_tag('password') ?> </div> </fieldset> <?php echo input_hidden_tag('referer',$sf_request->getAttribute('referer')) ?> <?php echo submit_tag('サインイン') ?> </form>
アクションを投稿できるように書き直す。
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
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が合っているか? 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')); } } } }
ここで押さえておきたい。
種類 | 例 | 違い |
---|---|---|
request属性 | $this->getRequest()->setAttribute() | データを所持するのはリファラーに送信するまで |
session属性 | $this->getUser()->setAttribute() | セッションとして所持される(他のアクションが使える) |
参考:http://symfony.xrea.jp/1.2/book/02-Exploring-Symfony-s-Code.html#parameter.holders
ログアウトを作る
ログインを作ったらログアウトも作らなきゃということで作ります。
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
public function executeLogout() { $this->getUser()->setAuthenticated(false); $this->getUser()->clearCredentials(); // subscriberを削除 $this->getUser()->getAttributeHolder()->removeNamespace('subscriber'); // トップページにリダイレクト $this->redirect('@homepage'); }
ログイン後とログイン前のメニュー部分を変える
現状だとログイン後もログインメニューが出てしまっているので
ログイン後はログアウトとか出るようにする。
場所: askeet2/apps/frontend/modules/user/templates/layout.php
<div id="header"> <ul> <?php if($sf_user->isAuthenticated()): ?> <li><?php echo link_to('サインアウト','user/logout') ?></li> <li><?php echo link_to($sf_user->getAttribute('nickname','','subscriber').'のプロフィール','user/profile') ?></li> <?php else: ?> <li><?php echo link_to('サインイン/ユーザー登録','user/login') ?></li> <?php endif ?> <li><?php echo link_to('トップページ','@homepage') ?></li> </ul> <h1><?php echo link_to(image_tag('askeet_logo.gif','alt=askeet'),'@homepage') ?></h1> </div>
▼ログイン前▼
▼ログイン後▼
$sf_user->isAuthenticatedにデータを抱えているときはログイン状態ということですね。
なるほど。
ページャーを作る
表示件数が多すぎると見づらいのでページャを付けようって話ですね。
まずはアクションを書く。
場所: askeet2/apps/frontend/modules/question/actions/actions.class.php
public function executeList() { // ページャー $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max')); $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; }
カスタムパラメータを使用する
sfConfig::get('app_pager_homepage_max')は
場所: askeet2/apps/frontend/config/app.ymlで設定できます。
app.ymlのpagerのhomepage_maxを呼び出している。なるほど。
# default values all: pager: homepage_max: 2
テンプレートを修正する
あとは表示部を変えるだけですね。
場所: askeet2/apps/frontend/modules/question/templates/listSuccess.php
<?php use_helper('Text') ?> <h1>人気の質問</h1> <?php foreach($question_pager->getResults() as $question): ?> <div class="question"> <div class="interested_block"> <?php include_partial('interested_user',array('question' => $question)) ?> </div> <h2><?php echo link_to($question->getTitle(),'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2> <div class="question_body"> <?php echo truncate_text($question->getBody(),200) ?> </div> </div> <?php endforeach; ?> <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>
ルーティングルール
場所: askeet2/apps/frontend/config/routing.yml
homepage: url: / param: { module: question, action: list } login: url: /login param: { module: user, action: login }
これでアドレスがすっきりした。
リファスタリング
Questionのアクションに書いたページャーに関するコードは
モデルと密接な関係があるのでモデルに移動します。
呼び出す元を書き換える
場所: askeet2/apps/frontend/modules/question/actions/actions.class.php
public function executeList() { $this->question_pager = QuestionPeer::getHomepagePager($this->getRequestParameter('page', 1)); }
呼び出す側を書く
場所: askeet2/lib/model/QuestionPeer.php
<?php /** * Subclass for performing query and update operations on the 'ask_question' table. * * * * @package lib.model */ class QuestionPeer extends BaseQuestionPeer { 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; } }
showにも応用
まずは、呼び出す側を書き換える。
場所: askeet2/apps/frontend/modules/question/actions/actions.class.php
public function executeShow() { $this->question = QuestionPeer::getQuestionFromTitle($this->getRequestParameter('stripped_title')); $this->forward404Unless($this->question); }
呼び出される側に追加する。
場所: askeet2/lib/model/QuestionPeer.php
public static function getQuestionFromTitle($title)
{
$c = new Criteria();
$c->add(QuestionPeer::STRIPPED_TITLE,$title);
return self::doSelectOne($c);
}
テンプレートも簡単に。
呼び出し部分はこんな感じに。
場所: askeet2/apps/question/templates/listSuccess.php
<?php use_helper('Text') ?> <h1>人気の質問</h1> <?php echo include_partial('list',array('question_pager' => $question_pager)) ?>
呼び出される側を書く。
場所: askeet2/apps/question/templates/_list.php
<?php foreach($question_pager->getResults() as $question): ?> <div class="question"> <div class="interested_block"> <?php include_partial('interested_user',array('question' => $question)) ?> </div> <h2><?php echo link_to($question->getTitle(),'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2> <div class="question_body"> <?php echo truncate_text($question->getBody(),200) ?> </div> </div> <?php endforeach; ?> <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>
review askeet Day 6
ログインフォームのバリデーション
YAMLに定義
場所: askeet2/apps/frontend/modules/user/validate/login.yml
methods: post: [nickname, password] names: nickname: required: true required_msg: ニックネームの入力が必要です。 validators: nicknameValidator password: required: true required_msg: パスワードの入力が必要です。 nicknameValidator: class: sfStringValidator param: min: 5 min_error: ニックネームは5文字以上の入力が必要です。
エラーの処理を入れる
まずはアクションの処理
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
public function handleError() { return sfView::ERROR; } public function handleErrorLogin() { return sfView::SUCCESS; }
テンプレートの変更
場所: askeet2/apps/frontend/modules/user/templates/loginSuccess.php
<?php echo form_tag('user/login') ?> <fieldset> <?php use_helper('Validation') ?> <div class="form-row"> <?php echo form_error('nickname') ?> <label for="nickname">ニックネーム:</label> <?php echo input_tag('nickname',$sf_params->get('nickname')) ?> </div> <div class="form-row"> <?php echo form_error('password') ?> <label for="password">パスワード:</label> <?php echo input_password_tag('password') ?> </div> </fieldset> <?php echo input_hidden_tag('referer',$sf_request->getAttribute('referer')) ?> <?php echo submit_tag('サインイン') ?> </form>
ユーザーの認証を付ける
先ほど実装したモノはあくまでも入力の有無を確認するだけで
認証まで行いませんでした。なので、認証機能を付けます。
まずはバリデータを書き直す。
場所: askeet2/apps/frontend/modules/user/validate/login.yml
methods: post: [nickname, password] names: nickname: required: true required_msg: ニックネームの入力が必要です。 validators: [nicknameValidator, userValidator] password: required: true required_msg: パスワードの入力が必要です。 nicknameValidator: class: sfStringValidator param: min: 5 min_error: ニックネームは5文字以上の入力が必要です。 userValidator: class: myLoginValidator param: password: password login_error: このアカウントは存在しないか入力したパスワードが間違っています。
パスワードを保存する
今回はセキュリティ対策も含めて
2重に暗号化します。そして、そのアカウント毎にカギ(salt)を用意します。
そのためDBに二つ記録しておく必要があります。
ということでschema.ymlを書き直す。
場所: askeet2/config/schema.yml
ask_user: _attributes: { phpName: User, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } nickname: { type: varchar(50), required: true, index: true } first_name: varchar(100) last_name: varchar(100) email: varchar(100) sha1_password: varchar(40) salt: varchar(32) created_at: ~
書き直したら
symfony propel-build-model symfony propel-build-sql symfony propel-insert-sql
setPasswordメソッドを追加する
場所: askeet2/lib/model/User.php
public function setPassword($password) { $salt = md5(rand(100000,999999).$this->getNickname().$this->getEmail()); $this->setSalt($salt); $this->setSha1Password(sha1($salt.$password)); }
テストデータにパスワードを追加する
場所: askeet2/data/fixtures/test_data.yml
User: anonymous: nickname: anonymous first_name: Anonymous last_name: Coward crazyup: nickname: crazyup first_name: Shota last_name: Enomoto password: shota dinotaro: nickname: dino first_name: Taro last_name: Dino password: dino
追加したら
php batch/load_data.php
カスタムバリデータ
場所: askeet2/apps/frontend/lib/myLoginValidator.class.php
<?php class myLoginValidator extends sfValidator { public function initialize($context,$parameters = null) { // 親クラスを初期化 parent::initialize($context); // デフォルトを設定 $this->setParameter('login_error','入力が無効です。'); $this->getParameterHolder()->add($parameters); return true; } public function execute(&$value,&$error) { $password_param = $this->getParameter('password'); $password = $this->getContext()->getRequest()->getParameter($password_param); $login = $value; // anonymousはエラーにする if($login == 'anonymous') { $error = $this->getParameter('login_error'); return false; } $c = new Criteria(); $c->add(UserPeer::NICKNAME,$login); $user = UserPeer::doSelectOne($c); // nicknameが存在するか if($user) { // passwordは合っているか? if(sha1($user->getSalt().$password) == $user->getSha1Password()) { $this->getContext()->getUser()->setAuthenticated(true); $this->getContext()->getUser()->addCredential('subscriber'); $this->getContext()->getUser()->setAttribute('subscriber_id',$user->getId(),'subscriber'); $this->getContext()->getUser()->setAttribute('nickname',$user->getNickname().'subscriber'); return true; } } $error = $this->getParameter('login_error'); return false; } }
アクションからカスタムバリデータを呼び出す
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
public function executeLogin() { if($this->getRequest()->getMethod() != sfRequest::POST) { // フォームを表示する $this->getRequest()->setAttribute('referer',$this->getRequest()->getReferer()); } else { // フォーム投稿を取り扱う // 最後のページにリダイレクトする return $this->redirect($this->getRequestParameter('referer','@homepage')); } }
アクセス制限する
質問登録画面が登録ユーザーだけ登録できるようにしたいので
質問登録画面はまだ作っていませんが、アクセス制限だけ書く。
場所: askeet2/apps/frontend/modules/question/config/security.yml
add: is_secure: on credentials: subscriber all: is_secure: off
signInとsignOutをメソッド化する。
myUser.class.phpに書く
場所: askeet2/apps/frontend/lib/myUser.class.php
<?php class myUser extends sfBasicSecurityUser { public function signIn($user) { $this->setAttribute('subscriber_id',$user->getId(),'subscriber'); $this->setAuthenticated(true); $this->addCredential('subscriber'); $this->setAttribute('nickname',$user->getNickname(),'subscriber'); } public function signOut() { $this->getAttributeHolder()->removeNamespace('subscriber'); $this->setAuthenticated(false); $this->clearCredentials(); } }
signIn()に置き換える。
場所: askeet2/apps/frontend/lib/myLoginValidator.class.php
<?php class myLoginValidator extends sfValidator { public function initialize($context,$parameters = null) { // 親クラスを初期化 parent::initialize($context); // デフォルトを設定 $this->setParameter('login_error','入力が無効です。'); $this->getParameterHolder()->add($parameters); return true; } public function execute(&$value,&$error) { $password_param = $this->getParameter('password'); $password = $this->getContext()->getRequest()->getParameter($password_param); $login = $value; // anonymousはエラーにする if($login == 'anonymous') { $error = $this->getParameter('login_error'); return false; } $c = new Criteria(); $c->add(UserPeer::NICKNAME,$login); $user = UserPeer::doSelectOne($c); // nicknameが存在するか if($user) { // passwordは合っているか? if(sha1($user->getSalt().$password) == $user->getSha1Password()) { $this->getContext()->getUser()->signIn($user); return true; } } $error = $this->getParameter('login_error'); return false; } }
signOut()に置き換える
場所: askeet2/apps/frontend/modules/user/actions/actions.class.php
public function executeLogout() { $this->getUser()->signOut(); // トップページにリダイレクト $this->redirect('@homepage'); }
よく使うセッション属性もメソッド化
場所: askeet2/apps/frontend/lib/myUser.class.php
public function getSubscriberId() { return $this->getAttribute('subscriber_id','','subscriber'); } public function getSubscriber() { return UserPeer::retrieveByPk($this->getSubscriberId()); } public function getNickname() { return $this->getAttribute('nickname','','subscriber'); }
使う場所を早速直す
場所: askeet2/apps/frontend/templates/layout.php
<div id="header"> <ul> <?php if($sf_user->isAuthenticated()): ?> <li><?php echo link_to('サインアウト','user/logout') ?></li> <li><?php echo link_to($sf_user->getNickname().'のプロフィール','user/profile') ?></li> <?php else: ?> <li><?php echo link_to('サインイン/ユーザー登録','user/login') ?></li> <?php endif ?> <li><?php echo link_to('トップページ','@homepage') ?></li> </ul> <h1><?php echo link_to(image_tag('askeet_logo.gif','alt=askeet'),'@homepage') ?></h1> </div>