簡単　STEPごとに作る自作MVC WebFramework PART2

# 自作 Php MVC Web Framework PART2

[PART1はこちら](https://qiita.com/sakunowman/items/b8f206661ab11af68d38)

完成したレポジトリのリンクを張っておきます。
developブランチで確認してください。
[レポジトリ](https://github.com/Saknowman/study_php_web_framework)

前回までのSTEPは

- [STEP0: 環境構築](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step0-%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89)
- [STEP1: 全体像](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step1-%E5%85%A8%E4%BD%93%E5%83%8F)
- [STEP2: プロジェクト](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step2-%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)
- [STEP3: リクエストとレスポンス](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step3-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%A8%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9)
- [STEP4: Controller](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step4-controller)
- [STEP5: View](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step5-view)
- [STEP6: Router](https://qiita.com/sakunowman/items/b8f206661ab11af68d38#step6-router)

でしたね。

続きをやっていきたいと思います。
これからやるSTEPは以下になります

- [STEP7: DB](#step7-db)
- [STEP8: セッションとユーザー認証](#step8-セッションとユーザー認証)
- [STEP9: Middleware](#step9-middleware)

# STEP7: DB
次は**DBの実装**を行っていきます。
DBMangerに**各テーブルのレポジトリーを登録**し、**DBManger**で管理するという方法をとります。

このSTEPではDBを使用できるようにDBMangerを作成し、TaskAppでCRUDの動作確認を行っていきます。

* CRUD: Create, Read, Update, Delete

このSTEPで実装・編集するファイルは以下になります。

```text
project.com
      ├─ Config/
      │     └─ DBSettings.php
      │
      ├─ Libs/
      │     ├─ DB/
      │     │   ├─ DBManager.php
      │     │   ├─ Entity.php
      │     │   └─ Repository.php
      │     │
      │     └─ Project.php
      │
      ├─ TaskApp/
      │     ├─ Controllers/
      │     │   └─ TasksController.php
      │     │
      │     ├─ Entities/
      │     │   └─ Task.php
      │     │
      │     ├─ migrations/
      │     │   └─ init_tasks_table.php
      │     │
      │     ├─ Repositories/
      │     │   └─ TasksRepository.php
      │     │
      │     └─ RoutingTable.php
      │
      └─ templates/
            └─ task/
                ├─ create.tmp.php
                ├─ detail.tmp.php
                ├─ edit.tmp.php
                └─ index.tmp.php
```



## データベースの設定等
今回の記事の本質的な部分ではないのでコマンドなどの説明は省略します。

まずMysqlで下記を行います。

- アプリ専用ユーザーの登録
- アプリ専用データベースの登録
- アプリ専用ユーザーにアプリ専用データベースへの権限付与

Projectで扱うDBの設定値は以下です。

| KEY | VALUE |
| :---: | :--- |
| USER | sample_app_user |
|PASSWORD|Pasuwa-do123|
|DRIVER|mysql|
|DB_NAME|sample_app_db|
|HOST|localhost|


### アプリ専用ユーザ・データベースの作成

では、rootユーザーでmysqlにログインして、以下を実行していきます。

```mysql
use mysql;
# 今いるユーザーの一覧をみる、　ここに新しくユーザを追加します。
SELECT user, host FROM user;

# 下記のコマンドでユーザを作成する
# CREATE USER 'ユーザー名'@'ホスト名' IDENTIFIED BY 'パスワード';
CREATE USER 'sample_app_user'@'localhost' IDENTIFIED BY 'Pasuwa-do123';

# 次にサンプルアプリ用のデータベースを作りましょう。
CREATE DATABASE sample_app_db;

# 先ほど作ったユーザはまだ権限を全く持っていません。
# 先ほど作ったユーザーに新しく作成したデータベースの権限を付与します。
# * はすべてという意味です。　
# なのでこの場合は sample_app_dbに対してすべての権限を持っています。
GRANT ALL ON sample_app_db.*  to 'sample_app_user'@'localhost';

# 権限が付与されたか確認しましょう
SHOW GRANTS FOR 'sample_app_user'@'localhost';

# いったんLogoutします。
quit;
```

### Taskテーブルの作成

アプリ専用ユーザーとデータベースが作成できたら、sqlを書いてTaskテーブルを作成してみましょう。

TaskApp/migrations/init_tasks_table.sqlを作成し、初期データも入れちゃいましょう。

```sql:TaskApp/migrations/init_tasks_table.sql
create table sample_app_db.tasks
(
    id     int      not null primary key auto_increment,
    title  char(30) not null,
    status char(10) not null
);


desc sample_app_db.tasks;

INSERT INTO sample_app_db.tasks (title, status) VALUES
('title a', 'todo'),
('title b', 'doing'),
('title c', 'done');

SELECT * from sample_app_db.tasks;
```

では作成したsqlを実行します。

```bash
mysql -u sample_app_user -p < /var/www/project.com/TaskApp/migrations/init_tasks_table.sql
```

結果に下記が表示されていれば成功です。

- desc sample_app_db.tasks;の結果

|Field|Type|Null|Key|Default|Extra|
|---|---|---|---|---|---|
|id|int(11)|NO|PRI|NULL|auto_increment|
|title|char(30)| |NO| |NULL|
|status|char(10)| |NO| |NULL|

- select * from sample_app_db.tasks;の結果

|id|title|status|
|---|---|---|
|1|title a|todo|
|2|title b|doing|
|3|title c|done|


### Config/DBSettings.php
**DBの設定用ファイル**を作成しましょう

```php:Config/DBSettings.php
<?php
namespace Config;


class DBSettings
{
    public const USE_DB = true;
    public const USER = 'sample_app_user';
    public const PASSWORD = 'Pasuwa-do123';
    public const DRIVER = 'mysql';
    public const DB_NAME = "sample_app_db";
    public const HOST = "localhost";
    public const OPTIONS =
        [
            [\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION]
        ];

    public const REPOSITORIES_TABLE =
        [
        ];
}
```

## DBManager
DBの設定が完了したので実際にDBMangerを作成していきます。
**DBとの接続にはPOD**を使います。

DBMangerは以下の機能を持っています。

- DBとの接続
- レポジトリーの登録
- レポジトリーの取得

```php:Libs/DB/DBManger.php
<?php


namespace Libs\DB;


use Config\DBSettings;

class DBManager
{
    private static DBManager $instance;
    private \PDO $connection;
    private array $repository_table = array();

    private function __construct()
    {
        $this->initialize();
    }

    public function __destruct()
    {
        foreach ($this->repository_table as $repository){
            unset($repository);
        }
        unset($this->connection);
    }

    public static function instance()
    {
        if (empty(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * @param $repository_key
     * @return Repository
     */
    public function repository($repository_key)
    {
        return $this->repository_table[$repository_key];
    }

    public function registerRepositories(array $repository_table)
    {
        $this->repository_table = array_merge($this->repository_table, $repository_table);
    }


    private function initialize()
    {
        $dsn = $this->createDsn();

        $this->connection = new \PDO(
            $dsn,
            DBSettings::USER,
            DBSettings::PASSWORD
        );

        foreach(DBSettings::OPTIONS as $option){
            $this->connection->setAttribute($option[0], $option[1]);
        }

        $repositories = array();
        foreach(DBSettings::REPOSITORIES_TABLE as $repo_table){
            $repo = new $repo_table['repository'](
                $repo_table['table_name'],
                $this->connection);
            $repositories[$repo_table['key']] = $repo;
        }

        $this->registerRepositories($repositories);
    }

    private function createDsn(): string
    {
        $dsn = DBSettings::DRIVER . ":" .
            "dbname=" . DBSettings::DB_NAME . ";" .
            "host=" . DBSettings::HOST . ";";
        return $dsn;
    }
}
```

では初期化処理をProject.phpで行いましょう。
**DBManger::instance()** するだけで初期化を行えます

```php:Libs/Project.php
<?php
...

use Libs\DB\DBManager;

class Project
{
    ...

    private function __construct()
    {
        $this->_request = Request::instance();
        $this->_router = new Router(ProjectSettings::ROUTING_TABLE_CLASSES);
        DBManager::instance();
    }
    
    ...
}
```
<http://project.com/tasks/john>　にアクセスし前回と結果に変化がなければ成功です。
ここで driver not foundのエラーが出た場合は mysql driver がphpにインストールされていない状態なので、インストールしましょう。
最初に説明した方法で環境構築を行っている場合は、下記のコマンドでインストールできます。

```bash
yum install -y --enablerepo=remi-php74 php-mysqlnd
```

## Repository / Entity
では次に**Repository**と**Entity**を作成し、TaskAppで確認していきましょう。

まずベースとなるRepositoryクラスとEntityクラスを作成します。

まずEntityクラスを作成します。
Entityクラスは**テーブルのコラムの一覧**を返す機能を持っています。

```php:Libs/DB/Entity.php
<?php
namespace Libs\DB;


abstract class Entity
{
    public string $id;
    public abstract static function columns();
}
```

テーブルの**idコラムがプライマリキーと限定**したクラスになっています。
新しいテーブルを作る際には**idをプライマリキー**として作成してください。

次にRepositoryクラスを作成します。
Repositoryクラスは以下の機能を持っています。

- sqlを実行する機能
- sqlを実行した結果を取得する機能
- テーブルのデータ一覧を取得する機能

最初はこれだけですがこれから、以下の機能を追加しつつTaskAppを完成させていこうと思います。

- フィルター機能
- id指定でデータを取得する機能
- データを追加する機能
- データを更新する機能
- データを削除する機能

```php:Libs/DB/Repository.php
<?php


namespace Libs\DB;


class Repository
{
    protected string $_table_name;
    protected \PDO $_connection;
    private string $_entity_class;

    /**
     * Override this method to return entity class of repository.
     * @return null
     */
    protected function entityClass()
    {
        return null;
    }

    public function __construct($table_name, \PDO $connection, $entity_class = null)
    {
        $this->_table_name = $table_name;
        $this->_connection = $connection;
        $this->_entity_class = is_null($entity_class) ? $this->entityClass() : $entity_class;
        if (is_null($this->_entity_class)) {
            throw new \InvalidArgumentException('$this->entity_class is required not string.');
        }
    }

    public function all()
    {
        $sql = "select * from {$this->_table_name}";
        return $this->fetchAll($sql);
    }
    
    public function execute($sql, $params = [])
    {
        $query = $this->_connection->prepare($sql);
        $query->execute($params);
        return $query;
    }

    public function fetchAll($sql, $params = [])
    {
        $query = $this->execute($sql, $params);
        return $query->fetchAll(\PDO::FETCH_CLASS, $this->_entity_class);
    }
}
```

## TaskApp
では実際にDBを用いてTaskAppを完成させていきましょう。
**TaskAppにCRUDの機能**を搭載していきます。

### Taskの全件表示
まずはTaskを全件表示させてみましょう。

#### レポジトリーとエンティティの登録
ベースとなるRepositoryクラスとEntityクラスを継承した、
**TasksRepositoryクラス**と**Taskクラス**を作成していきます。

```php:TaskApp/Entites/Task.php
<?php
namespace TaskApp\Entities;


use Libs\DB\Entity;

class Task extends Entity
{
    public string $title;
    public string $status;

    public static function columns()
    {
        return ['title', 'status'];
    }
}
```

```php:TaskApp/Repositories/TasksRepository.php
<?php
namespace TaskApp\Repositories;


use Libs\DB\Repository;
use TaskApp\Entities\Task;

class TasksRepository extends Repository
{
    protected function entityClass()
    {
        return Task::class;
    }
}
```

作成した**TasksRepositoryを登録**しましょう。

```php:Config/DBSettings.php
...

public const REPOSITORIES_TABLE =
        [
            [
                'key' => "tasks",
                'table_name' => 'tasks',
                'entity' => \TaskApp\Entities\Task::class,
                "repository" => \TaskApp\Repositories\TasksRepository::class
            ],
        ];
...
```

#### 全件出力処理
ではレポジトリとエンティティの登録が完了したので、
TasksControllerで**Taskの全件データをTemplateに渡して表示**していきましょう。

```php:TaskApp/Controllers/TasksController.php
<?php
...

class TasksController extends Controller
{
    private $_repository;

    public function __construct()
    {
        parent::__construct();
        $this->_repository = \Libs\DB\DBManager::instance()->repository('tasks');
    }

    public function index($params)
    {
        return $this->render('tasks/index', ['tasks' => $this->_repository->all()]);
    }
    
    ...
}

```

TaskApp/RoutingTableを変更します。
ここでついでにDetailのルートも変更します。

```php:TaskApp/RoutingTable.php
<?php

namespace TaskApp;


use TaskApp\Controllers\TasksController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', TasksController::class, 'index'],
        ['int:id', 'GET', TasksController::class, 'detail'],
    ];
}

```

あとはテンプレートを変更して動作確認を行いましょう。

```html:templates/tasks/index.tmp.php
...

<body>
<h1>Tasks</h1>
<ul>
    <?php foreach ($tasks as $task){?>
        <li>
            <a href="/tasks/<?php $escape($task->id) ?>">
                <?php $escape($task->title) ?> : <?php $escape($task->status) ?>
            </a>
        </li>
    <?php } ?>
</ul>
</body>

...
```

では実際にブラウザで確認してみましょう。 <http://project.com/tasks>
下記の内容が表示されていれば成功です。


```text
Tasks
・title a : todo
・title b : doing
・title c : done
```

#### Taskの詳細ページ
全件出力ができるようになったので次は**一つ一つのTaskを表示**できるようにしましょう。

Libs/DB/Repository.phpに以下の機能を追加します。

- フィルター機能
- id指定でデータを取得する機能

```php:Libs/DB/Repository.php
...

class Repository
{
    ...
    
    public function get($id)
    {
        $result = $this->where('id', '=', $id);
        return empty($result[0]) ? null : $result[0];
    }

    public function where($column, $operator, $value)
    {
        $sql = "select * from {$this->_table_name} where {$column} {$operator} :value";
        return $this->fetchAll($sql, [':value' => $value]);
    }

    public function execute($sql, $params = [])
    
    ...
}

```

テンプレートの**詳細ページにTaskの情報**を渡すようにTasksControllerを変更しましょう。

```php:TaskApp/Controllers/TasksController.php
...

class TasksController extends Controller
{
    ...

    public function detail($params)
    {
        $task = $this->_repository->get($params['id']);
        if(is_null($task))
            return $this->render404();
        return $this->render('tasks/detail', ['task' => $task]);
    }
}
```

あとはテンプレートを変更して動作確認を行います。


```html:templates/tasks/detail.tmp.php
...

<body>
<a href="/tasks/"> LIST PAGE</a>
<ul>
    <li>
        Title: <?php $escape($task->title) ?>
    </li>
    <li>
        Status: <?php $escape($task->status) ?>
    </li>
</ul>
</body>

...
```

では実際にブラウザで確認してみましょう。 <http://project.com/tasks>
一覧ページにアクセスしそれぞれのTaskのリンクから詳細ページに行き、
正しい情報が表示されるか確認しましょう。



#### Taskの作成ページ
Taskの一覧の表示と詳細表示ができるようになりましたね、
次は**新しいTaskを作成**できるようにしましょう。

Libs/DB/Repository.phpに以下の機能を追加します。

- データを追加する機能

```php:Libs/DB/Repository.php
...

class Repository
{
    ...

    public function insert(Entity $entity)
    {
        $columns = implode(', ', $this->_columns());
        $values_columns = implode(', ', $this->_to_params($this->_columns()));
        $sql = "insert into {$this->_table_name} ({$columns}) values ({$values_columns})";
        $params = [];
        foreach ($this->_columns() as $key) {
            $params[$this->_to_param($key)] = $entity->$key;
        }
        $this->execute($sql, $params);
    }
    
    public function execute($sql, $params = [])
    ...

    private function _columns(): array
    {
        return $this->_entity_class::columns();
    }

    private function _to_param(string $key): string
    {
        return ':' . $key;
    }

    private function _to_params(array $keys): array
    {
        $result = [];
        foreach ($keys as $key) {
            $result[] = $this->_to_param($key);
        }

        return $result;
    }
}

```
ではTaskApp側を実装していきます。
今回追加するアクションは以下の2個になります。

- create: Taskの追加フォームを表示するアクション
- store:  Taskの追加フォームから送られてきたデータをもとにTaskを追加するアクション

まずTaskAppのRoutingTableで登録しましょう。
**storeアクションがPOSTで登録**されていることに注目してください。

```php:TaskApp/RoutingTable.php
<?php

namespace TaskApp;


use TaskApp\Controllers\TasksController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', TasksController::class, 'index'],
        ['int:id', 'GET', TasksController::class, 'detail'],
        ['', 'POST', TasksController::class, 'store'],
        ['create', 'GET', TasksController::class, 'create'],
    ];
}
```

ではTasksControllerにstoreとcreateを追加します。

```php:TaskApp/Controllers/TasksController.php
<?php
...
use TaskApp\Entities\Task;

class TasksController extends Controller
{
    ...

    public function create($params)
    {
        return $this->render('tasks/create');
    }

    public function store($params)
    {
        $task = new Task();
        $task->title = $this->_request->post('title');
        $task->status = $this->_request->post('status');
        $this->_repository->insert($task);
        return $this->redirect('/tasks/');
    }
}
```

では動作確認をしたいのでcreateテンプレートを作成しましょう。

```php:templates/tasks/create.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<form action="/tasks/" method="POST">

    <ul>
        <li>
            <label>
                Title:
                <input type="text" name="title">
            </label>
        </li>
        <li>
            <label>
                Status:
                <input  type="text" name="status">
            </label>
        </li>
    </ul>

    <input type="submit" value="Add">
</form>
</body>
</html>

```

ついでに一覧ページに作成ページへのリンクも張ってあげましょう

```php:templates/tasks/index.tmp.php
...

<a href="/tasks/create">Add</a>
</body>
...
```

では実際にブラウザで確認してみましょう。 <http://project.com/tasks/create>
実際に入力したデータが一覧ページに追加されるか確認してください。


#### Taskの更新・削除
新しいTaskの作成までできるようになりましたね、
あとは更新と削除だけですね。

Libs/DB/Repository.phpに以下の機能を追加します。

- データを更新する機能
- データを削除する機能

```php:Libs/DB/Repository.php
...

class Repository
{
    ...

    public function update(Entity $entity)
    {
        $sql = "update {$this->_table_name} set ";
        $params = [];
        foreach ($this->_columns() as $column) {
            $key = $this->_to_param($column);
            $sql .= " {$column} = {$key},";
            $params[$key] = $entity->$column;
        }
        $sql = rtrim($sql, ',');
        $sql .= " where id = :id";
        $params[':id'] = $entity->id;
        $this->execute($sql, $params);
    }

    public function delete($id)
    {
        $sql = "delete from {$this->_table_name} where id = :id";
        $this->execute($sql, [':id' => $id]);
    }

    public function execute($sql, $params = [])
    ...
}
```

ではTaskApp側を実装していきます。
今回追加するアクションは以下の3個になります。

- edit: Taskの編集フォームを表示するアクション
- update:  対象のTaskデータを変更するアクション
- delete: 対象のTaskを削除するアクション


まずTaskAppのRoutingTableで登録しましょう。
**updateアクションがPUT**で、**deleteアクションがDELETE**で登録されていることに注目してください。

```php:TaskApp/RoutingTable.php
<?php
namespace TaskApp;


use TaskApp\Controllers\TasksController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', TasksController::class, 'index'],
        ['int:id', 'GET', TasksController::class, 'detail'],
        ['', 'POST', TasksController::class, 'store'],
        ['create', 'GET', TasksController::class, 'create'],
        ['int:id/edit', 'GET', TasksController::class, 'edit'],
        ['int:id', 'PUT', TasksController::class, 'update'],
        ['int:id', 'DELETE', TasksController::class, 'delete'],
    ];
}

```

ではTasksControllerにeditとupdateとdeleteを追加します。

```php:TaskApp/Controllers/TasksController.php
<?php
...

class TasksController extends Controller
{
    ...

    public function edit($params)
    {
        $task = $this->_repository->get($params['id']);
        if(is_null($task))
            return $this->render404();
        return $this->render('tasks/edit', ['task' => $task]);
    }

    public function update($params){
        $task = $this->_repository->get($params['id']);
        if(is_null($task))
            return $this->render404();

        $task->title = $this->_request->post('title');
        $task->status = $this->_request->post('status');

        $this->_repository->update($task);
        return $this->redirect('/tasks/' . $params['id']);
    }

    public function delete($params){
        $this->_repository->delete($params['id']);

        return $this->redirect('/tasks/');
    }
}
```


では動作確認をしたいのでeditテンプレートを作成しましょう。
formのアクションはPOSTですが、form内のhiddenのinputで**メソッドタイプをPUTに指定**しています。
これはRequestクラスのmethodType関数でPOSTされたデータに_methodがある場合はそのメソッドタイプを使用するように設定されています。
HTMLフォームのメソッドタイプはGETかPOSTしか指定できないので **_methodを利用して、メソッドタイプを変更**しましょう。

```php:templates/tasks/edit.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<form action="/tasks/<?php $escape($task->id) ?>" method="POST">
    <input type="hidden" name="_method" value="PUT">
    <ul>
        <li>
            <label>
                Title:
                <input type="text" name="title" value="<?php $escape($task->title) ?>">
            </label>
        </li>
        <li>
            <label>
                Status:
                <input  type="text" name="status" value="<?php $escape($task->status) ?>">
            </label>
        </li>
    </ul>

    <input type="submit" value="Update">
</form>
</body>
</html>

```

次にTaskの詳細ページに編集と削除のリンクを張ります。
削除リンクはフォームを使用していてDELETEのメソッドタイプを送信しています。

```php:templates/tasks/detail.tmp.php
...

<a href="/tasks/<?php $escape($task->id) ?>/edit">EDIT</a>

<form action="/tasks/<?php $escape($task->id) ?>" method="POST">
    <input type="hidden" name="_method" value="DELETE">
    <input type="submit" value="DELETE">
</form>
</body>
```

では実際にブラウザで確認してみましょう。 <http://project.com/tasks/>
一覧ページから一つタスクを選んで編集や削除ができるか確認しましょう。


# STEP8: セッションとユーザー認証
次に**セッションの管理**とセッションを用いた**ユーザー認証機能**の実装を行いたいと思います。

このSTEPで実装・編集するファイルは以下になります。

```text
project.com
      ├─ Config/
      │     ├─ DBSettings.php
      │     └─ ProjectSettings.php
      │
      ├─ Libs/
      │     └─ Apps
      │     │   └─ Auth/
      │     │       ├─ Controllers/
      │     │       │    └─ UserController.php
      │     │       │                          
      │     │       ├─ Entities/
      │     │       │    └─ User.php
      │     │       │                        
      │     │       ├─ migrations/
      │     │       │    └─ create_users_table.sql
      │     │       │                        
      │     │       ├─ Repositories/
      │     │       │    └─ UsersRepository.php
      │     │       │                        
      │     │       ├─ Services/
      │     │       │    └─ UsersService.php
      │     │       │                        
      │     │       ├─ AuthApplication.php
      │     │       └─ RoutingTable.php
      │     │
      │     └─ Https
      │         └─ Session.php     
      │
      └─ templates/
            └─ auth/
                ├─ login.tmp.php
                ├─ my_page.tmp.php
                └─ sign_up.tmp.php
```


## セッション
PHPには[標準の機能にセッションを扱う機能](https://www.php.net/manual/ja/session.examples.basic.php)が提供されています。
今回はそれを用いてセッションの管理をしていきます。
セッションを扱いやすいようにSessionクラスを作成します。

```php:Libs/Https/Session.php
<?php


namespace Libs\Https;


class Session
{

    private static Session $_instance;
    private static bool $is_session_started = false;
    private static bool $is_session_id_regenerated = false;

    private function __construct()
    {
        if (self::$is_session_started) return;

        $this->startSession();
    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

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

    public function get($key, $default = null)
    {
        return isset($_SESSION[$key]) ? $_SESSION[$key] : $default;
    }

    public function unSet($key)
    {
        unset($_SESSION[$key]);
    }

    public function clear()
    {
        $_SESSION = array();
    }

    public function regenerate($destroy = true)
    {
        if (self::$is_session_id_regenerated) return;

        session_regenerate_id($destroy);
        self::$is_session_id_regenerated = true;
    }

    private function startSession(): void
    {
        session_start();
        self::$is_session_started = true;
    }
}
```

動作確認をしたいのでTasksControllerで動作確認を行いましょう

```php:TaskApp/Controllers/TasksController.php
...

    public function detail($params)
    {
        echo \Libs\Https\Session::instance()->get('task_id') . '<br>';
        $task = $this->_repository->get($params['id']);
        \Libs\Https\Session::instance()->set('task_id', $params['id']);
        if(is_null($task))
            return $this->render404();
        return $this->render('tasks/detail', ['task' => $task]);
    }
    
...
```

では実際にアクセスして動作を確認しましょう
まず<http://project.com/tasks/1>にアクセスしましょう。
最後のIDを変更して再度アクセスしましょう。
変更するたびに前回のIDが出力されていれば成功です。

動作確認が完了したのでTasksControllerを変更前の状態に戻しておきましょう。

```php:TaskApp/Controllers/TasksController.php
...

    public function detail($params)
    {
        $task = $this->_repository->get($params['id']);
        if(is_null($task))
            return $this->render404();
        return $this->render('tasks/detail', ['task' => $task]);
    }
    
...
```


## ユーザー認証
セッションの管理ができそうなので、次は**ユーザー認証機能**を追加していきます。
新しくAuthアプリをLibs/Appsに追加していきます。
Authアプリは以下の機能を提供します。

- MyPageの表示
- SignUp機能
- Login機能
- Logout機能

とりあえず、DBにユーザテーブルを登録します。

```sql:Libs/Apps/Auth/migrations/create_users_table.sql
create table sample_app_db.users
(
    id int not null primary key auto_increment,
    name varchar(255) not null unique,
    password varchar(255) not null
)
```

sqlが作成できたので実行しましょう。

```bash
mysql -u sample_app_user -p < /var/www/project.com/Libs/Apps/Auth/migrations/create_users_table.sql
```

あたらしいAppを作るので
**AuthApplicationを作成してプロジェクトに登録**しましょう。
実装しましょう。

```php:Libs/Apps/Auth/AuthApplication.php
<?php
namespace Libs\Apps\Auth;


use Libs\Application;

class AuthApplication extends Application
{

}
```

ProjectSettingsに登録しましょう。

```php:Config/ProjectSettings.php
<?php
...
use Libs\Apps\Auth\AuthApplication;


class ProjectSettings
{
    ...
    public const APPLICATIONS = [
        ConfigApplication::class,
        AuthApplication::class,
        TaskApplication::class
    ];
    
    ...
}
```

### SignUp機能
まずはSignUp機能の実装からやっていきましょう。

#### Userのレポジトリとエンティティ
まずは**Userエンティティとレポジトリ**を作成しましょう。

```php:Libs/Apps/Auth/Entites/User.php
<?php
namespace Libs\Apps\Auth\Entities;


use Libs\DB\Entity;

class User extends Entity
{
    public string $name;
    public string $password;

    public static function columns()
    {
        return ['name', 'password'];
    }
}
```

```php:Libs/Apps/Auth/Repositories/UsersRepository.php
<?php


namespace Libs\Apps\Auth\Repositories;


use Libs\DB\Repository;
use Libs\Apps\Auth\Entities\User;

class UsersRepository extends Repository
{
    protected function entityClass()
    {
        return User::class;
    }

    public function isUniqueName($name)
    {
        $users = $this->where('name', '=', $name);
        return empty($users);
    }

    public function add($name, $password)
    {
        $user = new User();
        $user->name = $name;
        $user->password = password_hash($password, PASSWORD_BCRYPT);
        $this->insert($user);
    }
}
```

UsersRepositoryに**isUniqueName関数とadd関数**を追加しました。
isUniqueNameは**ユーザー名が重複**するのを避けるために使用します。
add関数をわざわざ用意したのは機密情報(ここではユーザのパスワード)をDBで暗号化した状態で扱うためです。
**password_hashというphpの関数**を使用して、**暗号化したパスワードをDBに保存**しています。
ログイン機能の実装の際に暗号化したパスワードとユーザが入力したパスワードの比較方法を説明します。

ではUsersRepositoryができたのでDBSettingsに登録しましょう。

```php:Config/DBSettings.php
...

class DBSettings
{
    ...
    
    public const REPOSITORIES_TABLE =
        [
            [
                'key' => "users",
                'table_name' => 'users',
                "repository" => \Libs\Apps\Auth\Repositories\UsersRepository::class
            ],
            [
                'key' => "tasks",
                'table_name' => 'tasks',
                'entity' => \TaskApp\Entities\Task::class,
                "repository" => \TaskApp\Repositories\TasksRepository::class
            ],
        ];
}
```

#### AuthService
認証の機能はいろんなところで使用されることが予想されるのでAuthServiceを作成しましょう。

この時点でのAuthServiceは**新規ユーザを作成する機能**を実装します。

```php:Libs/Apps/Auth/Services/AuthService.php
<?php


namespace Libs\Apps\Auth\Services;


use Libs\DB\DBManager;

class AuthService
{
    public static function addNewUser($name, $password)
    {
        $errors = [];
        if (self::_repository()->isUniqueName($name) === false) {
            $errors['name'] = "'{$name}'' is already exists.";
        };

        if (strlen($password) < 8) {
            $errors['password'] = "Password required at least 8 letters.";
        }

        if (count($errors) === 0)
            self::_repository()->add($name, $password);
        return $errors;
    }

    protected static function _repository()
    {
        return DBManager::instance()->repository('users');
    }

}
```

addNewUser関数はユーザーネームが重複してないこと、パスワードが8文字以上であることをValidationしてます。Validationが成功したら、新しいユーザを作成します。

ではUserControllerを作成してユーザの登録をしてきましょう。

```php:Libs/Apps/Auth/Controllers/UserController.php
<?php


namespace Libs\Apps\Auth\Controllers;


use Libs\Controllers\Controller;
use Libs\Apps\Auth\Services\AuthService;

class UserController extends Controller
{
    public function myPage($params)
    {
        echo '<pre>';
        var_dump(\Libs\DB\DBManager::instance()->repository('users')->all());
        echo '</pre>';
        return new \Libs\Https\Response('');
    }

    public function signUpForm($params)
    {
        return $this->render('auth/sign_up');
    }

    public function signUp($params)
    {
        $errors = AuthService::addNewUser($this->_request->post('name'), $this->_request->post('password'));
        if (count($errors) === 0)
            return $this->redirect('/auth');
        return $this->render('auth/sign_up', ['errors' => $errors]);
    }
}
```

myPageは動作確認するために登録したユーザー全件を出力してます。
新規Task作成と同様、**ユーザ登録フォームの表示アクション**と、**ユーザ登録アクション**の二つを実装しました。

ではRoutingTableを作成し登録しましょう。

```php:Libs/Apps/Auth/RoutingTable.php
<?php


namespace Libs\Apps\Auth;


use Libs\Apps\Auth\Controllers\UserController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', UserController::class, 'myPage'],
        ['sign-up', 'GET', UserController::class, 'signUpForm'],
        ['sign-up', 'POST', UserController::class, 'signUp'],
    ];
}
```

```php:Config/ProjectSettings.php
...

class ProjectSettings
{
    ...

    public const ROUTING_TABLE_CLASSES = [
        ['/^tasks(\/|)/', \TaskApp\RoutingTable::class],
        ['/^auth(\/|)/', \Libs\Apps\Auth\RoutingTable::class],
    ];
    
    ...
}
```

あとはテンプレートを作成して動作確認を行いましょう。

```php:templates/auth/sign_up.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<?php if(empty($errors) === false){ ?>
    <ul>
        <?php foreach($errors as $key => $message){ ?>
            <li> <?php $escape($message) ?></li>


        <?php } ?>
    </ul>
<?php } ?>

<form action="/auth/sign-up" method="POST">

    <ul>
        <li>
            <label>
                User Name:
                <input type="text" name="name">
            </label>
        </li>
        <li>
            <label>
                Password:
                <input  type="text" name="password">
            </label>
        </li>
    </ul>

    <input type="submit" value="SignUp">
</form>
<a href="/auth/login">Login</a>
</body>
</html>
```


では実際にブラウザで確認してみましょう。 <http://project.com/auth/sign-up>
新規のユーザーを登録し、[Document](http://project.com/auth)に新しいユーザが表示されていれば成功です。


### Login・Logout機能
ユーザーを登録する機能ができたら次は**ログイン・ログアウト**ですよね。
ここから**セッション**を使用していきます。

AuthServiceに以下の機能を追加します。

- ログインする機能
- ログインしてるか確認する機能
- ログアウトする機能
- ログインしているユーザーを取得する機能
- ユーザー名からユーザを取得する機能

```php:Libs/Apps/Auth/Services/AuthService.php
<?php


namespace Libs\Apps\Auth\Services;


use Libs\Apps\Auth\Entities\User;
use Libs\DB\DBManager;
use Libs\Https\Session;

class AuthService
{
    const AUTHENTICATED_KEY = '_authenticated';
    const AUTH_ID_KEY = '_auth_id';

    public static function getLoginUser($is_secure = true)
    {
        $user =  self::_repository()->get(self::_session()->get(self::AUTH_ID_KEY));
        if (empty($user))
            return $user;
        if($is_secure === false)
            return $user;
        $user->password = '';
        return $user;
    }

    public static function getUser($name)
    {
        $result = self::_repository()->where('name', '=', $name);
        return empty($result[0]) ? null : $result[0];
    }

    public static function login(User $user, $password)
    {
        if (password_verify($password, $user->password) === false) {
            return false;
        }

        self::_session()->set(self::AUTHENTICATED_KEY, true);
        self::_session()->set(self::AUTH_ID_KEY, $user->id);
        self::_session()->regenerate();
        return true;
    }

    public static function logout()
    {
        self::_session()->set(self::AUTHENTICATED_KEY, false);
        self::_session()->set(self::AUTH_ID_KEY, false);
        self::_session()->regenerate();
    }

    public static function isAuthenticated()
    {
        return self::_session()->get(self::AUTHENTICATED_KEY) === true;
    }

    public static function addNewUser($name, $password)
    {
        $errors = [];
        if (self::_repository()->isUniqueName($name) === false) {
            $errors['name'] = "'{$name}'' is already exists.";
        };

        if (strlen($password) < 8) {
            $errors['password'] = "Password required at least 8 letters.";
        }

        if (count($errors) === 0)
            self::_repository()->add($name, $password);
        return $errors;
    }

    protected static function _repository()
    {
        return DBManager::instance()->repository('users');
    }

    protected static function _session()
    {
        return Session::instance();
    }


}
```

**login関数**を見てみましょう。
**暗号化したパスワードを比較するにはpassword_verify関数**を使用します。
これで実際に入力されたパスワードが暗号化されたパスワードとの比較を行います。
比較に成功した場合はセッションに以下の二つをセットします。

- 認証に成功しているよー状態
- 認証に成功したユーザーのID

ユーザを取得する関数は**セッションにセットされているIDを使用**して取得してきます。

logout関数に関してはそれぞれのセッションにfalseを入れてしまえばそれでlogoutになります。


では、UserControllerに以下のアクションを追加して、ルートを登録しましょう。

- ログインしているユーザーのマイページを表示(名前だけ)するアクション
- ログインフォームを表示するアクション
- ログインするアクション
- ログアウトするアクション

```php:Libs/Apps/Auth/Controller/UserController.php
<?php
namespace Libs\Apps\Auth\Controllers;


use Libs\Controllers\Controller;
use Libs\Apps\Auth\Services\AuthService;

class UserController extends Controller
{

    public function myPage($params)
    {
        if (AuthService::isAuthenticated() === false)
            return $this->redirect('/auth/login');
        return $this->render(
            'auth/my_page',
            ['user' => AuthService::getLoginUser()]);
    }

    public function signUpForm($params)
    {
        return $this->render('auth/sign_up');
    }

    public function signUp($params)
    {
        $errors = AuthService::addNewUser($this->_request->post('name'), $this->_request->post('password'));
        if (count($errors) === 0)
            return $this->redirect('/auth/login');
        return $this->render('auth/sign_up', ['errors' => $errors]);
    }

    public function loginForm($params)
    {
        return $this->render('auth/login');
    }

    public function login($params)
    {
        $failed_result = $this->render('auth/login', ['error' => 'Name or password is correct.']);
        $user = AuthService::getUser($this->_request->post('name'));
        if (is_null($user))
            return $failed_result;

        if (AuthService::login($user, $this->_request->post('password')))
            return $this->redirect('/auth/');

        return $failed_result;
    }

    public function logout($params)
    {
        AuthService::logout();
        return $this->redirect('/auth/login');
    }

}
```

```php:Libs/Apps/Auth/RoutingTable.php
<?php
namespace Libs\Apps\Auth;


use Libs\Apps\Auth\Controllers\UserController;

class RoutingTable extends \Libs\Routing\RoutingTable
{
    protected array $urlPatterns = [
        ['', 'GET', UserController::class, 'myPage'],
        ['sign-up', 'GET', UserController::class, 'signUpForm'],
        ['sign-up', 'POST', UserController::class, 'signUp'],
        ['login', 'GET', UserController::class, 'loginForm'],
        ['login', 'POST', UserController::class, 'login'],
        ['logout', 'GET', UserController::class, 'logout'],
    ];
}
```

あとはテンプレート追加して動作確認をしましょう。

```php:templates/auth/my_page.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
User: <?php $escape($user->name)?>
<a href="/auth/logout">Logout</a>
</body>
</html>

```

```php:templates/auth/login.tmp.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<?php if(empty($error) === false){  $escape($error); }?>

<form action="/auth/login" method="POST">

    <ul>
        <li>
            <label>
                User Name:
                <input type="text" name="name">
            </label>
        </li>
        <li>
            <label>
                Password:
                <input  type="text" name="password">
            </label>
        </li>
    </ul>

    <input type="submit" value="Login">
</form>
<a href="/auth/sign-up">Sign Up</a>
</body>
</html>
```

では実際にブラウザで確認してみましょう。 <http://project.com/auth>
ログインしてない状態でマイページにアクセスするとログインページにリダイレクトします。
ログインページで先ほど作成したユーザでログインしてください。
ログインに成功するとマイページにリダイレクトしてユーザー名が表示されれば成功です。
ログアウトして再度<http://project.com/auth>にアクセスしログインページにリダイレクトされればログアウト成功です。


# STEP9: Middleware
最後に**Middleware機能**の追加を行いましょう。
Middleware機能を追加して、ユーザ認証していなければアクセスできないようにするなどを実装していきたいと思います。

このSTEPで実装・編集するファイルは以下になります。


```text
project.com
      ├─ Config/
      │     ├─ ConfigApplication.php
      │     └─ ProjectSettings.php
      │
      └─ Libs/
            ├─ Apps
            │   └─ Auth/
            │       ├─ Controllers/
            |       |   └─ UserController.php
            |       |
            │       └─ Middleware/
            │           ├─ AuthMiddleware.php
            |           └─ RequiredAuthenticationMiddleware.php
            |
            ├─ Https/
            │   └─ Request.php
            │
            ├─ Middleware/
            │   ├─ BaseMiddleware.php
            │   └─ MiddlewareManager.php
            │
            └─ Project.php
```


## Middleware実装
ではMiddlewareの実装を行っていきます。
ここでのMiddlewareには以下の機能を提供します。

- コントローラのアクションを実行する前に、リクエストを受け取って何かしらの処理をする。
- HTTPレスポンスを返す前にレスポンスを受け取って何かしらの処理をする。

まずベースとなるMiddlewareを作成しましょう。

```php:Libs/Middleware/BaseMiddleware.php
<?php


namespace Libs\Middleware;


use Libs\Https\Request;
use Libs\Https\Response;

class BaseMiddleware
{
    protected bool $_have_to_return_response_immediately = false;

    /**
     * Every request pass here.
     * You can edit request in here like set request->user.
     *
     * Return request if you want continue.
     * Return response if you want return response immediately like 401 response.
     *
     * @param Request $request
     * @return Request|Response
     */
    public function processRequest(Request $request){
        return $request;
    }

    /**
     * Every responses pass here.
     * You can edit response in here like set cookie header.
     *
     * Set true to $this->_have_to_return_response_immediately if you want return response immediately.
     *
     * @param Response $response
     * @return Response
     */
    public function processResponse(Response $response){
        return $response;
    }

    public function haveToReturnResponse(){
        return $this->_have_to_return_response_immediately;
    }
}
```

Middlewareを登録する場合は**このクラスを継承したMiddlewareを登録**します。

- processRequest関数: 
  - リクエスト受け取り何かしらの処理をする関数です。
  - 何も問題がなければリクエストを返却してください。
  - 問題が発生し(ログインしてないなど)すぐにレスポンスを返したい場合は、レスポンスを返却するという仕様です。
- processResponse関数: 
  - レスポンスを受け取り何かしらの処理をする関数です。
  - 何も問題がなければレスポンスを返却してください。
  - 何か問題が発生した場合は$_have_to_return_resonse_immediatelyをtrueにしてレスポンスを返却するという仕様です。


上記の仕様を踏まえて、MiddlewareManagerクラスを作成しましょう。

```php:Libs/Middleware/MiddlewareManager.php
<?php


namespace Libs\Middleware;


use Libs\Https\Response;

class MiddlewareManager
{
    private array $_middleware_list = [];

    public function __construct($middleware_list)
    {
        foreach ($middleware_list as $middleware) {
            $this->_middleware_list[] = new $middleware;
        }
    }

    public function processRequest($request)
    {
        $result = $request;
        foreach ($this->_middleware_list as $middleware) {
            $result = $middleware->processRequest($result);
            if ($result instanceof Response)
                break;
        }

        return $result;
    }

    public function processResponse($response) : Response
    {
        $result = $response;
        foreach (array_reverse($this->_middleware_list) as $middleware) {
            $result = $middleware->processResponse($result);
            if ($middleware->haveToReturnResponse())
                break;
        }

        return $result;
    }

}
```

では作成したMiddlewareManagerをProject.phpを編集して機能するようにします。

```php:Config/ProjectSettings.php
class ProjectSettings
{
    ...
    public const MIDDLEWARE_CLASSES = [];
    
    public const ROUTING_TABLE_CLASSES = [
    ...
```

```php:Libs/Project.php
<?php


namespace Libs;


use Config\ProjectSettings;
use Libs\Controllers\Controller;
use Libs\DB\DBManager;
use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Middleware\MiddlewareManager;
use Libs\Routing\Router;

/**
 * Class Project
 * @package Libs
 */
class Project
{
    private static Project $_instance;
    private Request $_request;
    private Router $_router;
    private MiddlewareManager $middleware_manager;

    private function __construct()
    {
        DBManager::instance();
        $this->_request = Request::instance();
        $this->_router = new Router(ProjectSettings::ROUTING_TABLE_CLASSES);
        $this->middleware_manager = new MiddlewareManager(ProjectSettings::MIDDLEWARE_CLASSES);

    }

    public static function instance()
    {
        if (empty(self::$_instance)) {
            self::$_instance = new Project();
        }

        return self::$_instance;
    }

    public function run()
    {
        $result = $this->middleware_manager->processRequest($this->_request);
        if ($result instanceof Response) {
            $result->send();
            return;
        }
        list($controller, $action, $params) = $this->_selectController();
        $response = $this->_actionController($controller, $action, $params);
        $response = $this->middleware_manager->processResponse($response);
        $response->send();
    }

    private function _selectController()
    {
        $result = $this->_router->resolve($this->_request);
        if (is_null($result)) {
            $controller = ProjectSettings::NOT_FOUND_CONTROLLER;
            return [new $controller(), 'index', []];
        }
        return [new $result['class'], $result['action'], $result['params']];
    }

    private function _actionController(Controller $controller, string $action, array $params)
    {
        return $controller->run($action, $params);
    }
}
```

run関数内部で**コントローラーのアクションを選択する前にMiddlewareのリクエストプロセスを実行**します。
**アクション実行後Middlewareのレスポンスプロセスを実行**します。

実際に動作確認をしてみましょう。
動作確認用のサンプルMiddlewareを作成します。

```php:Libs/Apps/Auth/Middleware/MiddlewareA.php
<?php


namespace Libs\Apps\Auth\Middleware;


use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Middleware\BaseMiddleware;

class MiddlewareA extends BaseMiddleware
{

    public function processRequest(Request $request){
        echo "Request pass the Middleware A. <br><br>";
        return $request;
    }

    public function processResponse(Response $response){
        echo "Response pass the Middleware A. <br><br>";
        return $response;
    }
}
```

```php:Libs/Apps/Auth/Middleware/MiddlewareB.php
<?php


namespace Libs\Apps\Auth\Middleware;


use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Middleware\BaseMiddleware;

class MiddlewareB extends BaseMiddleware
{

    public function processRequest(Request $request){
        echo "Request pass the Middleware B. <br><br>";
        return $request;
    }

    public function processResponse(Response $response){
        echo "Response pass the Middleware B. <br><br>";
        return $response;
    }
}
```
作製したMiddlewareを登録します。

```php:Config/ProjectSettings.php
class ProjectSettings
{
    ...
    public const MIDDLEWARE_CLASSES = [
        \Libs\Apps\Auth\Middleware\MiddlewareA::class,
        \Libs\Apps\Auth\Middleware\MiddlewareB::class,
    ];
    ...
```

では、実際に動作確認をしましょう <http://project.com/>
以下の結果が表示されていれば成功です。

```text
Request pass the Middleware A.

Request pass the Middleware B.

Response pass the Middleware B.

Response pass the Middleware A.

Page not found.
```

動作を見てもらえばわかる通り、リクエストは登録されている**上からの順番**で、レスポンスを**下からの順番**で実行されます。

動作確認が完了したらProjectSettings.phpのMIDDLEWARE_CLASSESを空配列に戻しておきましょう。
あとサンプルで作成したMiddlewareAとMiddlewareBを削除しておきましょう。

```php:Config/ProjectSettings.php
class ProjectSettings
{
    ...
    public const MIDDLEWARE_CLASSES = [];
    ...
```


## Requestにユーザを引っ付ける
DjangoやLaravelでユーザを取得する方法の一つに

```php
$request->user()
```

や

```python
request.user
```

があると思います。
これをできるようにMiddlewareで**リクエストにuser**を引っ付けちゃいましょう。

AuthMiddlewareを作成して登録します。

```php:Libs/Apps/Auth/Middleware/AuthMiddleware.php
<?php
namespace Libs\Apps\Auth\Middleware;


use Libs\Https\Request;
use Libs\Middleware\BaseMiddleware;
use Libs\Apps\Auth\Services\AuthService;

class AuthMiddleware extends BaseMiddleware
{
    public function processRequest(Request $request){
        $request->user = AuthService::getLoginUser();
        return $request;
    }
}
```

```php:Config/ProjectSettings.php
    ...
    
    public const MIDDLEWARE_CLASSES = [
        \Libs\Apps\Auth\Middleware\AuthMiddleware::class,
    ];
    
    ...
```


では実際に動作確認をしましょう。
UserControllerのmyPage関数内でAuthService::getLoginUser関数を使用しているところをリクエストから取得するように変更します。

```php:Libs/Apps/Auth/Controllers/UserController.php
...

class UserController extends Controller
{

    public function myPage($params)
    {
        if (AuthService::isAuthenticated() === false)
            return $this->redirect('/auth/login');
        return $this->render(
            'auth/my_page',
            ['user' => $this->_request->user]);
    }
    
    ...
```

ではログインして動作確認をしましょう。 <http://project.com/auth/>
以前と同じようにユーザー名が出力されれば成功です。

## ユーザー認証制限
**ユーザの認証がない状態でアクセスしたらログインページにリダイレクト**するMiddlewareを実装していきます。
RequiredAuthenticationMiddlewareを作成し登録しましょう。

```php:Libs/Apps/Auth/Middleware/RequiredAuthenticationMiddleware.php
<?php


namespace Libs\Apps\Auth\Middleware;


use Libs\Https\Request;
use Libs\Https\Response;
use Libs\Middleware\BaseMiddleware;
use Libs\Apps\Auth\Services\AuthService;

class RequiredAuthenticationMiddleware extends BaseMiddleware
{
    public static array $IGNORE_URL_PATTERNS = ['/^auth\/(login|sign-up)$/'];
    private array $_ignore_url_patterns;

    public function __construct()
    {
        $this->_ignore_url_patterns = self::$IGNORE_URL_PATTERNS;
    }

    public function processRequest(Request $request)
    {
        if (AuthService::isAuthenticated())
            return $request;
        foreach ($this->_ignore_url_patterns as $ignore_url_pattern) {
            if (preg_match($ignore_url_pattern, $request->pathInfo()))
                return $request;
        }
        return Response::redirect('/auth/login');
    }
}
```

**IGNORE_URL_PATTERNSにはユーザ認証なしでもアクセスできるパスの正規表現**パターンを設定します。
初期設定ではログイン画面と登録画面のみ制限を解除しています。

では登録して、アクセスしてみましょう。

```php:Config/ProjectSettings.php
    ...
    
    public const MIDDLEWARE_CLASSES = [
        \Libs\Apps\Auth\Middleware\AuthMiddleware::class,
        \Libs\Apps\Auth\Middleware\RequiredAuthenticationMiddleware::class,
    ];
    
    ...
```

では実際に動作確認を行います。まずログアウトしてください。 <http://project.com/auth/logout>
その後Taskの一覧ページにアクセスして、ログインページに飛ばされることを確認しましょう。  <http://project.com/tasks/>
次はログインしてもう一度Taskの一覧ページにアクセスして一覧が表示されれば成功です。


### IGNORE_URL_PATTERNSの設定
IGNORE_URL_PATTERNSに登録するとそのページはログインなしでアクセスすることができるようになります。
Taskの一覧ページを見れるように、ConfigApplicationのready関数でIGNORE_URL_PATTERNSを拡張しましょう。

```php:Config/ConfigApplication.php
<?php
namespace Config;


use Libs\Application;

class ConfigApplication extends Application
{
    public function ready()
    {
        \Libs\Apps\Auth\Middleware\RequiredAuthenticationMiddleware::$IGNORE_URL_PATTERNS[] = '/^tasks\/$/';
    }
}
```

では実際に動作確認を行います。まずログアウトしてください。 <http://project.com/auth/logout>
その後Taskの一覧ページにアクセスして、一覧が表示されることを確認しましょう。  <http://project.com/tasks/>
Taskの詳細ページアクセスするとログインページに飛ばされることが確認できれば成功です。



# まとめ
以上で自作　MVC WebFrameworkの完成です。
最後まで一緒に頑張ってくれた方ありがとうございます。
だんだん疲れてきて雑になっているかもしれませんがご容赦ください。

あくまでも最低限の機能を学ぶためのWebFrameworkですので、セキュリティー面などはほぼ考慮してないです。
このフレームワークを使って開発したものを公開するのはやめといたほうがいいと思います。自分ならしませんｗ。
ただ自分で作ったフレームワークを拡張してアプリを作るはとても楽しいです。
良ければ拡張しながら遊んでってもらえばなーと思います。

自分が拡張した例を紹介しますね。

- ViewにFormViewを追加した
- TemplateシステムにLayoutを追加した。Layoutをextendsして扱えるということです。
- DIを実装、これは昔C# でやってみたことがあってPHPでもやりたかったのでやりましたｗ
- ORマッパー的な奴、難しくてあまりうまいことできなかったです。


今度実装したDIパターンとかまとめてみようかなと思います。

完成したレポジトリのリンクを張っておきます。
developブランチで確認してください。
[レポジトリ](https://github.com/Saknowman/study_php_web_framework)

長い記事でしたがお付き合いただきありがとうございました！！！