【PHPでECサイト②】ログイン機能

## はじめに
今回は、PHPとMySQLを使ってログイン機能を実装していきます。
**※当ページは、[【PHPでECサイト】](https://qiita.com/taqmi/items/877292fc8c902f86e20e)で作られたものを前提にしています。**

### バージョン
PHP：7.4.5
phpMyAdmin：5.0.2
MySQL：5.7.30

### 今回作成するファイル
**html**
- login.php
- login_process.php
- signup.php
- signup_process.php
- logout.php

**model**
- users.php

**view**
- login_view.php
- signup_view.php

## テーブルの作成

```sql:sample_users.sql
CREATE TABLE `sample_users` (
  `user_id` int(11) NOT NULL,
  `user_name` varchar(20) NOT NULL,
  `password` varchar(20) NOT NULL,
  `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE `sample_users`
  MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT,
  ADD PRIMARY KEY (`user_id`);
```
 
## 定義
**処理に使うものを追記**

```php:const.php
define('SIGNUP_URL', '/signup.php');
define('LOGIN_URL', '/login.php');
define('LOGOUT_URL', '/logout.php');

// 正規表現
define('REGEXP_ALPHANUMERIC', '/\A[0-9a-zA-Z]+\z/'); 

// 文字数制限
define('USER_NAME_LENGTH_MIN', 6);
define('USER_NAME_LENGTH_MAX', 20);
define('USER_PASSWORD_LENGTH_MIN', 6);
define('USER_PASSWORD_LENGTH_MAX', 20);
```

## ユーザ登録
### Modelの作成
**users.phpを作成し、ユーザ登録・ログイン処理を記述**

```php:users.php

// ログイン
function get_user_by_name($db, $name){
    $sql = "
      SELECT
        user_id,
        user_name,
        password
      FROM
        sample_users
      WHERE
        user_name = ?
    ";
    return fetch_query($db, $sql, array($name));
}

function login_as($db, $name, $password){
    $user = get_user_by_name($db, $name);
    if($user === false || $user['password'] !== $password){
        return false;
    }
    set_session('user_id', $user['user_id']);
    return $user;
}

// ユーザ登録
function regist_user($db, $name, $password) {
    if(is_valid_user($name, $password) === false){
        return false;
    }
    return insert_user($db, $name, $password);
}

// バリデーション
function is_valid_user($name, $password){
    // 短絡評価を避けるため一旦代入。
    $is_valid_user_name = is_valid_user_name($name);
    $is_valid_password = is_valid_password($password);
    return $is_valid_user_name && $is_valid_password ;
}
  
function is_valid_user_name($name) {
    $is_valid = true;
    if(is_valid_length($name, USER_NAME_LENGTH_MIN, USER_NAME_LENGTH_MAX) === false){
      set_error('ユーザー名は'. USER_NAME_LENGTH_MIN . '文字以上、' . USER_NAME_LENGTH_MAX . '文字以内にしてください。');
      $is_valid = false;
    }
    if(is_alphanumeric($name) === false){
      set_error('ユーザー名は半角英数字で入力してください。');
      $is_valid = false;
    }
    return $is_valid;
}
  
function is_valid_password($password){
    $is_valid = true;
    if(is_valid_length($password, USER_PASSWORD_LENGTH_MIN, USER_PASSWORD_LENGTH_MAX) === false){
      set_error('パスワードは'. USER_PASSWORD_LENGTH_MIN . '文字以上、' . USER_PASSWORD_LENGTH_MAX . '文字以内にしてください。');
      $is_valid = false;
    }
    if(is_alphanumeric($password) === false){
      set_error('パスワードは半角英数字で入力してください。');
      $is_valid = false;
    }
    return $is_valid;
}
  
// ユーザデータの挿入
function insert_user($db, $name, $password){
    $sql = "
      INSERT INTO
        sample_users(user_name, password)
      VALUES (?,?);
    ";
    return execute_query($db, $sql, array($name, $password));
}
```

```php:functions.php
// ログイン処理
function is_logined(){
  return get_session('user_id') !== '';
}

// バリデーション
function is_valid_length($string, $minimum_length, $maximum_length = PHP_INT_MAX){
  $length = mb_strlen($string);
  return ($minimum_length <= $length) && ($length <= $maximum_length);
}

function is_alphanumeric($string){
  return is_valid_format($string, REGEXP_ALPHANUMERIC);
}

function is_valid_format($string, $format){
  return preg_match($format, $string) === 1;
}
```

### Viewの作成
※メッセージ表示のところは、繰り返し使用する為、別のファイルに記述し、それを読み込んでいます。

```php:signup_view.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>ユーザ登録</title>
  </head>

  <body>
    <h1>ユーザ登録ページ</h1>
    
    <!-- メッセージ・エラーメッセージ -->
    <?php include VIEW_PATH. 'templates/messages.php'; ?>

    <!-- ログインフォーム -->
    <form method="post" action="signup_process.php">
      <div>
        <label>ユーザ名：</label>
        <input type="text" name="name">
      </div>
      <div>
        <label>パスワード：</label>
        <input type="password" name="password">
      </div>
      <input type="submit" value="新規登録">
      <input type="button" onclick="location.href='<?php print(LOGIN_URL); ?>'" value="ログインページへ">
    </form>
  </body>
</html>
```

### Controllerの作成

```php:signup.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';

session_start();

// ログインされていれば、商品管理ページへ遷移
if(is_logined() === true){
    redirect_to(ADMIN_URL);
}

include_once VIEW_PATH. 'signup_view.php';
```

```php:signup_process.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'users.php';

session_start();

// ログインされていれば、商品管理ページへ遷移
if(is_logined() === true){
    redirect_to(ADMIN_URL);
}

// Postされたものを定義
$name = get_post('name');
$password = get_post('password');

// データベース接続
$db = get_db_connect();

// ユーザ登録処理
try{
    $result = regist_user($db, $name, $password);
    if($result === false){
        set_error('ユーザ登録に失敗しました。');
        redirect_to(SIGNUP_URL);
    }
}catch(PDOException $e){
    set_error('ユーザ登録に失敗しました。');
    redirect_to(SIGNUP_URL);
}
set_message('ユーザ登録が完了しました。');

// ユーザ登録完了後、そのままログインして商品管理ページへ遷移
login_as($db, $name, $password);
redirect_to(ADMIN_URL);
```

## ログイン

### Viewの作成
※メッセージ表示のところは、繰り返し使用する為、別のファイルに記述し、それを読み込んでいます。

```php:login_view.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>ログイン</title>
  </head>

  <body>
    <h1>ログインページ</h1>
    
    <!-- メッセージ・エラーメッセージ -->
    <?php include VIEW_PATH. 'templates/messages.php'; ?>

    <!-- ログインフォーム -->
    <form method="post" action="login_process.php">
      <div>
        <label>ユーザ名</label>
        <input type="text" name="name">
      </div>
      <div>
        <label>パスワード</label>
        <input type="password" name="password">
      </div>
      <input type="submit" value="ログイン">
      <input type="button" onclick="location.href='<?php print(SIGNUP_URL); ?>'" value="ユーザ登録ページへ">
    </form>
  </body>
</html>
```

### Controllerの作成
```php:login.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH . 'functions.php';

session_start();

// ログインされていれば、商品管理ページへ遷移
if(is_logined() === true){
  redirect_to(ADMIN_URL);
}

include_once VIEW_PATH . 'login_view.php';
```

```php:login_process.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'users.php';

session_start();

// ログインされていれば、商品管理ページへ遷移
if(is_logined() === true){
    redirect_to(ADMIN_URL);
}

// Postされたものを定義
$name = get_post('name');
$password = get_post('password');

// データベース接続
$db = get_db_connect();

// ログイン処理
$user login_as($db, $name, $password);
if($user === false){
    set_error('ログインに失敗しました。');
    redirect_to(LOGIN_URL);
}
set_message('ログインしました。');
redirect_to(ADMIN_URL);
```

## ログアウト

### Modelの作成

```php:users.php
// ログイン状況
function get_user($db, $user_id){
    $sql = "
      SELECT
        user_id, 
        user_name,
        password
      FROM
        sample_users
      WHERE
        user_id = ?
    ";
    return fetch_query($db, $sql, array($user_id));
}
  
function get_login_user($db){
    $login_user_id = get_session('user_id');  
    return get_user($db, $login_user_id);
}  
```

### Viewの作成
遷移先のページに、ログアウトできるよう追記

```php:admin_view.php
<p>ようこそ、<?php print($user['user_name']); ?>さん。</p>
<a href="<?php print(LOGOUT_URL);?>">ログアウト</a>
```

### Controllerの作成

```php:logout.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';

session_start();

$_SESSION = array();
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
  $params["path"],
  $params["domain"],
  $params["secure"],
  $params["httponly"]
);

session_destroy();

redirect_to(LOGIN_URL);
```

```php:admin.php
require_once MODEL_PATH. 'users.php';

// ログインされてなければ、ログインページへ遷移
if(is_logined() === false){
    redirect_to(LOGIN_URL);
}  

$user = get_login_user($db);
```

## 参考
[session_get_cookie_params](https://www.php.net/manual/ja/function.session-get-cookie-params.php)