会員登録機能を実装する。PHPでSNSを作成してみる#14

今回は、会員登録機能を記します。

前回の章では、APIとDBの接続部分の内部処理を作成しました。
今回は、実際にフォームから情報の入力、データの保存まで行います。
実際のフォームについては以下の章で作成をしています。

処理の流れ

この章までに作成したファイルに追記していきます。

必要なファイル

下記は、新規で作成したファイル及び追記したファイルのみ記載しています。
※下記のみだと正常に実行されないのでシリーズの過去記事をご覧ください

sns_training
├── api
│    └── Connection
│         └── api.php
├── app
│    ├── auth
│    │     └── hash.php
│    ├── constants
│    │     └── errorMessageConstant.php
│    ├── controllers
│    │     ├── homeController.php
│    │     └── registController.php
│    ├── models
│    │     ├── inputModels
│    │     │      ├── A00001InputModel.php
│    │     │      └── A00002InputModel.php
│    │     └── outputModels
│    │            └── A00001OutputModel.php
│    └── services
│           └── userServices
│                ├── registUserService.php
│                └── userExistsService.php
└── views
     └── pages
          ├── home.php
          └── regist.php

今回は、会員登録フォームで入力した値が
DBに登録され、/homeに遷移するところまで行います。

バリデーションや、セッションの実装については後の章で記します。

TABLE: USERS

USERSテーブルを作成しましょう。

DB名や接続情報については、下記の章に記述しています。
APIとDB接続処理の作成をする。PHPでSNSを作成してみる#13

今回実装で使用する最小限の構成でカラムを作成しましょう。

名前 データ型 照合順序 NULL デフォルト値 その他
 ID  int(11) いいえ  なし  AUTO_INCREMENT
PRIMARY
 USER_ID  varchar(20)  utf8_general_ci いいえ なし
 USER_NAME  varchar(20)  utf8_general_ci いいえ なし
 PASSWORD  varchar(100)  utf8_general_ci いいえ なし
--
-- テーブルの構造 `USERS`
--

