【PHP簡易掲示板１】ログイン機能（mysqli 使用）

# 必要なページをリストアップする

| ページ | ファイル名 
|:-----------|------------:|:------------:|
| ログイン | authenticate.php 
| 会員登録ページ | register.php 
| ユーザフォーム | user_form.php 


# データベースの作成
```
CREATE TABLE `users` 
( `id` INT(255) NOT NULL AUTO_INCREMENT , `user_name` VARCHAR(50) NOT NULL 
, `nickname` VARCHAR(50) NOT NULL , `email` VARCHAR(255) NOT NULL 
, `password` VARCHAR(255) NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB;
```

nicknameは重複してはいけないので、以下のSQL も加えて実行します。

```
ALTER TABLE `users` ADD UNIQUE(`nickname`);
```

# ユーザフォームの作成。

ログイン画面、ユーザ登録画面の両方でユーザフォームを読みこみます。

```user_form.php
<?php $fname = basename( $_SERVER['PHP_SELF'] , ".php");?>
<body>
  <div class="container">
    <div class="col-xs-8 col-xs-offset-2">
      <div class="card border-info">
          <div class="card-header h1"><?php print ( FORMTITLE ); ?></div>
          <div class="card-body">
            <form method="POST">
              <div class="form-group">
                <p>ユーザ名</p>
                <input type="text"  class="form-control" name="user_name" placeholder="お名前を10文字以内で入力してください" required />
              </div>
              <?php if ( $fname != 'authenticate' ): ?>
              <div class="form-group">
                <p>ニックネーム</p>
                <input type="text"  class="form-control" name="nickname" placeholder="nicknameを入力してください" required />
              </div>
              <div class="form-group">
                <p>メールアドレス</p>
                <input type="email"  class="form-control" name="email" placeholder="Emailで入力してください" required />
              </div>
              <?php endif; ?>
              <div class="form-group">
                <p>パスワード:<span id ='password_length'>0</span>文字<span>( 半角英数字5から15文字以内でお願いします。)</span></p>
                <input type="password"  class="form-control" name="password" placeholder="Passwordを5~15文字以内で入力してください"  required />
              </div>
              <button type="submit" class="btn btn-lg btn-info" name= "action"><?php print ( BUTTONTEXT ); ?></button>
              <a href="<?php print ( URL ); ?>"><?php print ( LINKTEXT ); ?></a>
            </form>
          </div>
      </div>
    </div>
  </div>
</body>
</html>
```

# navbarの作成

各ファイル共通で読みこむnavbarを作成します。
isAuthenticated()関数は後ほど、作成いたします。

```navbar.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>
  </title>
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
</head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
</script>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand">
        php-bulletinboard
      </a>
    </div>
    <div class="collapse navbar-collapse" id="navbarEexample">
      <ul class="navbar-nav mr-auto">
        <?php if ( ! isAuthenticated() ): ?>
        <li class="nav-item">
          <a class="nav-link text-white" href="register.php">新規登録</a>
        </li>
        <li class="nav-item">
          <a class="nav-link text-white" href="authenticate.php">ログイン</a>
        </li>
        <?php else: ?>
        <li class="nav-item">
          <a class="nav-link text-white" href="signout.php">ログアウト</a>
        </li>
        <?php endif; ?>
      </ul>
    </div>
  </div>
</nav>
</body>
```


defineで各ページのユーザフォームで使う定数を定義します。

```register.php
<?php 

include ( "common.php" );
include ( "session.php" );
include ( "user.php" );
include ( "navbar.php" );

// submitボタンが押された場合の処理
if ( isset( $_POST['action'] ) ) {
    user_register( $_POST['user_name'], $_POST['nickname'], $_POST['email'] , $_POST['password'] ));
}

define( "FORMTITLE", "アカウント作成" );
define( "BUTTONTEXT", "アカウントを作成する" );
define( "URL", "authenticate.php" );
define( "LINKTEXT", "ログインはこちら" );

require ( "user_form.php" );
```

