学習日記60日目

スタートアップ研修記はこちらです。

どうも、enomotoです。
気づけば、今週も金曜日。最近、毎日が早いです。
ということで、本日のaskeet復習日記です。

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('&laquo;','question/list?page=1') ?>
   <?php echo link_to('&lt;','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('&gt;','question/list?page='.$question_pager->getNextPage()) ?>
   <?php echo link_to('&raquo;','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('&laquo;','question/list?page=1') ?>
   <?php echo link_to('&lt;','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('&gt;','question/list?page='.$question_pager->getNextPage()) ?>
   <?php echo link_to('&raquo;','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>