CREATE TABLE `USERS` (
  `ID` int(11) NOT NULL,
  `USER_ID` varchar(20) NOT NULL,
  `USER_NAME` varchar(20) NOT NULL,
  `PASSWORD` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `USERS`
--
ALTER TABLE `USERS`
  ADD PRIMARY KEY (`ID`),
  ADD KEY `ID` (`ID`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `USERS`
--
ALTER TABLE `USERS`
  MODIFY `ID` int(11) NOT NULL AUTO_INCREMENT;
IDはAUTO_INCREMENTを使用して自動採番されるようにしています。
また、USER_ID、PASSWORDについては、NULLは存在しないようにしています。これは、レコードがある以上必ず値が入る前提だからです。上記では、USER_NAMEをNOT NULLにしていますが、場合によってはデフォルト値を「名無さん」、NULLを許可してあげてもよいかもしれませんね。

●ユーザテーブルは論理削除か、物理削除か?

うーん。ユーザテーブルは論理削除の方がよいかなぁ。。。

はい、論理削除が安全です。USERSテーブルに有効フラグのカラムを作成して、1 or 0 のように管理をするべきでしょう。

じゃがのぅ。個人情報を含んでいるんじゃ。。

個人情報保護法によって、アカウント削除後の必要のなくなった個人情報の削除義務が発生するんじゃ。

 

はい、個人情報に該当するデータについては、マスキング処理を施す必要がございます。

ただし、処理の煩雑さが増すのでデータ保持が不必要の場合は、物理削除でも問題がないでしょう。
理想を言うのであれば、90日間は論理削除。その日を超えると物理削除でしょうかね。

 

 

●内部ユーザIDは設けるべきか?

困ったわねぇ、ネットで誹謗中傷された相手がユーザIDを変えてしまって追えないわね〜。

お任せください。USERSテーブルに内部IDを設けさせていただいております。そのため、ユーザに見えるUSER_IDを変更しても内部IDは一定です。

今回の実装では設けません。また、内部IDを設けなくとも自動採番のIDが一定なので実は問題ありません。

ただし、Twitterのようにエンドユーザが調べれば分かるような内部IDを主キーのIDにするにはよくないです。せめて一意な乱数がよいですよね。

registController.php

<?php
namespace App\Controllers;

use App\Services\UserServices\registUserService;
use App\Services\UserServices\userExistsService;

class registController extends controller{
    //@初期表示
    public function index(){
        $model = null; 
        $this->View("regist",$model);
    }

    //@会員登録
    public function registPost($request){
      $model = null;
      $userExistsService = new userExistsService;
      $userExistsObj = $userExistsService->request([
        'USER_ID'=>$request['userId'],
      ]);

      //ユーザIDが重複しない場合
      if($userExistsObj['CODE'] != 1){
        $model = $userExistsObj;
        $this->Json($model);
      }

      $registUserService = new registUserService;
      $registUserObj = $registUserService->request([
        'USER_ID'=>$request['userId'],
        'USER_NAME'=>$request['userName'],
        'USER_PASS'=>$request['userPassWord'],
      ]);

      $model = $registUserObj;
      $this->Json($model);
    }
}
?>
$userExistsService = new userExistsService;
$userExistsObj = $userExistsService->request([ 
   'USER_ID'=>$request['userId']
 ]);

リクエストするユーザIDと一致するユーザ数を返すサービスをインスタンス化しています。
後述するAPIで引数はUSER_IDのみです。そのためここでも引数はUSER_IDのみです。

返却された値は$userExistsObjに代入されます。

//ユーザIDが重複しない場合 
if($userExistsObj['CODE'] != 1){
  $model = $userExistsObj;
   $this->Json($model);
}

一致するユーザ数の入ったオブジェクト「$userExistsObj」から
CODE(成功or失敗)のステータスで1(成功)を除いた場合に、
Viewにオブジェクトを返却します。

後述しますuserExistsServiceクラスで、ユーザ数が1以上の場合
CODEを0、0の場合はCODEを1としているために、CODEで分岐をさせています。

$this->Json(…)のメソッド内部でdieを使用して処理を停止しているため、処理終了の記述をする必要はありません。

$registUserService = new registUserService;
$registUserObj = $registUserService->request([ 
   'USER_ID'=>$request['userId'],
 'USER_NAME'=>$request['userName'],
 'USER_PASS'=>$request['userPassWord']
 ]);

この処理を通る時、必ずリクエストされたユーザIDと一致するユーザIDは無いという前提です。

今回の実装では、平文のパスワードはサービスクラスでハッシュ化します。

$model = $registUserObj;
$this->Json($model);

ここの処理では、ユーザ登録の成功したか否かの情報が入っています。
ビューに返却しています。

homeController.php

<?php
namespace App\Controllers;
class homeController extends controller{
//@初期表示
public function index(){
  $model = null; 
  $this->View("home",$model);
  }
}
?>

今回Homeでは、会員登録後の遷移画面として使用するだけなので、必要最低限の実装です。

regist.php

registビュー

<!DOCTYPE html>
 <html lang="ja">
 <head>
   <?php require_once(__DIR__ . "/../components/head.php"); ?>
 </head>
<body>
 <div id="app px-4">
  <div class="row mx-0">
   <div class="offset-0 col-12 col-md-6 offset-md-4">
    <h3 class="text-center"> 
     会員登録
    </h3>
    <form method="POST" id="registForm">
     <div class="form-group">
       <label for="userId">ユーザID</label>
       <input type="text" class="form-control" name="userId" id="userId">
    </div>
    <div class="form-group">
      <label for="userName">ユーザ名</label>
      <input type="text" class="form-control" name="userName" id="userName">
   </div> 
   <div class="form-group">
     <label for="userPassWord">パスワード</label>
     <input type="password" class="form-control" name="userPassWord" id="userPassWord">
   </div>
   <div class="form-group">
     <button type="submit" class="btn btn-lg btn-primary form-control">登録する</buttton>
   </div>
  </form>
</div>
</div>
</div>
</body>
<script>
$('#registForm').submit(function(){
  event.preventDefault();
 $.ajax({
   url: 'post/regist',
   type: 'POST',
   dataType: 'json',
   data: $(this).serializeArray(),
   timeout: 5000,
  }).done(function(data){
  if(data['CODE'] != 1){
    alert(data['MESSAGE']);
  }else{
    location.href="./home";
  }
 }).fail(function(){
   alert('内部エラーです。');
 });
});
</script>
</html>
if(data['CODE'] != 1){
 alert(data['MESSAGE']);
}else{...}

コントローラから返却されたオブジェクト内の「CODE」では処理の成功か否かを判断しています。今回は「1」以外は失敗としています。

失敗した時は、オブジェクト内の「MESSAGE」をダイアログボックスとして表示しています。

この「MESSAGE」はエラーメッセージが入っています。

else{ 
 location.href="./home";
}

処理が成功した場合は、「~/home」に遷移します。

home.php

会員登録が完了しました。

今回Homeでは、会員登録後の遷移画面として使用するだけなので、必要最低限の実装です。

errorMessageConstant.php

<?php
namespace App\Constants;

class errorMessageConstant {
  const ALREADY_EXISTS_USER_ID = "このユーザIDは既に使用されています。";
  const INTERNAL_ERROR = "内部エラーです。";
}
?>
class errorMessageConstant {....}

このクラスはエラーメッセージをまとめて管理するファイルです。
エラーメッセージを変更する必要がないのでファイルに定数として記述しています。

もし、変更する可能性があるのであれば、DBにメッセージ用のテーブルを作成してもよいでしょう。

ただし、識別するためのID?などをソース内で持っておくと改修が面倒なので注意が必要ですね。

hash.php

<?php
namespace App\Auth;

class Hash{
    public static function SetPass($val){
        return password_hash($val,PASSWORD_DEFAULT);
    }
    public static function ExistPass($inputpass,$digestPass){
        return password_verify($inputpass,$digestPass);
    }
}
?>

このクラスは、パスワードのハッシュ化、認証時に使用します。

public static function SetPass($val){ 
 return password_hash($val,PASSWORD_DEFAULT);
}

PHPのpassword_hashという関数を使用してハッシュ化します。
ストレッチング回数も自動、ソルト値も指定しなければランダムな値が使用されます。基本、お任せが推奨ですね。

password_hashや、password_verifyについては、下記の記事で紹介をしています。

【パスワードのハッシュ化】PHPでpassword_hashを使った認証方法のご紹介。

A00001InputModel.php

<?php
namespace App\Models\InputModels;

class A00001InputModel {
    //ユーザ名
    public $USER_ID;
}
?>

A00001というAPIでは、リクエストされるユーザIDと一致するユーザ数を返却します。

A00002InputModel.php

<?php
namespace App\Models\InputModels;

class A00002InputModel {
    //ユーザ名
    public $USER_ID;
    
    //ユーザ名
    public $USER_NAME;

    //ダイジェストパスワード
    public $PASSWORD;
}
?>

A00002は、会員登録で使用するインプットモデルです。

A00001OutputModel.php

<?php
namespace App\Models\OutputModels;

class A00001OutputModel {
        public $COUNT;
}
?>

A00001のアウトプットモデルです。
ユーザ数を持っています。

api.php

<?php
namespace Api\Connection;
use Api\Connection\Connection;
class api{
  /*
  @A00001
  一致するユーザ数を返します。
  */
  public function A00001($param = null){
    $sql = null;
    $sql .= <<< EOM 
SELECT 
  COUNT(*) AS COUNT
FROM 
  USERS USR
WHERE 
  USR.USER_ID = :USER_ID;
EOM;
 $connection = new Connection(); 
return $connection->con($sql, $param);
  }

    /*
   @A00002
   ユーザを登録します。
  */
  public function A00002($param = null){
    $sql = null;
    $sql.= <<< EOM 
INSERT INTO
USERS (
  USER_ID,
  USER_NAME,
  PASSWORD
)
VALUES (
  :USER_ID,
  :USER_NAME,
  :PASSWORD );
EOM;
 $connection = new Connection();
 return $connection->con($sql,$param);
  }
}

それぞれ、SQL文を作成します。

userExistsService.php

<?php
namespace App\Services\UserServices;
use App\Services\service;
use App\Models\InputModels\A00001InputModel;
use App\Models\OutputModels\A00001OutputModel;
use App\Constants\errorMessageConstant;

class userExistsService extends service {

    protected $REQUEST_API_NAME;
    public function input($request = null){
      $this->REQUEST_API_NAME = "A00001";
      $a00001 = new A00001InputModel(); 
      $a00001->USER_ID = $request['USER_ID'];
      return $a00001;
    }

    public function output($result = null,$request = null){
      $a00001OutputModel = new A00001OutputModel;
      $a00001OutputModel->COUNT = $result['0']['COUNT'];
      //リクエストされたユーザIDが重複しない場合
      if($a00001OutputModel->COUNT < 1){ 
        return $this->callRtnHandle($a00001OutputModel);  
      }
      return $this->callRtnHandle($a00001OutputModel,errorMessageConstant::ALREADY_EXISTS_USER_ID,0);
    }
}
?>
$a00001OutputModel->COUNT = $result['0']['COUNT'];

SQLではCOUNTのみで返却されますが、多重の構造になっているため、[0][‘COUNT’]と指定しています。

//リクエストされたユーザIDが重複しない場合
 if($a00001OutputModel->COUNT < 1){
 return $this->callRtnHandle($a00001OutputModel);
 }

リクエストされたユーザIDのカウントが1未満であれば、モデルをそのまま返しています。
成功時は、この時点でサービスクラスの処理は終了です。

return $this->callRtnHandle($a00001OutputModel,errorMessageConstant::ALREADY_EXISTS_USER_ID,0);

この処理は、ユーザIDが既に存在していた場合に返却される値です。
第二引数では、エラーメッセージをコンストラクタのエラーメッセージを取得しています。
第三引数では、エラーステータスとなるCODE「0」を返却しています。

なお、引数を指定しない場合は、それぞれ
第二引数がNULL、第三引数が1です。

registUserService.php

<?php 
namespace App\Services\UserServices;
use App\Services\service;
use App\Auth\Hash;
use App\Models\InputModels\A00002InputModel;
use App\Constants\errorMessageConstant;

class registUserService extends service {

    protected $REQUEST_API_NAME;
    public function input($request = null){
      $this->REQUEST_API_NAME = "A00002";

      $digestPasswd = Hash::SetPass($request['USER_PASS']);

      $a00002 = new A00002InputModel(); 
      $a00002->USER_ID = $request['USER_ID'];
      $a00002->USER_NAME = $request['USER_NAME'];
      $a00002->PASSWORD = $digestPasswd;
      return $a00002;
    }

    public function output($result = null,$request = null){
      if($result){
        return $this->callRtnHandle($result);    
      }
      return $this->callRtnHandle($result,errorMessageConstant::INTERNAL_ERROR,0);  
    }
}
?>
$digestPasswd = Hash::SetPass($request['USER_PASS']);

ここで、平文のパスワードをハッシュ化しています。
USERSのPASSWORDにはハッシュ化したパスワードを入れましょう。

if($result){...}

取得以外の構文を使用したとき、成功か否かが$resultに入っています。
そのため、$resultの値で分岐をさせています。

会員登録をしてみる

登録成功

実装した会員登録をしてみましょう。
まずは、ユーザIDを「test」として登録をしてみます。

「登録する」を押下すると、「会員登録が完了しました。」というページに遷移しました。
実装した通り、このページに遷移したということは、登録が成功しています。

データベースで「USERS」テーブルのレコードを確認します。
問題なく反映されています。

登録失敗

では、先ほどのユーザIDに重複させる形で登録をしてみます。

重複ID「test」で登録します。
既にDBに同じIDがある場合、ポップアップで
「このユーザIDは既に使用されています。」と表示され登録がされません。

このメッセージについてはerrorMessageConstantで管理しています。

おわりに

今回は、会員登録の処理について作成しました。
随分、長文となり失礼しました。

私自身、常にインプットしてよりよい実装を心がけています。
あくまで趣味レベルになっていますが、それでも日々、実務で使用できるように努力しています。

自走するのは大切ですが、何も考えずに突っ走るのは危険です。
現在どのようなシステムがなぜ、そのような実装になっているのか。どこに留意すべきか。など何も考えずに学べるわけありません。
独学でも厳しい内容です。
現役エンジニアから学ぶことによって、簡単に”知ること”ができます。

テックアカデミーでは、受講生に1人ずつ現役エンジニアのパーソナルメンターがつきます

そのため、より即戦力となる知識をつけることができます。
効率的に学ぶことは、実務でも役に立ちます。

そんな効率的に学びたい方
ぜひ、テックアカデミー をご利用ください。

 

コメント

スポンサーリンク
スポンサーリンク
タイトルとURLをコピーしました