明滅するプログラマの思索

WEBエンジニアとして勤務している一介の男が、日々気づいたことをまとめるブログです

PHP でウィザード形式のページ遷移を実現する trait

Wizard とは

ウィザードとは、対話形式で遷移を踏みつつ処理を進めさせるためのユーザインターフェイスのことです。
ウェブでは、複数ページにまたがるフォームなどで実装されています。

ウィザードを実装するためには、遷移途中の入力データや今どのページを参照しているのかのステップ情報を保持しておいて、最終的にデータベースなどにコミットするような仕組みを用意する必要があります。
今回はウィザードを簡単に実装するために PHP の trait で実装してみました。

実装条件

ソフトウェア バージョン
PHP 5.6.30

入力データの保持に SESSION を利用しています。
また、サンプルコードで HTML テンプレートを扱うため、Smarty を利用しています。

まずは使い方

今回作成した trait を利用したウィザードの運用コードを先に載せます。

<?php
class Form
{
  use \Wizard;

  // $steps に記述した値と同じ名称のメソッドをすべて用意する
  protected $steps = [
    'step1',
    'step2',
    'complete'
  ];

  public function step1()
  {
    if (@$_GET['m'] == 'next') {
      $this->nextStep();
      exit;
    }
    $this->smarty->assign('step', $this->getCurrentStep());
    $this->smarty->display("step1.html");
  }

  public function step2()
  {
    if ($this->checkPrev() === false) {
      exit;
    }
    if ($this->step_moved === false) {
      switch (@$_GET['m']) {
        case 'back':
          $this->backStep();
          break;
        case 'next':
          $this->set('date', date('Y-m-d H:i:s'));
          $this->nextStep();
          break;
      }
    }
    $this->smarty->display("step2.html");
  }

  public function complete()
  {
    if ($this->checkPrev() === false) {
      exit;
    }

    $sql = 'INSERT INTO form_table (create_date) VALUES(:create_date)';
    $sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
    $sth->execute([':create_date' => $this->get('date')]);
    $this->smarty->display("complete.html");
  }
}

まず Form クラスは Wizard トレイトを use しています。
ウィザードで遷移する各ページには、それぞれ1つずつメソッドを用意して実装します。
そしてそれぞれのメソッド名を protected $steps に配列で格納します。
各ページの遷移する順番は $steps に格納された順となります。

  protected $steps = [
    'step1',
    'step2',
    'complete'
  ];

上記であれば step1 → step2 → complete という順に遷移します。
つまりそれは、step1() メソッド→ step2() メソッド→ complete() メソッドの順に実行されることを意味しています。
そしてこの Form クラスのインスタンスを作成し、run() メソッドをコールすれば、ウィザードが開始されます。

<?php
$form = new Form();
$form->run();

run() メソッドをコールすると、まず最初に step1() メソッドが実行されます。
step1.html の HTMLテンプレートは以下のようにしておきます。

<html>
<body>
<a href="./?m=next">次へ</a>
</body>
</html>

「次へ」リンクにより、パラメータ m=next が渡されます。
step1() メソッドは m=next を受け取ると nextStep() メソッドを実行します。その結果、即座に step2() メソッドをコールできます。

step2.html の HTMLテンプレートは以下のようにしておきます。

<html>
<body>
<a href="./?m=back">戻る</a>
<a href="./?m=next">次へ</a>
</body>
</html>

このとき、現在のステップは step1 から step2 に移動しています。
そのため、「戻る」リンクでは backStep() が実行され step1() メソッドをコールし、「次へ」リンクでは nextStep() が実行され complete() メソッドをコールします。

complete() メソッドが実行されると、DBにSQLクエリを投げて complete.html を表示してウィザードは終了します。

メソッド一覧

Wizard トレイトを use したクラスで利用可能になるメソッドは以下の通りです。

nextStep()

次のステップに遷移する

Boolean nextStep( $run = true )
  • $run
    • 遷移後、そのステップのメソッドを実行するかどうか
    • boolean (true : 実行する / false : 実行しない)
    • 省略可(省略時は true)

backStep()

1つ前のステップに遷移する

Boolean backStep( $run = true )
  • $run
    • 遷移後、そのステップのメソッドを実行するかどうか
    • boolean (true : 実行する / false : 実行しない)
    • 省略可(省略時は true)

checkPrev()

1つ前のステップを終了しているかどうかを返す

Boolean checkPrev( $run = true )
  • $run
    • チェックした結果、1つ前のステップを終了していない場合、そのステップを実行するかどうか
    • boolean (true : 実行する / false : 実行しない)
    • 省略可(省略時は true)

getCurrentStep()

現在のステップを返却する

String getCurrentStep()

isLastStep()

現在のステップが最後かどうかを返す

Boolean isLastStep()

set()

任意のキーで任意の値を格納する。Wizard を使用している間ずっとキャッシュされる

Boolean set( $param_key, $value )
  • $param_key
    • String
    • キーとなる文字列を指定する
  • $value
    • mixed
    • 値を格納する

