Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

さくらvpsとcakephp2.6.7で開発日記 その0002 会員登録フォームの作成をする

参考にさせて頂いたページ

ストレージエンジンについて

MySQLの「InnoDB」と「MyISAM」についての易しめな違い - Programming log - Shindo200

バリデーションについて

vagrantその19-41 cakephp入門をやってみる(オリジナル・バリデーションを定義する) - MOTOMICHI WORKS BLOG

データバリデーション — CakePHP Cookbook 2.x ドキュメント

よくある同一内容を入力させる時のバリデーションチェック - cakephperの日記(CakePHP, Laravel, PHP)

http://oneday.ter.jp/php/cakephp-php/927.html

ベースURLの取得

パス定数と変更方法やURLの取得
http://kwski.net/cakephp-2-x/1228/

Modelのファイル内でテーブルデータを取得する

データを取得する — CakePHP Cookbook 2.x ドキュメント

テーブルデータを取得するときのrecursiveプロパティについて

モデルの属性 — CakePHP Cookbook 2.x ドキュメント

CakePHP 2.x - recursive設定によるfind()性能改善

Modelで設定されている$validateのうち、一部だけをコントローラーで使用する

コントローラーからのバリデーション — CakePHP Cookbook 2.x ドキュメント

saveの第二引数と第三引数について

CakePHP:save()メソッドを使用し、データの登録・更新 | raining

CakePHP Model#save()内でvalidates()を呼ばない - Shin x blog

Model::createの効果と使用するかしないかの判断について

データを保存する — CakePHP Cookbook 2.x ドキュメント

【CakePHP】Model::create() の使い方と注意点 | バシャログ。 | 横浜でWeb制作を行うシーブレインスタッフによる技術情報ブログ

日時をunix timestampに変換する方法について

PHPで日付関数を使いこなす(date, strtotime) - Qiita

これから作成する機能の概要

  1. 仮登録ページ(Signup/index)でメールアドレスを入力する
  2. 入力されたメールアドレス宛に、本登録ページのURLを自動送信
  3. メールに記載されたURLをクリックして、ニックネームとパスワードを入力して、会員登録完了

usersテーブルを作成する

