TODOアプリのチュートリアルをご紹介します。この章では既に登録されているタスク情報を一覧画面に反映し、必要な場合にタスクの状態を変更する機能を実装します。

※この記事は執筆・公開から1年以上経過しています。記事の情報が古くなっている場合がありますのでご注意ください。

公開日時:2019/12/13 11:03 最終更新:2021/06/09 22:13   Pine Framework
Framework MVC PHP

チュートリアル3~登録されたタスクの一覧表示反映とタスクの編集機能

この章ではまず、前章で登録したタスクを/home画面の一覧表示に反映させます。

ブラウザで/homeにアクセスしてみましょう。現在はダミーデータが表示されています。

今回まず編集するのはGetIndexView.phpです。Pine FrameworkではViewから永続化層(データベース)への参照を行うことが許可されており、このためのインターフェイスであるBambooインスタンスへの参照が全てのViewに対して提供されています。

では、データベースのtasksテーブルから、登録されているタスク情報一覧を取得するコードを書きましょう。

/sites/pine_site/module/home/views/GetIndexView.php
    /**
     * 入力内容にエラーが無い場合の正常系画面表示
     * 
     * @param   \pine\Dto       $dto
     * @return  bool
     */
    private function normal(\pine\Dto $dto): bool
    {
        $this->val["index"]     = "index.twig";
        $this->val["tpl"]       = "get_index.twig";
        $this->val["head"]      = array_merge($this->val["head"], []);
        $this->val["script"]    = array_merge($this->val["script"],
                                    [
                                        "/home/js/bundle.min.js"
                                    ]);
        $this->val["css"]       = array_merge($this->val["css"],
                                    [
                                        ["path" => "/home/css/bundle.min.css", "media" => "all"]
                                    ]);
        $this->val["sub_title"] = "";
        $this->val["keywords"]  = "";
        $this->val["desc"]      = "";

        // write your codes here.

        $result = $this->bamboo->setup("Task")->select("*")->orderBy(["task_id", bamboo\OrderBy::ASC])->execute();
        $this->val["tasks"]     = $result;

        return true;
    }

// write your codes here.の後の2行が追加した行です。

このように、Pine Frameworkはクエリビリディング機能を用いて、抽象化したSQLを書き綴るようにしてクエリの発行ができます。

2行目の$this->valは、Twigテンプレートに引き渡す連想配列の変数です。key=value形式で自由に設定できます。

このコードを追記したら、一度、/home画面を再表示してみましょう。まだ取得したタスクを描画するコードを記述していないので何も起きませんが、エラーがなければ再びタスク一覧のダミーデータが表示されます。

取得したタスク情報を表示

エラーが無いことが確認できたら、実際に取得したタスクを表示するコード書きましょう。編集するのはTwigテンプレートであるget_index.twigです。

/sites/pine_site/home/views/templates/get_index.twig
<h1>タスク一覧</h1>
<a href="/home/new_task"><button type="button" id="new-task">新規タスクの作成</button></a>
<table id="tasks">
    <thead>
        <tr>
            <th>編集</th>
            <th>タイトル</th>
            <th>状態</th>
            <th>期限</th>
            <th>削除</th>
        </tr>
    </thead>
    <tbody>
{% for t in tasks %}
        <tr>
            <td><a href="/home/edit?task_id={{ t.task_id }}"><button type="button" class="edit">編集</button</a></td>
            <td>{{ t.title }}</td>
            <td><span class="status {{ t.status }}">{{ t.status|cmd_label }}</span></td>
            <td>{{ t.timelimit }}</td>
            <td><a href="/home/delete?task_id={{ t.task_id }}"><button type="button" class="delete">削除</button></a></td>
        </tr>
{% endfor %}
    </tbody>
</table>

上記のように、Viewで取得して設定したtasksをfor文で回して1件ずつtという変数でとりだし、それぞれのプロパティについて、ダミー文字列の位置に埋め込んでいます。

上記の内、t.status|cmd_labelはTwigのFilter機能を使ってデータを整形するための記述で、これからcmd_labelというFilterをViewの中に記述していきます。