```authenticate.php
<?php 

include ( "dbconfig.php" );
include ( "common.php" );
include ( "session.php" );
include ( "navbar.php" );

// submitボタンが押された場合の処理
if ( isset( $_POST['action'] ) ) {
    authenticate( $_POST['user_name'], $_POST['password'] );
}

define( "FORMTITLE", "ログインはこちら" );
define( "BUTTONTEXT", "ログインする" );
define( "URL", "register.php" );
define( "LINKTEXT", "会員登録はこちら" );

require ( "user_form.php" );
```

# データベース接続処理

```dbconfig.php
<?php
// データベースの接続 

function get_db() {
    static $mysqli;

    $HOST = "localhost";
    $USERNAME = "root";
    $PASSWORD = "";
    $DBNAME = "bulletinboard";

    $mysqli = new mysqli( $HOST, $USERNAME, $PASSWORD, $DBNAME );

    if ( $mysqli->connect_error ){
        print $mysqli->connect_error();
        exit;
    } else {
        $mysqli->set_charset( "utf8" );
    }

    return $mysqli;
}


function escape( $str ) {
    $mysqli = get_db();

    return $mysqli->real_escape_string( $str );
}
```

# 登録処理

```user.php
<?php

// 登録処理
function user_register( $user_name, $nickname, $email, $password ) {

    $errors = user_validation( $user_name, $nickname, $email, $password );

    if ( count( $errors ) == 0 ) {
        user_insert( $user_name, $nickname, $email, $password );
    }

    error_display( $errors );
}

// ユーザのバリデーション（若干抜けてる部分があるかもしれません）
function user_validation( $user_name, $nickname, $email, $password ) {

    $errors = array();

    if ( ! mb_strlen( trim( $user_name ) ) ) {
        $errors[] = "ユーザ名が空です";
    } else if ( mb_strlen( trim( $user_name ) ) > 15 ) {
        $errors[] = "ユーザ名は15文字以内で入力してください";
    } else if ( is_numeric( $user_name ) ) {
        $errors[] = "ユーザ名は文字列でなくてはいけません";
    }

    if ( ! mb_strlen( trim( $nickname ) ) ) {
        $errors[] = "nicknameが空です";
    } else if ( mb_strlen( trim( $nickname ) ) > 15 ) {
        $errors[] = "nicknameは15文字以内で入力してください";
    } else if ( ! isUniq( 'users', 'nickname' , trim( $nickname ) ) ) {
        $errors[] = "nicknameが重複しています";
    } else if ( is_numeric( $nickname ) ) {
        $errors[] = "nicknameは文字列でなくてはいけません";
    }

    if ( ! mb_strlen( trim( $email ) ) ) {
        $errors[] = "emailが空です";
    } else if ( mb_strlen( trim( $email ) ) > 255 ) {
        $errors[] = "emailは255文字以内で入力してください";
    } 

    if ( ! mb_strlen( trim( $password ) ) ) {
        $errors[] = "passwordが空です";
    } else if ( ! preg_match( '/^[0-9a-z]{5,15}$/', trim( $password ) ) ) {
        $errors[] = "passwordは半角英数字5文字から15文字以内でお願いします";
    }   

    return $errors;
}

function user_insert( $user_name, $nickname, $email, $password ) {
    // データをエスケープ
    $user_name = escape( $user_name );
    $nickname = escape( $nickname );
    $email = escape( $email );
    $password = escape( $password );
    $password = password_hash( $password, PASSWORD_DEFAULT ); // password hash化

    $query = "INSERT INTO users 
                ( user_name, nickname,  email, password 
            ) VALUES (
                '$user_name', '$nickname', '$email', '$password'
            )";
    
    $result = query( $query );

    print 'ユーザの作成に成功しました';

    $result->close();
}
```
# ログイン処理