実行したsqlは以下の通り

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL DEFAULT '',
  `signup_code` varchar(255) NOT NULL,
  `is_registered` tinyint(1) NOT NULL DEFAULT '0',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `name` varchar(255) NOT NULL DEFAULT '',
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `signup_code` (`signup_code`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

AppController.php

下記の記事で記述した内容からそのまま。

さくらvpsとcakephp2.6.7で開発日記 その0001 お問い合わせフォームの作成をする - MOTOMICHI WORKS BLOG

SignupController.php

記述内容は以下の通り

<?php
App::uses('AppController', 'Controller');
App::uses('CakeEmail', 'Network/Email');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');

class SignupController extends AppController {
  // Model/User.phpを使う為の設定
  public $uses = array('User');

  public function index(){
    // 送信データが無い場合、Signup/index.ctpを表示する
    if(!$this->request->data){
      $this->render();
      return;
    }

    // 送信された値をUserモデルで使用できるようにset
    $this->User->set($this->request->data);

    // バリデーションチェック
    if(!$this->User->validates(array('fieldList' => array('email')))) {
      // バリデーションに引っかかった場合の処理
      $this->render();
      return;
    }

    // emailフォームに入力された値を$emailに格納
    $email = $this->request->data['User']['email'];

    // $emailと現在時刻を結合してハッシュ化
    $signup_code = md5($email.time());

    // 下記2つの条件に一致するレコードを1列取得して$userに格納する
    // - フィールド名emailの値が$email
    // - フィールド名is_registeredの値が0
    //
    // (仮登録されているデータが既にある場合は取得)
    $user = $this->User->find('first',array(
      'conditions' => array(
        'email' => $email,
        'is_registered' => 0
      )
    ));

    // 仮登録データの有無によって、処理を分ける
    if($user){
      // 仮登録データがDBに既にある場合の処理
      // UPDATEする事になるので、saveの第三引数に渡す配列を設定
      $fields = array('signup_code');
      // DBから取得した値のままだと更新されないので、modifiedをnullにする
      $user['User']['modified'] = null;
    }else{
      // 仮登録データがDBにまだ無い場合の処理
      // INSERTする事になるので、saveの第三引数に渡す配列はnullを設定
      $fields = null;
      // INSERT文実行をすることになるので、create()して、フィールドの初期値でModelを初期化
      // UPDATE文を実行するときはcreate()しては駄目なのでいつも注意する!
      $this->User->create();
      // フォームから送信された値を$userに挿入
      $user = array('User' => $this->request->data['User']);
    }

    // 仮登録状態ということをデータベースに登録するので0
    $user['User']['is_registered'] = 0;

    // signup_codeフィールドに入れる値を格納
    $user['User']['signup_code'] = $signup_code;

    // usersテーブルにUPDATEまたはINSERT(UPDATEの場合は$fieldsで指定したものだけを更新)
    if(!$this->User->save($user,false,$fields)){
      $this->Session->setFlash('システムエラーが発生しました。恐れ入りますが始めからやり直してください。');
      $this->render();
      return;
    }

    // "https://hogehoge.com/"みたいなベースURLを取得
    $full_base_url = Router::url( '/', true);
    // 本登録用のURLを生成して、$urlに格納
    $url = $full_base_url.'signup/activate/'.$signup_code;

    //emailの自動送信処理
    $cake_email = new CakeEmail('default');// Config/email.phpの$defaultの設定でインスタンスを生成
    $cake_email->from( array('mw.contacts@hoge.com' => 'Sender'));// 送信元を設定
    $cake_email->to($email);// 送信相手を設定
    $cake_email->subject('仮登録のお知らせ');// 件名を設定
    $cake_email->emailFormat('text');// HTML or テキストメール
    $cake_email->template('signup');// テンプレート
    // テンプレートへ渡す変数
    $cake_email->viewVars(array(
      'url'=>$url
    ));
    // メールを送信
    if($cake_email->send()){
      // メール送信成功時の処理
      // Signup/email_sent.ctpにリダイレクト
      $this->redirect('email_sent');
    }else{
      // メール送信失敗時の処理
      $this->Session->setFlash('システムエラーが発生しました。恐れ入りますが始めからやり直してください。');
      $this->render();
      return;
    }
  }// end of action index

  public function email_sent(){
  }// end of action email_sent

  public function activate($signup_code){

    // 下記2つの条件に一致するレコードを1行取得して$userに格納する
    // - フィールド名signup_codeの値が$signup_code
    // - フィールド名is_registeredの値が0
    $user = $this->User->find(
      'first',
      array(
        'conditions' => array(
          'signup_code' => $signup_code,
          'is_registered' => 0
        )
      )
    );

    // 上記の処理で、条件に一致する行がデータベースに無かった場合、仮登録が済んでいないのでリダイレクトする
    if(!$user){
      $this->Session->setFlash('URLが無効です。恐れ入りますがメール送信からやり直してください。');
      $this->redirect('index');
    }

    // 仮登録情報更新日時のtimestampを取得
    $modified_time = strtotime($user['User']['modified']);
    // 現在日時のtimestampを取得
    $time = time();
    // 仮登録から86,400秒よりも経過しているかを判定
    if(86400 < $time - $modified_time){
      $this->Session->setFlash('URLの有効期限が切れています。恐れ入りますがメール送信からやり直してください。');
      $this->redirect('index');
    }

    // 送信データが無い場合、Signup/activate.ctp(本登録ページ)を表示する
    if(!$this->request->data){
      $this->render();
      return;
    }

    // 送信された値をUserモデルで使用できるようにset
    $this->User->set($this->request->data);

    // バリデーションチェック
    if(!$this->User->validates(array('fieldList' => array('name','password')))) {
      // バリデーションに引っかかった場合の処理
      $this->render();
      return;
    }

    // フォームから送信されたパスワードをハッシュ化
    $passwordHasher = new BlowfishPasswordHasher();
    $user['User']['password'] = $passwordHasher->hash($this->request->data['User']['password']);

    // $user['User']['password_confirm']はもうModel内で使用しないので、unset
    unset($user['User']['password_confirm']);

    // 本登録完了のステータスを保存するので1
    $user['User']['is_registered'] = 1;

    // 送信されたニックネームを保存するので、$user['User']['name']の値を上書き
    $user['User']['name'] = $this->request->data['User']['name'];

    // usersテーブルをUPDATE
    if(!$this->User->save($user,false)){
      //save失敗した場合の処理
      $this->Session->setFlash('システムエラーが発生しました。もう一度送信をお願いします。');
      $this->render();
      return;
    }

    // save成功したら、ログインページにリダイレクト
    $this->redirect('/users/login');

  }// end of action activate

}

Model/User.phpを作成する

記述内容は以下の通り

<?php
App::uses('AppModel', 'Model');

class User extends AppModel {
  public $name = 'User';

  /**
   * 
   * 
   * @return boolean
   */
  public function isUniqueAndActive($valid_field){
    // field名を取得(例えばemail)
    $fieldname = key($valid_field);
    // 下記二つの条件に一致する行をjoinしない設定で取得
    //   - $fieldnameフィールドが、フォームから送信された値である
    //   - is_registeredフィールドが、1である
    $user = $this->find(
      'first',
      array(
        'conditions' => array(
          $fieldname => $this->data[$this->name][$fieldname],
          'is_registered' => 1
        ),
        'recursive' => -1
      )
    );
    if($user){
      // conditionsに該当する行が、既に存在する場合は、false
      return false;
    }

    return true;

  }

  /**
   *
   * @return boolean
   */
  function checkCompare($valid_field , $suffix){
    $fieldname = key($valid_field);
    if($this->data[$this->name][$fieldname] === $this->data[$this->name][$fieldname.$suffix]){
      return true;
    }
    return false;
  }

  /*
   * バリデーション対象となるキーと、そのルールを設定
   */
  public $validate = array(
    'email' => array(
      array(
        'rule' => 'notEmpty',
        'message' => 'メールアドレスを入力してください。'
      ),
      array(
        'rule' => 'email',
        'message' => '形式が正しくありません。'
      ),
      array(
        'rule' => array('checkCompare','_confirm'),
        'message' => 'メールアドレスが一致していません。'
      ),
      array(
        'rule' => 'isUniqueAndActive',
        'message' => 'すでに使用されています。'
      )
    ),
    'name' => array(
      array(
        'rule' => 'notEmpty',
        'message' => '必須項目です。'
      )
    ),
    'password' => array(
      array(
        'rule' => 'notEmpty',
        'message' => 'パスワードを入力してください。'
      ),
      array(
        'rule' => array('minLength', '8'),
        'message' => '8文字以上入力してください。'
      ),
      array(
        'rule' => array('custom', '/^[a-zA-Z0-9]+$/'),
        'message' => '半角英数字で入力してください。'
      ),
      array(
        'rule' => array('checkCompare','_confirm'),
        'message' => 'パスワードが一致していません。'
      )
    )
  );

}

View/Signup/index.ctpを作成する

記述内容は以下の通り

<h1>会員登録</h1>
<?php echo($this->Form->create()); ?>

<div class="email">
<?php
echo $this->Form->label('User.email', 'メールアドレス: ');
echo $this->Form->text(
  'User.email',
  array(
    'errorMessage' => false,
    'div' => false,
    'required' => false
  )
);
echo $this->Form->error('User.email');
?>
</div>

<div class="email-confirm">
<?php
echo $this->Form->label('Usr.email_confirm', 'メールアドレス(確認): ') ;
echo $this->Form->text(
  'User.email_confirm',
  array(
    'errorMessage' => false,
    'div' => false,
    'required' => false
  )
);
?>
</div>

<?php
echo($this->Form->end('送信'));
?>

View/Signup/email_sent.ctpを作成する

記述内容は以下の通り

<h1>会員登録</h1>

<div>
  ご入力いただいたメールアドレスに、メールを送信しました。<br>
  メール本文に記載されているURLから本登録を完了してください。
</div>

View/Signup/activate.ctpを作成する

記述内容は以下の通り

<h1>会員登録</h1>
<?php echo($this->Form->create()); ?>

<div class="name">
<?php
echo $this->Form->label('User.name', 'ニックネーム: ');
echo $this->Form->text(
  'User.name',
  array(
    'errorMessage' => false,
    'div' => false,
    'required' => false
  )
);
echo $this->Form->error('User.name');
?>
</div>

<div class="password">
<?php
echo $this->Form->label('User.password', 'パスワード: ');
echo $this->Form->password(
  'User.password',
  array(
    'errorMessage' => false,
    'div' => false,
    'required' => false
  )
);
echo $this->Form->error('User.password');
?>
</div>

<div class="passowrd-confirm">
<?php
echo $this->Form->label('User.password_confirm', 'パスワード(確認): ');
echo $this->Form->password(
  'User.password_confirm',
  array(
    'errorMessage' => false,
    'div' => false,
    'required' => false
  )
);
?>
</div>

<?php
echo($this->Form->end('送信'));
?>

View/Emails/text/signup.ctp

記述内容は以下の通り

このメールは自動送信しております。

このメールアドレスにお問い合わせをいただくことはできません。

24時間以内に下記のURLから本登録をしてください。

<?php echo ($url); ?>

また、このメールにお心当たりの無い場合は、このメールを破棄してください。

app/Config/email.php

下記の記事で記述した内容からそのまま。

さくらvpsとcakephp2.6.7で開発日記 その0001 お問い合わせフォームの作成をする - MOTOMICHI WORKS BLOG

今回の制作を通して学習できたこと

必要なファイル

  • AppController.php
  • SignupController.php
  • Model/User.php
  • View/Signup/index.ctp
  • View/Signup/email_sent.ctp
  • View/Signup/activate.ctp
  • View/Emails/text/signup.ctp
  • app/Config/email.php

$components, $helpers, $uses について、

コントローラ — CakePHP Cookbook 2.x ドキュメントによると

CakePHP は、 AppController とアプリケーションのコントローラとで、次の変数をマージします。
- $components
- $helpers
- $uses

とのこと。
AppController.phpに何を書くべきかは今後の課題。

findについて

$this->find()でそのModel自体のfindが使える。

$this->User->findでControllerからfindが使える。

詳しくはデータを取得する — CakePHP Cookbook 2.x ドキュメント

BlowfishPasswordHasherの使用について

CakePHP2.4でpasswordHasherクラスを変更する方法 - Qiita

App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');

のように読み込んで、

    // フォームから送信されたパスワードをハッシュ化
    $passwordHasher = new BlowfishPasswordHasher();
    $user['User']['password'] = $passwordHasher->hash($this->request->data['User']['password']);

のようにしてハッシュ化する。

日時をunix timestampに変換する方法について

cakephpではdatetime型のmodifiedフィールドには2015-09-19 01:43:51のような形式で自動的に日時が挿入される。

$modified_time = strtotime('2015/09/21 10:10:10');

$modified_time = strtotime('2015-09-21 10:10:10');

みたいな感じでunix timestampに変換する。

自作のバリデーションルールと適用について

User.phpの記述内容を参照。