なお、Pine Frameworkでは規約として、Action固有のTwig FilterとTwig Functionについてはact_***というようにプレフィックスを付ける決まりになっています。同様に、コマンド内で共通で使う場合はcmd_***、サイト内で共通で使う場合はsite_***と決まっていて、そのTwig FilterやTwig Functionの実装がどこのViewに記述されているのかひと目で分かるように規約が定められています。

では実際にact_labelフィルタを実装しましょう。

Twigフィルタの実装

cmd_labelは/homeコマンドで共通で使うフィルタのため、homeコマンド内のCommandCommonView.phpに記述します。

/sites/pine_site/module/home/views/CommandCommonView.php
////////////////////////////////////////////////////////////////////////////////
// Twig Filters
//   note: filterの記述場所を明確にするため、プレフィックスとして cmd_ を付加してください。
class CommandCommonTwigFilters extends SiteCommonTwigFilters
{
    public function cmd_label(string $value) : string
    {
        switch($value)
        {
            case "not-started":     return "未着手";
            case "started":         return "進行中";
            case "done":            return "完了";
            default:
                return "不明";
        }
    }
}

上記のようになります。

このように、Twig FilterやTwig Functionを利用してプログラミングコードをテンプレートから分離すると、プログラミング知識の無いデザイナーがテンプレートのデザインを自由に編集することが出来るようになります。

さて、ここまででタスクの一覧を表示するためのコードが完成しました。ブラウザで/home画面をリロードしてみましょう。

正しく取得されたタスクの一覧

上のキャプチャ画面では期限がマイクロ秒まで表示されていますが、これについてはcmd_labelと同様にTwigフィルタを定義し、適切なフォーマットに整形するようにすると良いでしょう。Pine FrameworkではYmdHisという、YYYY-mm-dd HH:ii:ss形式に日時をフォーマットするフィルタが準備してあります。

<td>{{ t.timelimit }}</td>の部分を、<td>{{ t.timelimit|YmdHis }}</td>に変更して確かめてみてください。

タスクの状態の編集

タスクの一覧を表示できるようになったら、次はタスクの状態の変更行うフォームを作ります。

タスクの状態の変更を行うためのeditアクションを新しく作成しましょう。

pine pine_site make action home edit get html

new_taskアクションを作成した時と同様に、GetEdit.phpやGetEditUME.phpといったeditアクションに関係したファイルが自動生成されます。

タスク一覧の編集ボタンからはGETメソッドでtask_idが送信されてくるので、これを受け取ってヴァリデーションを行うGetEditUME.phpを編集しましょう。

/sites/pine_site/module/home/logic/umes/GetEditUME.php
    protected function getValidationDefinitions(): array
    {
        return [
            "task_id" => [
                "name" => "タスクID", "type" => "int", "min" => 1, "max" => PHP_INT_MAX, 
                "auto_correct" => true, "trim" => pine\UME::TRIM_ALL, "null_byte" => false,
                "method" => pine\UME::GET, "require" => true
            ],
        ];
    }

タスクの状態を入力するフォームは、get_newtask.twigの内容を複製して加筆編集します。スタイルシートも同様にget_newtask.scssを複製して加筆編集し、import.scssで読み込みを行ってください。

/sites/pine_site/module/home/views/templates/get_edit.twig
<form id="modify-task" action="/home/modify" method="POST">
    <table>
        <tr>
            <th>タスク名</th>
            <td>
                <input type="text" name="title" value="{{ task.title }}">
                <input type="hidden" name="task_id" value="{{ task.task_id }}">
                <input type="hidden" name="timestamp" value="{{ task.upd_at|YmdHis }}">
            </td>
        </tr>
        <tr>
            <td>
                <select name="status">{{ task.status|act_status_raw }}</select>
            </td>
        </tr>
        <tr>
            <th>期限</th>
            <td><input type="text" name="timelimit" value="{{ task.timelimit|YmdHis }}"></td>
        </tr>
    </table>
    {{ site_hidden_ticket_raw() }}
    <button type="submit">タスクを更新する</button>
</form>