```session.php
<?php

session_start();
session_regenerate_id();
$fname = basename( $_SERVER['PHP_SELF'] , ".php");
var_dump( $_SESSION );


// ユーザ新規登録画面、ログイン画面のどちらかかつログアウトページでない場合
if ( $fname == 'authenticate' || $fname == 'register' && $fname != 'signout') {
    if ( isAuthenticated() ) {
        header( "Location: root.php" );
        exit();
    } 
// ユーザ新規登録画面、ログイン画面以外
} else if ( ! isAuthenticated() ) { 
    // ログインしていない場合はログイン画面に遷移  
    header( "Location: authenticate.php" );
    exit();
} else {
    // ログインしている場合は、前回ログインからの時間が30分以上ならsessionを破棄する
    $now = strtotime("now");
    $session_last = session_get( 'session_created_at' );
    $elapsed_time = $now - $session_last;

    if( $elapsed_time > 1800 ){
        session_clear();
        print 'ログイン後、30分が経過したのでログアウトします お手数ですが、リロードして再度ログインをお願いいたします';
        exit();
    }
} 

function authenticate( $user_name, $password ) {

    $user_name = escape( $user_name );
    $password = escape( $password );

    $query = "
        SELECT * FROM users 
        WHERE user_name='$user_name'
        ";

    $result = query( $query );

    while ($row = $result->fetch_assoc()) {
        $user_id = $row['id'];
        $user_name = $row['user_name'];
        $nickname = $row['nickname'];
        $email = $row['email'];
        $db_password = $row['password'];
    }

    // データベースの切断
    $result->close();

    if ( password_verify( $password, $db_password ) ) {
        session_set( 'user_id', $user_id );
        session_set( 'user_name', $user_name );
        session_set( 'nickname', $nickname );
        session_set( 'email', $email );
        session_set( 'password', $db_password );
        session_set( 'session_created_at', strtotime( 'now' ) );
        session_regenerate_id( true );
        header( "Location: posts.php" );
        exit;
    } else {
        print 'ログインに失敗しました';
    }
}

function session_set( $key, $value ) {
    $_SESSION[$key] = $value;    
}

function session_clear() {
    $_SESSION = array();
}

// ログインしていればtrueを返却、していなければfalseを返却
function isAuthenticated()
{
    if ( isset( $_SESSION['user_name'] ) ) {
        return true;
    }

    return false;
}

function session_get( $key ) {
    if ( isset( $_SESSION[$key] ) ) {
        return $_SESSION[$key];
    } 

    return null;
}
```

複数回行う処理は関数にします。

```common.php

function h( $str ) {
    return htmlspecialchars( $str, ENT_QUOTES, 'UTF-8');
}

function query(  $query ) {
    $mysqli = get_db();

    $result = $mysqli->query( $query );

    if ( ! $result ) {
        print 'クエリが失敗しました' . "Errormessage: %s\n" . $mysqli->error;
        $mysqli->close();
        exit();
    }

    return $result;
}   

// テーブル名、カラム、値を指定してテーブルに、指定したカラムを持つ値のレコードが存在するか判定
function isUniq( $table, $column, $value ) {

    $value = escape( $value );

    $query = "
        SELECT * FROM $table
        WHERE $column = '$value'
        ";

    $result = query( $query );

    if ( mysqli_num_rows( $result ) == 0  ) {
        return true;
    }

    return false;
}


function get_current_datetime() {
    $now = new DateTime();
    $now = $now->format('Y-m-d H:i:s');

    return $now;
}

function error_display( $errors ) {   
    foreach ( $errors as $error ) {
        ?>
        <div class="container">
            <div class="alert alert-dismissible alert-warning">
            <?php print h( $error ) ?>
            </div>
        </div>
        <?php
    }
}
```



# サインアウト

navbar.phpに以下を追記。

```
<li class="nav-item">
   <a class="nav-link text-white" href="signout.php">ログアウト</a>
</li>
```

```signout.php
<?php 

include ( "session.php" );
include ( "navbar.php" );

$_SESSION = array();

header( "Location: authenticate.php" );
exit();
```