【PHPデザインパターン】21_State～状態を表す

# 引用記事
この記事を書くきっかけになったブログです。

記事内の解説やソースコードは、こちらのブログと著者の公開リポジトリを参考にしています。

> [Do You PHP はてな〜[doyouphp][phpdp]PHPによるデザインパターン入門 - State～状態を表す](http://d.hatena.ne.jp/shimooka/20141219/1418965549)

# 概要
- 「state」は「状態」という意味。
- 「状態」と「状態による振る舞い」を1つのクラスにまとめる。
- クラス内では振る舞いの変化を記述せず、状態を表すオブジェクトを導入することでこれを実現する。
- 状態に固有の処理をまとめることができる。

## 構成要素
### Stateクラス
- それぞれの状態に共通のAPIを定義するクラス。
- 状態固有の振る舞いを行うメソッドを定義する。

### ConcreteStateクラス
- Stateクラスのサブクラス。
- 状態ごとの具体的な処理内容を記述する。

### Contextクラス
- State型のオブジェクトを内部に保持し、具体的な処理をそのオブジェクトに委譲するクラス。

# 実演
## 処理の流れ
- ユーザーのログイン状況により、表示メニューを変更する。
- ログオフ状態であれば、``UnauthorizedStateクラス``のメソッドでログインへのリンクを表示する。
- ログイン状態であれば、``AuthorizedStateクラス``のメソッドでカウンタとログオフへのリンクを表示する。
- GETメソッドのパラメータにより、メッセージを変更する。
- ``User型インスタンス``をセッションとして持たせる。表示メニューのメソッドは``Userクラス``で定義する。

## ファイル構造
```sh
MyState
  ├── AuthorizedState.php
  ├── UnauthorizedState.php
  ├── User.php
  ├── UserState.php
  └── my_client.php
```

## ソースコード
### Stateクラス
#### UserState.php
```UserState.php
<?php
namespace DoYouPhp\PhpDesignPattern\State\MyState;

/**
 * Stateクラスに相当する
 * 状態毎の動作・振る舞いを定義する
 */
interface UserState
{
    public function isAuthenticated();
    public function nextState();
    public function getMenu();
}
```

### ConcreteStateクラス
#### UnauthorizedState.php
```UnauthorizedState.php
<?php
namespace DoYouPhp\PhpDesignPattern\State\MyState;

use DoYouPhp\PhpDesignPattern\State\MyState\UserState;
use DoYouPhp\PhpDesignPattern\State\MyState\AuthorizedState;


/**
 * ConcreteStateクラスに相当する
 * 未認証の状態を表すクラス
 */
class UnauthorizedState implements UserState
{
    private static $singleton = null;

    private function __construct()
    {
    }

    // UnauthorizedState型インスタンスが存在しない場合のみ、生成する
    // 生成済みの場合は、UnauthorizedState型インスタンスを返す
    public static function getInstance()
    {
        if (self::$singleton === null) {
            self::$singleton = new UnauthorizedState();
        }

        return self::$singleton;
    }

    public function isAuthenticated()
    {
        return false;
    }

    // 次の状態（ログイン状態）を返す
    public function nextState()
    {
        return AuthorizedState::getInstance();
    }

    // 状態を移行するためのリンクを生成する
    // GETメソッドでパラメータを渡す
    public function getMenu()
    {
        $menu = '<a href="?mode=state">ログイン</a>';

        return $menu;
    }

    // インスタンスの複製を許可しないようにする
    final public function __clone()
    {
        throw new \RuntimeException(get_class($this).'型インスタンスのコピーはできません');
    }
}
```
 
#### AuthorizedState.php
```AuthorizedState.php
<?php
namespace DoYouPhp\PhpDesignPattern\State\MyState;

use DoYouPhp\PhpDesignPattern\State\MyState\UserState;
use DoYouPhp\PhpDesignPattern\State\MyState\UnauthorizedState;


/**
 * ConcreteStateクラスに相当する
 * 認証後の状態を表すクラス
 */
class AuthorizedState implements UserState
{
    private static $singleton = null;

    private function __construct()
    {
    }

    // AuthorizedState型インスタンスが存在しない場合のみ、生成する
    // 生成済みの場合は、AuthorizedState型インスタンスを返す
    public static function getInstance()
    {
        if (self::$singleton == null) {
            self::$singleton = new AuthorizedState();
        }

        return self::$singleton;
    }

    public function isAuthenticated()
    {
        return true;
    }

    // 次の状態（ログオフ状態）を返す
    public function nextState()
    {
        return UnauthorizedState::getInstance();
    }

    // 状態を移行するためのリンクを生成する
    // ログイン状態でのみ生存するカウンタ・リセット機能を生成する
    // GETメソッドでパラメータを渡す
    public function getMenu()
    {
        $menu = '<a href="?mode=inc">カウントアップ</a> | '
              .'<a href="?mode=reset">リセット</a> | '
              .'<a href="?mode=state">ログアウト</a>';

        return $menu;
    }

    // インスタンスの複製を許可しないようにする
    final public function __clone()
    {
        throw new \RuntimeException(get_class($this).'型インスタンスのコピーはできません');
    }
}
```

### Contextクラス
#### User.php
```User.php
<?php
namespace DoYouPhp\PhpDesignPattern\State\MyState;

use DoYouPhp\PhpDesignPattern\State\MyState\UnauthorizedState;


/**
 * Contextクラスに相当する
 */
class User
{
    private $name;
    private $state;
    private $count = 0;

    // ユーザー名をプロパティにセットする
    public function __construct($name)
    {
        $this->name = $name;

        // 初期状態（ログオフ状態）のセット
        $this->state = UnauthorizedState::getInstance();
        // ログオフ状態はカウンタが使用されないため、カウンタをゼロにセット
        $this->resetCount();
    }

    // 状態を切り替える
    // ログイン状態・ログオフ状態を行き来する
    public function switchState()
    {
        echo "状態遷移:".get_class($this->state)."から";
        $this->state = $this->state->nextState();
        echo get_class($this->state).'へ<br>'."\n";

        // カウンタをゼロにセット
        $this->resetCount();
    }

    // ログイン状態かどうか確認する
    public function isAuthenticated()
    {
        return $this->state->isAuthenticated();
    }

    // 状態を移行するためのリンクを生成する
    public function getMenu()
    {
        return $this->state->getMenu();
    }

    public function getUserName()
    {
        return $this->name;
    }

    public function getCount()
    {
        return $this->count;
    }

    public function incrementCount()
    {
        $this->count++;
    }

    public function resetCount()
    {
        $this->count = 0;
    }

    // インスタンスの複製禁止を確認するためのメソッド
    public function copyInstance()
    {
        clone $this->state;
    }
}
```

### Client
#### my_client.php
```my_client.php
<?php
namespace DoYouPhp\PhpDesignPattern\State\MyState;

require dirname(dirname(__DIR__)).'/vendor/autoload.php';

use DoYouPhp\PhpDesignPattern\State\MyState;

    // セッションスタート
    session_start();

    // セッションがあればセットする
    $context = isset($_SESSION['context']) ? $_SESSION['context'] : null;
    // セッションがなければユーザーを新規作成する
    if (is_null($context)) {
        $context = new User('Taro');
    }

    // セッションがあればセットし、メッセージを変化させる
    $mode = (isset($_GET['mode']) ? $_GET['mode'] : '');
    switch ($mode) {
    case 'state':
        echo '<p style="color: #aa0000">状態を遷移します</p>';
        $context->switchState();
        break;
    case 'inc':
        echo '<p style="color: #008800">カウントアップしました</p>';
        $context->incrementCount();
        break;
    case 'reset':
        echo '<p style="color: #008800">カウントをリセットします</p>';
        $context->resetCount();
        break;
    }

    $_SESSION['context'] = $context;

    echo 'ようこそ'.$context->getUserName().'さん<br>'."\n";
    echo '現在、ログインして'.($context->isAuthenticated() ? 'います' : 'いません').'<br>'."\n";
    echo '現在のカウント：'.$context->getCount().'<br>'."\n";
    echo $context->getMenu().'<br>'."\n";

    // インスタンスの複製ができないことを確認する
    try {
        clone $context->copyInstance();
    } catch (\RuntimeException $e) {
        echo $e->getMessage();
    }
```