get()

任意のキーで格納されている値を返却する

Mixed get( $param_key )
  • $param_key
    • String
    • キーとなる文字列を指定する

Wizard トレイト

トレイトの PHPコードは以下です。

<?php
/**
 * trait Wizard
 * @property array $session_data
 * @property \Smarty $smarty
 */
trait Wizard
{
  private $session_data;
  private $smarty;

  private $step_moved = false;
  /*
   * WizardのSESSION構造について
   * $_SESSION['WIZARDCLASS']['data'] = array()
   *                  ['steps']['METHODNAME']['status'] = 1:完了
   *                  ['step'] = METHODNAME:current
   */
  public function __construct($smarty = null)
  {
    session_start();
    $this->getSessionData();
    if (is_null($smarty) == false && $smarty instanceof \Smarty) {
      $this->smarty = $smarty;
    }
  }

  public function __destruct()
  {
    if ($this->isLastStep()) {
      $this->execComplete();
    }
    $_SESSION[$this->getSessionKey()] = $this->session_data;
  }

  public function addStep($step)
  {
    $this->steps[] = $step;
    return max(array_keys($this->steps));
  }

  public function removeStep($key)
  {
    array_splice($this->steps, $key+1);
  }

  public function getCurrentStep()
  {
    if (is_null($this->session_data)) {
      $this->session_data = $this->getSessionData();
    }
    if (isset($this->session_data['step'])) {
      return $this->session_data['step'];
    }
    return $this->steps[0];
  }

  public function run()
  {
    $run_method = $this->getCurrentStep();
    return $this->$run_method();
  }

  public function execComplete()
  {
    $this->session_data = array();
    return true;
  }

  public function backStep($run = true)
  {
    $prev_key = false;
    foreach ($this->steps as $step_key) {
      if ($this->getCurrentStep() == $step_key) {
        break;
      }
      $prev_key = $step_key;
    }
    if ($prev_key === false) {
      return false;
    }
    $this->step_moved = true;
    unset($this->session_data['steps'][$this->getCurrentStep()]['status']);
    $this->session_data['step'] = $prev_key;
    if ($run) {
      $this->run();
      exit;
    }
    return true;
  }

  public function nextStep($run = true)
  {
    $next_key = false;
    $is_next = false;
    foreach ($this->steps as $step_key) {
      if ($is_next) {
        $next_key = $step_key;
        break;
      }
      if ($this->getCurrentStep() == $step_key) {
        $is_next = true;
      }
    }
    switch ($next_key) {
      /*
       * 次のステップが見つからなければ完了とみなす
       */
      case false:
        return $this->execComplete();
        break;
      default:
        $this->step_moved = true;
        $this->session_data['steps'][$this->getCurrentStep()]['status'] = 1;
        $this->session_data['step'] = $next_key;
        if ($run) {
          $this->run();
          exit;
        }
        break;
    }
    return true;
  }

  public function checkPrev($run = true)
  {
    $prev_key = false;
    foreach ($this->steps as $i => $step_key) {
      if ($this->getCurrentStep() == $step_key) {
        break;
      }
      $prev_key = $step_key;
    }
    if ($prev_key === false) {
      return true;
    }
    if (is_null($this->session_data)) {
      $this->session_data = $this->getSessionData();
    }
    if (isset($this->session_data['steps'][$prev_key]['status'])) {
      return true;
    }
    /*
     * チェックに失敗した場合は前のステップを実行する
     */
    if ($run) {
      $this->session_data['step'] = $prev_key;
      $this->run();
      exit;
    }
    return false;
  }

  private function isLastStep()
  {
    if ($this->steps[max(array_keys($this->steps))] == $this->getCurrentStep()) {
      return true;
    }
    return false;
  }

  protected function getSessionKey()
  {
    return get_class($this);
  }

  protected function getSessionData()
  {
    if (is_null($this->session_data)) {
      $this->session_data = $_SESSION[$this->getSessionKey()];
    }
    return $this->session_data;
  }

  public function set($param_key, $value)
  {
    if (is_null($this->session_data)) {
      $this->session_data = $this->getSessionData();
    }
    $this->session_data['data'][$param_key] = $value;
  }

  public function get($param_key)
  {
    if (is_null($this->session_data)) {
      $this->session_data = $this->getSessionData();
    }
    return (isset($this->session_data['data'][$param_key]) ? $this->session_data['data'][$param_key] : '');
  }

  public function setSmartyInstance($smarty)
  {
    $this->smarty = $smarty;
  }
}

ウィザード形式の遷移を実装しようとすると、だいたいの場合煩雑なコードになりがちで、しかもメンテナンスが面倒だったりします。
これはオープンソースのライブラリを利用してもあまり変わりません。
そこでなるべく煩雑なコードにならないよう実装してみたのが今回の動機です。
テンプレートエンジンやSESSIONデータの持ち方などを少し手直しすれば、だいたいの場合に流用できるんじゃないかと思います。