上記テンプレートの

<input type="hidden" name="timestamp" value="{{ task.upd_at|YmdHis }}">

は、楽観排他用のタイムスタンプを更新アクションに受け渡すための記述です。

Pine Frameworkではデータベースの情報更新の際、複数人の更新がコンフリクトを起こすのを防止するため、楽観排他用のタイムスタンプで排他検査を行う事が強制されています

このため、現在のレコードの最新更新時間を取得してフォームに埋め込むなどしておき、実際の更新作業時にこのタイムスタンプを送信して他の場所で当該レコードが更新されていないか検査する必要があります。

また、上記テンプレートの<select/>タグ内に、再び{{ task.status|act_status_raw }}というフィルタが出てきています。

これは、statusの値によって初期選択<option/>を変えるためで、前で説明したように、if文による分岐をテンプレートから分離するための措置です。

今回はact_***ですので、Action固有のフィルタとして登録します。

サフィックスの***_rawは、フィルタから返却される文字列についてHTMLエスケープを行なわずにそのまま出力する、という意味です。

TwigではデフォルトでHTMLタグをエスケープしますが、今回のフィルタの出力は<option/>となるため、これをそのまま出力します。

/sites/pine_site/module/home/views/GetEditView.php
////////////////////////////////////////////////////////////////////////////////
// Twig Filters
//   note: filterの記述場所を明確にするため、プレフィックスとして act_ を付加してください。
class TwigFilters extends CommandCommonTwigFilters
{
    public function act_status_raw(string $status) : string
    {
        $opts   = [
            "not-started"   => "未着手", 
            "started"       => "進行中",
            "done"          => "完了"
            ];

        $opts_html  = "";

        foreach($opts as $key => $val)
        {
            $selected   = ($key === $status) ? " selected" :  "";

            $opts_html  .= "<option value="{$key}"{$selected}>{$val}</option>";
        }

        return $opts_html;
    }
}

あとは、タスク一覧画面から送られてきたtask_idを元にtasksテーブルから情報を取得して、Twigテンプレートに渡すための変数taskに登録します。

/sites/pine_site/module/home/views/GetEditView.php
    /**
     * 入力内容にエラーが無い場合の正常系画面表示
     * 
     * @param   \pine\Dto       $dto
     * @return  bool
     */
    private function normal(\pine\Dto $dto): bool
    {
        $this->val["index"]     = "index.twig";
        $this->val["tpl"]       = "get_edit.twig";
        $this->val["head"]      = array_merge($this->val["head"], []);
        $this->val["script"]    = array_merge($this->val["script"],
                                    [
                                        "/home/js/bundle.min.js"
                                    ]);
        $this->val["css"]       = array_merge($this->val["css"],
                                    [
                                        ["path" => "/home/css/bundle.min.css", "media" => "all"]
                                    ]);
        $this->val["sub_title"] = "";
        $this->val["keywords"]  = "";
        $this->val["desc"]      = "";

        // write your codes here.

        $result = $this->bamboo->setup("Task")->select("*")->where(["task_id", $dto->R["task_id"]])->execute();
        $this->val["task"]      = $result[0];

        return true;
    }

なお、Bambooを使ったSELECT文の取得結果は配列になるため、今回の行ったプライマリキーであるtask_idを用いて行うSELECT文のように結果が必ず1件(または0件)である場合でも配列が返されます。

このため、Twig変数のtaskに設定する結果は$result[0]のように配列に対して添字をつけて指定する必要があります。

タスク一覧画面で任意のタスクの編集ボタンをクリックしてみましょう。次のような画面が表示されるはずです。

タスクの状態編集画面

タスクの状態変更の反映

タスクの状態変更入力フォームが完成したら、あとはタスクの情報更新用のアクションを作成します。

アクション名はmodifyで、POSTメソッドで動作します。

pine pine_site make action home modify post html

PostModify.phpやPostModifyUME.phpといったアクション用のファイルが生成されたら、まず、フォームからのリクエスト情報を検査するためのヴァリデーションクラスであるPostModifyUME.phpを編集します。

/sites/pine_site/module/home/logic/umes/PostModifyUME.php
    protected function getValidationDefinitions(): array
    {
        return [
            "task_id" => [
                "name" => "タスクID", "type" => "int", "min" => 1, "max" => PHP_INT_MAX, 
                "auto_correct" => true, "trim" => pine\UME::TRIM_ALL, "null_byte" => false,
                "method" => pine\UME::POST, "require" => true
            ],
            "title" => [
                "name" => "タスク名", "type" => "text", "min" => 1, "max" => 255, 
                "auto_correct" => false, "trim" => pine\UME::TRIM_ALL, "null_byte" => false,
                "method" => pine\UME::POST, "require" => true
            ],
            "status" => [
                "name" => "状態", "type" => "text", "choice" => ["not-started", "started", "done"], 
                "auto_correct" => false, "trim" => pine\UME::TRIM_ALL, "null_byte" => false,
                "method" => pine\UME::POST, "require" => true
            ],
            "timelimit" => [
                "name" => "タスク期限", "type" => "datetime", "min" => 1, "max" => 19, 
                "auto_correct" => true, "trim" => pine\UME::TRIM_ALL, "null_byte" => false,
                "method" => pine\UME::POST, "require" => true
            ],
        ];
    }

上記のstatusフィールドについて、これまでとは少し違った記述であるchoiceが使われている事に着目してください。

choiceは選択型の入力のヴァリデーションで用いる事ができます。ここに指定された配列で定義されるリスト以外の値が入力された場合はヴァリデーションエラーとなります。

上記UMEファイルを定義し終わったら、新規タスク登録の時と同様にModelを作成します。

pine pine_site make model home ModifyTask

生成されたModifyTaskModel.phpに、ブラウザから送信されてきた情報でタスクの状態を更新するコードを書いていきましょう。

/sites/pine_site/module/home/logic/models/ModifyTaskModel.php
declare(strict_types=1);
namespace pine\app;
use pine as pine;
use pine\bamboo as bamboo;

class ModifyTaskModel extends SiteCommonModel
{
    public function exec(\pine\Dto $dto): bool
    {
        $t  = new bamboo\Task();
        $t->task_id     = $dto->R["task_id"];
        $t->title       = $dto->R["title"];
        $t->status      = $dto->R["status"];
        $t->timelimit   = $dto->R["timelimit"];
        $t->timestamp   = $dto->R["timestamp"];
        $this->bamboo->update($t)->execute();

        return true;
    }
}

上記のようにモデルを作成したら、アクションファイルのPostModify::logic()にこのモデルを呼び出すコードを記述します。

/sites/pine_site/module/home/logic/actions/PostModify.php
    protected function logic(\pine\Dto $dto): bool
    {
        // 既存タスク情報の更新
        (new ModifyTaskModel())->exec($dto);

        return true;
    }

ここまでコードを記述したら、実際にタスク状態の更新を行ってみましょう。

正常に更新が終わったら/home画面を確認してみましょう。ステータスの状態が更新されているかと思います。

まとめとして

以上でこのチュートリアルは終了です。

タスク一覧画面には削除ボタンも存在しますが、実装方法は基本的には編集ボタンと同様です。

Bambooを使ったレコード削除のコードは以下のようになります。

// 論理削除の場合        
$t  = bamboo\Task();
$t->task_id     = $dto->R["task_id"];
$t->timestamp   = $dto->R["timestamp"];
$this->bamboo->delete($t)->execute();

// 物理削除の場合        
$t  = bamboo\Task();
$t->task_id     = $dto->R["task_id"];
$t->timestamp   = $dto->R["timestamp"];
$this->bamboo->delete($t, true)->execute();

Pine Frameworkでは通常、delete()による削除は論理削除であり、deletedフラグのカラムが1にセットされるだけでレコード情報は残りますが、delete()メソッドの第2引数にtrueを設定すると物理削除となり、テーブルからレコードが物理的に削除されます。

どちらを使うかは制作案件の仕様によって決まりますので、必要に応じて適切な方の削除を行ってください。

お疲れさまでした。

記事リンク