Pine Framework 記事一覧

Pine Framework 最新チュートリアル

公開日時:2021/05/22 22:25

このチュートリアルを行う準備については、以下の記事を参照してください。

簡易掲示板を作る

それでは実際に、簡単なアプリケーションを作成してみましょう。

作成するのはなんとなく検索して出てきた『【エクササイズ】Reactで簡易掲示板を作る | Web白熱教室』のようなアプリケーションにします。

参考にしたアプリケーション

データベース構築

まず、/my_site/assets/settings/BambooSettings.phpを開いて、CONNECT定数をtrueに設定し、アプリケーションとしてデータベースに接続する事を宣言します。

データベースに接続を行う

Database.phpにMySQLへの接続情報を記述する

次に、/my_site/assets/entironments/Database.phpを開いてdefaultデータベースの開発環境の接続定義を行います。

Database.phpでの接続設定

MySQLに上記のユーザーアカウントを作成する

CentOSにSSHで接続して、MySQLにrootでログインします。

MySQLにログイン

先程Database.phpに定義したデータベースmy_site用のアカウントvagrantを作成します。※パスワードは任意に設定してください。

CREATE USER 'vagrant'@'%' IDENTIFIED BY 'pJQs25$3c';

データベースmy_siteの全テーブルに対する全ての操作権限を与えます。

GRANT ALL PRIVILEGES ON `my_site`.* TO 'vagrant'@'%';

与えた操作権限を有効にします。

FLUSH PRIVILEGES;

確認してみましょう。

SELECT user, host, plugin FROM mysql.user;

作成したユーザーの確認

データベースmy_siteの作成

データベースの作成はbambooコマンドを使って行えます。

VSCodeで別の新しいターミナルを立ち上げますが、OSの文字コードの都合上、日本語のコメントを受け付けるためにWSLのターミナルを開いてください。

新しいWSLターミナルを開く

新しいWSLターミナル

WSLのターミナルが開いたら以下のコマンドでデータベースを作成します。

bamboo my_site make database my_site

データベース作成確認

よろしければYを入力して実行してください。

my_siteデータベース作成完了

テーブルとDataModelの作成

データベースの作成が完了したら、データベース(MySQL)に掲示板データを管理する為のテーブルとそれに紐づくエンティティであるDataModelを作成します。

Pine Frameworkでは、これらの作成はCLIツールである『bamboo』を使って、my_siteを作成した時と同様に自動で生成出来ます。

今回作成するテーブルのDataModel名はComment、テーブル名はcommentsとします。

Pine Frameworkの組み込みO/Rマッパー『Bamboo』では、DataModel名は単数形のアッパー・キャメルケース、対応するテーブル名は複数形のスネークケースにするのが標準です。

参考:O/Rマッピング(O/Rマッパー)とは - IT用語辞典 e-Words

テーブルの構造は以下のようにします。任意で変更していただいても構いません。

カラム名 データ型 主キー コメント
id BIGINT auto_increment コメントID
name VARCHAR(32) 投稿者名
comment TEXT 投稿内容

先程データベースを作成する際に開いたターミナルで以下のコマンドをタイプし、エンターを押してください。テーブル定義ファイル作成の為の対話型インターフェイスが開始されます。

bamboo my_site define table comment

Commentのtabledefinition.ymlの作成ウィザード

テーブル定義ウィザード

テーブルコメントを入力すると、以下、カラム定義の入力に移ります。入力フォーマットは以下です。

カラム名 データ型 #コメント

データ型は省略表記が使えます。例えば、INT型はi、BIGINT型はbi、VARCHAR型はvc、TEXT型はtx、DATETIME型はdtです。大文字・小文字は区別しません。

KEYは、カラム名の後にコロン:で区切ってpuifを付加する事で指定できます。それぞれ、PRIMARY KEY、UNIQUE KEY、INDEX、FULLTEXT(MySQLのMyISAMストレージエンジンの場合のみ)です。

カラムのDEFAULT値は、データ型の後にコロン:で区切って指定します。

これに加え、AUTO INCREMENTなカラムはai、NULL許可なカラムは NULLをスペース区切りで別途付加してください。

例えば、上記テーブル構造の入力は以下のようになります。※1行ずつ入力してください。

id bi ai #コメントID
name vc32 #投稿者名
comment tx #投稿内容

3行共入力が終わったら、何も入力せずにエンターキーを押してください。

事前確認

作成されるComment.ymlが表示されます。内容を確認してYを入力するとComment.ymlが書き出されます。

!ワンポイント

修正したい箇所がある場合は、ymlファイル書き出しの際に同時に書き出されるComment.xxxxx.recipeファイルを使って再度定義ファイルを作成出来ます。

Comment.ymlの内容確認

参考:書き出しディレクトリ /my_site/assets/tabledefinitions/Comment.yml

書き出されたComment.yml

bambooでは今作成したテーブル定義ファイルであるtabledefinition.ymlの生成と同時に、このymlを使って実際のテーブルとそれに紐づくエンティティであるDataModelファイルの生成が行なえます。

今回作成した [Comment.yml] で作成しますか?
Y を入力すると既存テーブルをDROP TABLEしてからCREATE TABLEを行います。[Y/y/n]

※上記確認メッセージで小文字のyを入力するとDROP TABLEは行われないため、既にテーブルが存在する場合はExceptionが投げられてエラー終了します。誤って意図しないテーブルを削除するのを避ける為です。

不要なレシピファイルの破棄、及び正規化(再連番処理)をしますか?[Y/n] 

※レシピファイルはtabledefinition.ymlを生成するための中間ファイルであるため、アプリケーション開発に於いて必須ではありません。同じテーブル用のtabledefinition.ymlを作成する度にレシピファイルは連番でバックアップされていく為、不要になった古いレシピファイルはここで削除出来ます。

/my_ste/assets/datamodels/Comment.php

DataModel

/my_ste/assets/tabledefinitions/Comment.yml

tabledefinition.yml

生成されたテーブル情報の確認

DESCRIBE my_site.comments;

テーブル構造

これで、データベースを使った掲示板アプリケーション開発の準備が整いました。

入力フォームの作成

それでは、コメントを投稿するためのHTMLフォームを作成しましょう。

Pine FrameworkではTwigテンプレートを採用しています。/my_site/module/home/views/templates/get_index.twigに、フォーム用のHTMLを記述してください。

<form>
    <div>
        <input type="text" id="name" class="fcs" value="">
        <textarea id="comment" class="fcs"></textarea>
    </div>
    <button type="button" id="post" class="fcs">コメントする</button>
</form>
<div id="comments"></div>

<!-- JsRenderのテンプレート -->
<script id="get_index-tpl" type="text/x-jsrender">
    <div class="comment" data-comment_id="">
        <h4><span></span></h4>
        <p></p>
    </div>
</script>

コメント投稿フォーム

fcsというクラス名は、TABキーではなくエンターキーでフォームエレメントのフォーカスを移動する為のjQueryライブラリを使って対象エレメントを指定するため物です。

画面下部の<script/>タグ内のhtmlはJavaScriptテンプレートエンジン『JsRender』で使用する投稿コメントのDOM生成用テンプレートです。

ブラウザ表示

コメント投稿処理はJavaScriptを用います。

/my_site/module/views/get_index/scripts/get_index.jsに処理を記述します。

(function()
{
    $(document).ready(function()
    {
        $(document).on("click", "#post", function()
        {
            let data    = {
                name:       $("#name").val(),
                comment:    $("#comment").val()
            };

            $.fetch({
                url:        "/home/publish/", 
                type:       "POST", 
                data:       data,
                dataType:   "JSON",
                done:       function(response, textStatus, jqXHR)
                {
                    if(!$.is_success(response, "popup"))  { return false; }

                    console.log(response.result);
                }
            }); 
        });
    });
})();

コメント投稿用JavaScript

上記、$.fetch()は、Pine Frameworkが標準で提供しているjQueryメソッド$.ajax()のラッパーです。

通信エラー等が起きた場合は、デフォルトでエラー内容のポップアップメッセージが表示されます。

urltypeで指定されている内容に従い、homeコマンドのPostPublishアクションにHTTPリクエストを送信します。

Actoinの作成

コメント投稿用のJavaScriptコードが出来たので、次はサーバー側のActionを記述します。

WSLターミナルを開いて、以下のコマンドを実行してください。

pine my_site make action home publish post json

『my_siteというサイトで、homeコマンドのpublishアクション、受け付けるHTTPメソッドはPOSTで、レスポンスはJSONで返すActionをmakeします』~というコマンドです。

Actoin作成コマンド実行後

/my_site/module/logic/action/PostPublish.php
<?php
        :
class PostPublish extends SiteCommonAction implements \pine\manual\ActionManual, \pine\manual\UtilityManual
{
    const   SITE_MAP    =  false;

    protected $static_page                  = false;
    protected $ume_class                    = "PostPublishUME";
    protected $validate_ticket              = true;
    protected $validate_ticket_on_get       = false;
    protected $validate_ticket_by           = TICKET::BY_COOKIE;
    protected $regenerate_ticket_after_post = true;
    protected $transaction                  = true;
    protected $response_type                = ResponseType::JSON;

    protected function prepare(Dto $dto) : bool
    {
        // write your codes here.

        return true;
    }

    protected function verror(Dto $dto) : void {}

    protected function deficient(Dto $dto) : void {}

    protected function logic(Dto $dto) : bool
    {
        // write your codes here.

        return true;
    }

    protected function fail(Dto $dto) : void {}

    protected function done(Dto $dto) : void {}

    protected function always(Dto $dto) : void {}

    protected function closer(Dto $dto) : void {}

    public function sitemap(SiteMapDto $dto) : array
    {
        $dto->loc           = $this->get_loc($dto);
        $dto->lastmod       = date(DATE_ATOM, filemtime(__FILE__));
        $dto->changefreq    = null;
        $dto->priority      = "0.9";

        return [$dto];
    }

}
/my_site/module/logic/ume/PostPublishUME.php
       :
class PostPublishUME extends SiteCommonUME implements \pine\manual\UMEManual, \pine\manual\UtilityManual
{
    public function __construct(\pine\Dto $dto)
    {
        parent::__construct($dto);

        $validators = $this->getLocalValidators();
        foreach($validators as $keys => $validator)
        {
            $this->registerValidator($keys, $validator);
        }
    }

    protected function getValidationDefinitions() : array
    {
        return [
            "id" => [
                "name" => "コメントID", "type" => "int", "min" => 0, "max" => PHP_INT_MAX, 
                "auto_correct" => true, "trim" => UME::TRIM_ALL, "null_byte" => false,
                "method" => UME::POST, "require" => true
            ],
            "name" => [
                "name" => "投稿者名", "type" => "text", "min" => 0, "max" => 32, 
                "auto_correct" => true, "trim" => UME::TRIM_ALL, "null_byte" => false,
                "method" => UME::POST, "require" => true
            ],
            "comment" => [
                "name" => "投稿内容", "type" => "text", "min" => 0, "max" => 65535, 
                "auto_correct" => true, "trim" => UME::TRIM_ALL, "null_byte" => false,
                "method" => UME::POST, "require" => true
            ],
        ];
    }

    protected function doCustomValidate(\pine\Dto $dto)
    {
        /* sample
        if($dto->R["test1"] !== \$dto->R["test2"]){
            $this->VE["unmatch_test"] = "テストパラメターが一致しません。";
        }
        */
    }

    protected function getLocalValidators() : array
    {
        return [
            /* sample
            // digit 全て数字か?
            "digit" => [UME::SIZE_STRING, function($obj, $key, $req, $conditions)
                        {
                            if ($conditions["auto_correct"] === true)
                            {
                                $req = mb_convert_kana($req, "n", "UTF-8");
                            }
                            if (EX::empty($req)){ return $req; }
                            if (!ctype_digit((string)$req))
                            {
                                $obj->setVE($key, I18N::get("UME.invalid_digit_value", [$conditions["name"]], "[:@0] には数字以外が含まれています。"));
                                return $req;
                            }
                            return (string)$req;
                        }],
            */
        ];
    }
}

UMEファイルは、クライアントからリクエストされたデータの整形やバリデーションを行うクラスです。

関連付けるデータモデルにCommentを指定したので、commentsテーブルの定義が反映されたバリデーターになっています。

上記のうち、idはAUTO INCREMENT(自動で連番が振られる)なカラムなのでクライアントからデータが渡される事はありません。削除しましょう。

       :
    protected function getValidationDefinitions() : array
    {
            "name" => [
                "name" => "投稿者名", "type" => "text", "min" => 0, "max" => 32, 
                "auto_correct" => true, "trim" => UME::TRIM_ALL, "null_byte" => false,
                "method" => UME::POST, "require" => true
            ],
            "comment" => [
                "name" => "投稿内容", "type" => "text", "min" => 0, "max" => 65535, 
                "auto_correct" => true, "trim" => UME::TRIM_ALL, "null_byte" => false,
                "method" => UME::POST, "require" => true
            ],
        ];
    }
        :

投稿されたコメントをデータベースに登録するModelの作成

クライアントから送信されたコメントを受け取って処理するActionが出来たので、内容をデータベースに登録するModelを作成しましょう。

Model名は任意に決められるので、ここではRegisterCommentというモデル名にします。

pine my_site make model home RegisterComment

Modelの作成後

* Usable code here:
// 投稿されたコメントをデータベースに登録する
(new RegisterCommentModel())->exec($dto);

Model作成処理の最後に表示されているこのコードを、先程作成したAction『PostPublish.php』のlogic()関数内に記述する事で、Actionが正常実行(バリデーションエラー等が無い状態)される時にRegisterCommentモデルを実行させる事が出来ます。

Actionファイルの編集

/my_site/module/logic/actions/PostPublish.php
        :
    protected function logic(Dto $dto) : bool
    {
        // 投稿されたコメントをデータベースに登録する
        (new RegisterCommentModel())->exec($dto);

        return true;
    }
        :
!ワンポイント

Actoin内のメソッドがどのようにコールされるかは、『Actionの詳細 - Actionのライフサイクル』を参照してください。

RegisterCommntモデルで投稿内容をデータベースに登録する

ActionからModelに処理が繋がったので、投稿内容をデータベースに登録する処理をModel内に記述します。

投稿内容の登録処理

/my_site/module/logic/models/RegisterCommentModel.php
       :
class RegisterCommentModel extends SiteCommonModel implements \pine\manual\ModelManual, \pine\manual\UtilityManual
{
    protected function _exec(Dto $dto) : bool
    {
        $c  = new \pine\bamboo\Comment();   // Commentオブジェクトの生成
        $c->name    = $dto->R["name"];
        $c->comment = $dto->R["comment"];

        $this->bamboo->setup("Comment")     // 捜査対象をcommentsテーブルとして明示
                    ->insert($c)            // INSERTクエリの生成
                    ->execute()             // 生成されたクエリの実行
                    ;

        // 直前にINSERTされたレコードのAUTO INCREMENTな値(id)を取得
        $id = $this->bamboo->getLastInsertId();

        // INSERTされたidのデータを取得し、コメント登録結果として$dto->resultにセット
        $dto->result    = $this->bamboo->setup("Comment")
                                ->select("*")
                                ->where(["id", $id])
                                ->execute()[0]
                                ;
        return true;
    }

}

!ワンポイント

UMEを介して妥当性検査(バリデーション処理)され、全てのリクエスト内容が検査を通過した場合だけ以降の処理に$dto->Rという連想配列として渡されてきます。

ですから、Modelに到達した時点でPostPublishUME.phpの定義に従い、nameは32文字以内の必須項目、commentは65,535文字以内の必須項目として保証されています。

Pine Frameworkでは、全てのDML(データ操作命令)はプレースホルダを使ったクエリとして実行されます、この為、Bambooを介して処理をしている限りSQLインジェクション脆弱性は絶対に起こりません

Modelのメンバ変数である$bambooには、データベースアクセス用のライブラリ『Bamboo』のインスタンスが入っており、既にデフォルトデータベースに接続済みです。

Model内で発生したエラー・例外は、呼び出しを行っているAction側でキャッチされて適切に異常処理されます。

DTOでのアクセス可能なメンバ変数の定義

DTO(Data Transfer Object)はPineFramework内でデータを運ぶ為のコンテナのようなオブジェクトです。

PHPのクラスでは、変数・関数共にデフォルトがpublicアクセスですが、Pine Frameworkが提供しているDTOクラスはマジックメソッドを使ってアクセスを制限しており、デフォルトでは、許可されていない変数へのアクセスは行えないようになっています。

このため、DTOのメンバ変数$accessible配列にアクセス可能な変数名を指定する必要があります。

※なお、この挙動は/my_site/assets/settings/DtoSetting.phpの定数STRICT_ACCESSORをfalseに設定する事で、変数$accessibleへ明示する事無くPHPの標準的な手法により自由に変数のpublicアクセスが行えるようになります。

DTO

/my_site/module/logic/models/PostPublishDto.php
        :
class PostPublishDto extends SiteCommonDto implements \pine\manual\DtoManual, \pine\manual\UtilityManual
{
    private $accessible = ["result"];   // アクセスを許可するプロパティ名
        :

登録済みコメント情報をブラウザに返す

done()で、$dto->resultの内容をブラウザに返します。

resultとして$dto->resultを指定します。

ブラウザへのレスポンス

/my_site/module/logic/actions/PostPublish.php
       :
    protected function done(Dto $dto) : void
    {
        $this->response->result = $dto->result;
        $this->flush();
    }
       :

※flush()は、JSONデータをクライアントに出力してPHPの実行を終了する処理の明示的表現です。Actionのメンバ変数$response_typeがResponseType::JSONの場合、Pine FrameworkはAction終了後に自動でJSONデータをレスポンスとして出力した後PHPの実行を終了します。ですからflush()は記述してもしなくとも構いません。

コメントの投稿テスト

では実際にコメントの投稿テストを行ってみます。

まず、何も入力しない状態で『コメントする』をクリックしてみましょう。

必須入力エラー

バリデーションエラーが発生します。PostCommentUME.phpでnameフィールドとcommentフィールドは必須入力項目となっている為です。

また、nameは文字数32文字以内のため、以下のようなnameを渡すと文字数オーバーでやはりエラーになります。

123456789012345678901234567890123

文字数オーバー

正しく投稿者名と投稿内容を入力してコメントすると、ワンタイムチケットエラーが発生します。

ワンタイムチケットエラー

これは、PostPublish.phpメンバ変数$validate_tickettrueになっている為です。

ワンタイムチケットは主に二重投稿やイタズラ投稿の防止等のために使われる手法です。

ここでの解決方法は以下の2通りですので、好きな方を採用してください。

  1. actions/PostPublish.phpのメンバ変数$validate_ticketをfalseにする。
  2. actions/GetIndex.phpのlogic()内に TICKET::regenerate(TICKET::BY_COOKIE); を記述してワンタイムチケットを発行する。
/my_site/module/home/logic/actions/GetIndex.php
        :
    protected function logic(Dto $dto) : bool
    {
        // ワンタイムチケットの発行
        TICKET::regenerate(TICKET::BY_COOKIE);

        return true;
    }
        :

正常なコメント投稿

異常系の挙動が分かったところで、今度は正常なコメント投稿を行ってみましょう。

正常なコメント投稿処理

デベロッパーツール

デベロッパーツールで確認すると、通信のstatusはtrueとなっており、データベースに登録されたコメントの情報がresultとして返されています。

次はこの情報からDOMを生成し、画面のコメントエリアに追加表示します。

投稿されたコメントの反映

まず、get_index.twgのJSRender用テンプレートに、投稿内容を反映する為のプレースホルダを追加します。

[[>----]]を利用するとHTMLのタグ< >が自動で実体参照に置き換えられ、XSS攻撃を簡単に防止できます。

/my_site/module/home/views/templates/get_index.twg
<!-- JsRenderのテンプレート -->
<script id="get_index-tpl" type="text/x-jsrender">
    <div class="comment" data-comment_id="[[>id]]">
        <h4>[[>name]]<span>[[>add_at]]</span></h4>
        <p>[[>comment]]</p>
        <button class="delete">×</button>
    </div>
</script>
!ワンポイント

JsRenderのデフォルトのプレースホルダは{---}ですがTwigとコンフリクトするため、Pine Frameworkでは二重ブラケットに変更してあります。この定義は/my_site/modlue/__com/views/base/scrpts/base.jsの中にあります。

$.views.settings.delimiters("[[", "]]");
/my_site/module/home/views/get_index/scripts/get_index.js
        :
        $(document).on("click", "#post", function()
        {
            let data    = {
                name:       $("#name").val(),
                comment:    $("#comment").val()
            };

            $.fetch({
                url:        "/home/publish/", 
                type:       "POST", 
                data:       data,
                dataType:   "JSON",
                done:       function(response, textStatus, jqXHR)
                {
                    if(!$.is_success(response, "popup"))  { return false; }

                    // JsRenderでテンプレートを取得してプレースホルダに値をバインドする
                    let tpl     = $("#get_index-tpl");
                    let comment = tpl.render(response.result);
                    $("#comments").prepend(comment);
                }
            }); 
        });
        :

反映されたコメント

投稿済みコメント一覧の表示

GETメソッドで既存投稿を全て取得するアクションGetCommentsを作成して、画面をリロードした時に、投稿済みのコメントが表示されるようにします。

WSLターミナルから以下のコマンドを実行します。

pine my_site make action home comments get json

全てのコメントを取得する為、クライアントから絞り込みを行うための情報を渡す必要はありません。この為バリデーション処理は必要ないので、UMEに関連付けるDataModelも有りません。空でエンターキーを押してください。

全てのコメントを取得するアクション

実際に、全ての既存コメントを取得するモデルも作成します。

pine my_site make model home GetAllComments

全てのコメントを取得するモデル

作成時に生成されたモデル呼び出しコードを、先程作成したGetComments.phpのlogic()内に追記します。

モデル呼び出しコードの追加

モデルに、全てのコメントを取得して返すコードを書きます。

全てのコメントを取得するモデルのコード

/my_site/module/home/logic/models/GetAllCommentsModel.php
        :
class GetAllCommentsModel extends SiteCommonModel implements \pine\manual\ModelManual, \pine\manual\UtilityManual
{
    protected function _exec(Dto $dto) : bool
    {
         // 全てのコメントを取得して$dto->resultにセット
         $dto->result    = $this->bamboo->setup("Comment")
                                ->select("*")
                                ->orderBy(["add_at", OrderBy::DESC])
                                ->execute()
                                ;

        return true;
    }

}

GetCommentsDto.phpとGetComments.phpについては、PostPublishアクションの時と同様のコードを記述します。

GetCommentsDto.php

GetComments.php

get_index.jsの$(document).ready();メソッド内で/home/commentsにGETメソッドでリクエストする処理を記述します。

JsRenderを使ってDOMを描画する処理は抽象化したrender()関数を作成しました。

全てのコメントを取得して描画する処理

/my_site/module/home/views/get_index/scripts/get_index.js
        :
(function()
{
    // JsRenderでテンプレートを取得してプレースホルダに値をバインドする
    function render(result)
    {
        let tpl     = $("#get_index-tpl");
        let comment = tpl.render(result);
        $("#comments").append(comment);
    }

    $(document).ready(function()
    {
        $.fetch({
            url:        "/home/comments/", 
            type:       "GET", 
            data:       {},
            dataType:   "JSON",
            done:       function(response, textStatus, jqXHR)
            {
                if(!$.is_success(response, "popup"))  { return false; }

                response.result.forEach(result => {
                    render(result);
                });
            }
        }); 
        :

コメントの削除ボタンの実装

[☓]ボタンがクリックされたらコメントを削除する機能を実装します。

削除用URLはHTTPメソッドのうちのDELETEメソッドを使って/home/coment/[id]にリクエストを行う事で削除出来るようにします。

/my_site/module/home/views/get_index/scripts/get_index.js
        :
        $(document).on("click", "#comments .delete", function()
        {
            let comment     = $(this).parent();
            let comment_id  = comment.attr("data-comment_id");

            $.fetch({
                url:        "/home/comment/" + comment_id, 
                type:       "DELETE", 
                data:       {},
                dataType:   "JSON",
                done:       function(response, textStatus, jqXHR)
                {
                    if(!$.is_success(response, "popup"))  { return false; }

                    comment.remove();
                }
            }); 
        });
    });
})();

WSLターミナルで、homeコマンドのコメント削除用アクション、DeleteCommentを作成します。使用するHTTPメソッドはDELETEです。

pine my_site make action home comment delete json

削除するコメントのidはurlとして渡すため、UMEに関連付けるDataModelはありません。

コメント削除用アクション

urlの一部として渡した値を使用するには、routesファイルを編集します。

/my_site/assets/routes/routes.ymlに先ほど作成したhome/comment@delete用のroutesが記載されているので、これを以下のように書き換えます。

home/comment@delete:
    ↓
home/comment/**@delete:
/my_site/assets/routes/routes.yml

routes.yml

では、実際にコメントを削除するモデルを作成しましょう。

pine my_site make model home DeleteComment

コメントを削除するモデルの生成

処理の最後に出力されているモデル呼び出しコードをアクションファイルDeleteComment.phpのlogic()内に追記してください。

後は、コメント削除処理をDeleteCommentModel.phpに記述します。

コメント削除コード

        :
class DeleteCommentModel extends SiteCommonModel implements \pine\manual\ModelManual, \pine\manual\UtilityManual
{
    protected function _exec(Dto $dto) : bool
    {
        $c  = new \pine\bamboo\Comment();
        $c->id          = (int)$dto->routes->params[0];
        $this->bamboo->setup("Comment")->delete($c)->execute();

        return true;
    }
}

これで、コメント削除機能の実装が終わりました。テストしてみてください。

ワンポイント!

Pine FrameworkでのDELETEクエリはデフォルトで論理削除として実行され、対象行のdeleted=1にするUPDATE文が実行されます。

論理削除

物理的に行を削除する場合は、以下のようにBambooのdelete()メソッドの最後にtrueを渡してください。

        :
class DeleteCommentModel extends SiteCommonModel implements \pine\manual\ModelManual, \pine\manual\UtilityManual
{
    protected function _exec(Dto $dto) : bool
    {
        $c  = new \pine\bamboo\Comment();
        $c->id          = (int)$dto->routes->params[0];
        $this->bamboo->setup("Comment")->delete($c, true)->execute();

        return true;
    }
}

おまけ

スタイルシートの調整

あとは、スタイルシートが全くあたっていないのでレイアウトが崩れていますので、簡単に調整します。

スタイルシートの調整

/my_site/module/home/views/get_index/stylesheets/get_index.scss
/**
 * Project Name : my_site
 * Description  : ホーム画面SCSS
 * Start Date   : 2021/05/24 03:01:22
 * Copyright    : Katsuhiko Miki, http://striking-forces.jp
 * 
 * @author Katsuhiko Miki
 */
form {
    input {
        vertical-align: top;
        width:          100px;
    }

    textarea {
        vertical-align: top;
        width:          400px;
        min-height:     50px;
    }

    margin-bottom:      20px;
}

div#comments {
    .comment {
        box-sizing:     border-box;
        position:       relative;
        width:          100%;
        margin:         4px;
        padding:        10px;
        border:         solid 1px black;

        h4 {
            margin:     0;

            span {
                font-weight:    normal;
                font-size:      0.75em;
                margin-left:    1em;
            }
        }

        button {
            box-sizing: border-box;
            position:   absolute;
            top:        0;
            right:      0;
            margin:     4px;

        }
    }
}

投稿日時のマイクロタイムの削除と、コメントの改行を反映させます。

コメントの改行はHTMLタグの<br>に置き換えしますが、XSS(クロス・サイト・スクリプティング)攻撃が有効にならないように予め<>を実体参照に置き換えます。

その上で、コメント表示箇所のJsRenderのプレースホルダを[[:----]]に置き換え、HTMLタグを有効にします。

/my_site/module/home/views/get_index/scripts/get_index.js
        :
(function()
{
    // JsRenderでテンプレートを取得してプレースホルダに値をバインドする
    function render(result)
    {
        result.add_at   = result.add_at.split(".")[0];

        // commentのレンダリングはHTMLタグを実体参照に置き換え
        result.comment  = result.comment.replace(/</g, "&lt;");
        result.comment  = result.comment.replace(/>/g, "&gt;");
        // 改行コードを<br>に置き換え
        result.comment  = result.comment.replace(/\n/g, "<br>");

        let tpl     = $("#get_index-tpl");
        let comment = tpl.render(result);
        $("#comments").append(comment);
    }
        :

/my_site/module/home/views/templates/get_index.twg
<form>
    <div>
        投稿者名:<input type="text" id="name" class="fcs" value="">
        投稿内容:<textarea id="comment" class="fcs"></textarea>
    </div>
    <button type="button" id="post" class="fcs">コメントする</button>
</form>
<div id="comments"></div>

<script id="get_index-tpl" type="text/x-jsrender">
    <div class="comment" data-comment_id="[[>id]]"<h4>[[<span>[[</span></h4>
        <p>[[:comment]]</p>
        <button class="delete">×</button>
    </div>
</script>

確認

最終確認

まとめ

以上で、簡易掲示板作成のチュートリアルは終了です。

このように、Pine Frameworkは殆どのソースコードが自動生成され、実際に記述するコードはほんの僅かで済みます。

これは、DRYやOAOOの原則に従って同じ定義や記述重複させたり何度も同じ手順を繰り返したりせず、本来集中すべきクリエイティブな活動に専念出来るような配慮が、Pine Frameworkのアプリケーションデザインの隅々に渡ってなされているからです。

ここで紹介したのは、Pine Frameworkが提供する洗練された機能のほんの一部です。

Pine Frameworkをぜひお使いになって、快適なWEBアプリケーション開発をお楽しみください。

チュートリアルを行う為の環境準備

公開日時:2021/05/22 22:25

Pine Frameworkのチュートリアルを行うには、以下の環境を準備してください。

  • アプリケーションを稼働させるWEBサーバーの準備
  • WEBサーバーとクライアントマシンにPHPをインストール
  • 暗号化されたPHPソースコードを実行する為のライブラリ『ionCube Loader』のインストール
  • アプリケーション開発の為のエディタ、統合開発環境(IDE)の準備

WEBサーバー環境構築について

WEBサーバー環境構築の方法が分からない方は、当サイトの以下の記事で紹介している方法を推奨します。

一般的な初心者向けのチュートリアルでは『XAMPP』というオールインワンPHP開発環境を使う説明が多く、あるいは現在のプロの現場ですと『Docker』という先進的な仮想化技術『コンテナ』を用いた開発が主流となりつつありますので、それらについて知識がある方はそちらを用いてください。

本チュートリアルで『Vagrant+VirtualBox』という2015年頃に主流だった開発手法を推奨しています。

Dockerを用いた開発ではなくこの開発環境で説明を行う理由は、実際の物理サーバーを用いた開発手法に近く、初学者がWEBシステムの実態像を理解し易いと考えるからです。

PHP実行環境構築について

WEBサーバーへのPHPのインストールについては、以下を参照してください。暗号化されたPHPソースコードを実行するためのライブラリ『ionCube Loader』についても、下記記事内で紹介されています。

クライアントマシンへのPHPのインストールについては、以下を参照してください。

ソースコードエディタについて

本チュートリアルではMicrosoft社『Visual Studio Code(VSCode)』を使って説明を行います。導入にあたっては以下の記事を参照してください。

チュートリアルのためのプロジェクト作成

公開日時:2021/05/22 22:25

ソースコード管理フォルダの作成

まず、Pine Framworkプロジェクトのソースコードを管理するフォルダを作ります。

本チュートリアルではVSCodeを使って開発を行うので、任意の場所にVSCode用のフォルダを作成しましょう。※フォルダ名は何でも構いません。

VSCode用管理フォルダの作成

フォルダを作成したら、管理フォルダ内に入ります。

フォルダ内でShift+右クリックでコンテキストメニューを開き、『PowerShell ウィンドウをここで開く』をクリックしてください。PowerShellが立ち上がります。

PowerShellを開く

PowerShellウィンドウ

以上で、準備完了です。

Pineプロジェクトの作成

それではPine Frameworkを使ったWEBアプリケーション『Pine(仮名:自由に変えてください)』を作成していきます。

Pine Frameworkのコードは、PHPライブラリのリポジトリ(システムを構成するデータやプログラムの情報が納められたデータベースのこと)である『Packagist.org』で管理されているため、Composerを使って簡単にインストール出来ます。

開いているPowerShellで、以下のコマンドをタイプしてください。※最後の『Pine』は自由に変えていただいて構いません。

composer create-project mikisan/pine Pine

しばらくの間処理が自動実行され、Pineというプロジェクトフォルダが作成されます。※PowerShellウィンドウは閉じてしまってください。

作成されたPineプロジェクト

Pineフォルダの中を見てみましょう。

作成されたPineプロジェクトフォルダ内のファイル

各ファイル・フォルダの説明は以下です。

ファイル、フォルダ 説明
[cache] 各種キャッシュ格納フォルダ
[composer] composerで管理されているライブラリ用フォルダ
[node_modules npmで管理されているライブラリ用フォルダ
[pine] Pine Framework本体のフォルダ
[sites] WEBサイト用のアプリケーションプログラム用フォルダ
.babelrc JavaScriptを古い環境で動作するようにコンパイルするライブラリBabelの設定ファイル
.gitignore Pine Frameworkのソースコード管理をしているGit用の設定ファイル
.composer.json Pine Frameworkで利用しているPHPライブラリをComposerが管理するための設定ファイル
.composer.lock インストール済みのPHPライブラリをComposerが管理するための設定ファイル
.development.js 復数のJavaScriptファイル、Scssファイル、画像ファイルを一纏め(バンドル)にするためのライブラリwebpackのコンフィギュレーションファイル
.package.json Pine Frameworkで利用しているJavaScriptライブラリをnpmが管理するための設定ファイル
.package-lock.json インストール済みのJavaScriptライブラリをnpmが管理するための設定ファイル
webpack.config.js npmでwebpackを実行する際のコンフィギュレーションファイル(development.js)のエントリポイント
!注目ポイント

これらのファイルやフォルダのうち、ユーザーが編集するのは『sitesフォルダ』内のファイルだけです。

sitesフォルダ

sitesフォルダは、WEBアプリケーションの実際のソースコード(ビジネスロジック)を格納するためのフォルダです。

概念図としては、以下のようになります。

Pine Framework 概念図

環境変数PATHを設定する

Pine Frameworkは、アプリケーション開発を簡単にするためのCLI(コマンドライン・インターフェイス)ツールである『pine』と『bamboo』が実装されています。

この2つのコマンドを簡単に実行出来るようにするため、OSの環境変数PATHにPine Frameworkのcommandディレクトリまでのパスを設定していきます。

環境変数の設定ウィンドウを開く

スタートメニューを右クリックして開くポップアップメニューから『システム』をクリックします。

※他にも様々な方法でシステムの詳細設定ウィンドウが開けます。

スタートメニューを右クリック

詳細情報ウィンドウが開いたら、右側にある『システムの詳細設定』をクリックしてください。『システムのプロパティ』ウィンドウが開きます。

設定> 詳細情報

システムのプロパティ

開いた『システムのプロパティ』ウィンドウで、画面右下にある『環境変数』ボタンをクリックしてください。『環境変数』ダイアログが開きます。 開いたダイアログの下のエリア『システム環境変数』の中から『Path』という行を見つけて選択し、右下の『編集』ボタンをクリックしてください。

環境変数名の編集』ダイアログが開きます。

環境変数ダイアログ

環境変数の編集ダイアログ

commandディレクトリのパス

確定させたら、開いている設定ウィンドウを全て『OK』をクリックして閉じてください。環境変数の設定は以上です。

サイト構築

それでは、実際にWEBアプリケーション用のサイトを作成してみましょう。

先程作成した『Pine』プロジェクトフォルダ内でShift+右クリックしてコンテキストメニューを表示し、再び『PowerShell ウィンドウをここで開く』からPowerShellを立ち上げます。

PowerShellを開く

開いたPowerShellウィンドウ

サイト構築は、『pine』コマンドを使います。

以下のようにタイプしてエンターキーを押してください。

pine -g my_site

Pine > sites フォルダ内に、『my_site』という名のサイトディレクトリが生成されます。

作成されたサイトディレクトリ

my_siteディレクトリ内

既存サイトと同じ名前のサイトは作れない

なお、既に存在しているサイトと同名のサイトは作成できません。

同名サイトの上書き禁止

コマンドオプションの説明

上記のコマンドで使用したオプション-gは、サイトを生成する為のオプション--genesis(天地創造)の省略表記です。

逆に、サイトディレクトリを破棄する為のオプションは--doom(最後の審判)で、省略表記は-dです。

下記コマンドを実行してみましょう。my_siteディレクトリが削除されます。

pine -d my_site

サイト削除確認

サイト削除完了

なお、上記のサイトディレクトリ削除確認ですが、--assumeyesオプション(省略表記-y)を付加する事で、確認プロンプトを出さず、直ちに削除を実行できます。

これらを組み合わせる事で、既存サイトを削除しつつ、同名の新規サイトの作成を1コマンドで実行出来ます。

pine -d -y -g my_site

VSCodeでプロジェクトフォルダを開く

準備が整ったら、VSCodeでPineプロジェクトフォルダを開きます。

VSCodeでプロジェクトフォルダを開く

開いたプロジェクトフォルダ

webpackでJavaScript、Scss、画像をバンドル

Pine Frameworkではモジュールバンドラー『webpack』を使って、moduleディレクトリ内にあるJavaScriptファイルやScssファイルのコンパイルとバンドル(一纏めにする事)を行い、assetsresourcesディレクトリのファイルと併せてWEB公開領域であるpublicディレクトリ内に書き出します。

VSCodeのターミナルメニューから新しいターミナルを選択してください(またはCtrl+Shift+@)。

新しいターミナル

ターミナルが開いたら以下のコマンドを実行してください。moduleディレクトリにあるJavaScriptやScssがバンドルされてpublicディレクトリ内に書き出されます。

npm run build-dev site=my_site

上記コマンドは、npmを使ってwebpackを実行しています。実行内容はpackage.json内に記述されています。

build-devを指定するとバンドルのみでMinify(省データ化)は行われませんが、build-prodはMinifyされます。

バンドルされて書き出されたjsとcss

WEBサーバにアップロード

ファイル転送ソフト等を使って、WEBサーバ上(ここでは/var/www)に『Pine』プロジェクトをアップロードしてください。

WinSCPによるプロジェクトのアップロード

SSHでログインしてPineプロジェクトフォルダ配下の全てのファイル・フォルダのオーナーをvagrant、グループをapacheにし、書き込み権限を与えてください。

sudo chown -R vagrant:apache /var/www/Pine
sudo chmod -R 0775 /var/www/Pine
!注意

Pine Frameworkではサイトフォルダ毎に__logsという、実行ログが書き出されるフォルダがあります。この為、__logsフォルダにはapacheの書き込み権限が必要です。

上記のようにvagrant:apacheでオーナーを設定した場合、__logsフォルダのパーミッションは0770等、少なくともグループ(apache)の書き込み権限を与えてください。

Pine Frameworkで許可されているドメイン、IPアドレス

Pine FrameworkではソースコードをionCube社(英国)製品『ionCube Encoder』で暗号化し、稼働可能なドメイン及びIPアドレスを制限しています。

このため、以下のいずれかに環境を設定する必要があります。

ドメインの場合
  • www.pinefun.club
  • www2.pinefun.club
  • www3.pinefun.club
  • www4.pinefun.club
  • www5.pinefun.club

※pinefun.clubというドメインは、Pine Frameworkをユーザーの方々に利用してもらう為にわたしが取得したドメインです。ローカル環境ではご自由に利用してください。

IPアドレスの場合(CLI実行時等)
  • 192.168.0.0~192.168.0.255
  • 192.168.1.0~192.168.1.255

Virtual Host設定

上記で許可されているドメインを、Apacheのバーチャルホストに設定します。

sudo vi /etc/httpd/conf.d/pinefun.club.conf
/etc/httpd/conf.d/pinefun.club.conf
<VirtualHost *:80>
    ServerAdmin your@mail.address.jp
    DocumentRoot "/var/www/Pine/sites/my_site/public"
    ServerName pinefun.club
    ServerAlias www.pinefun.club

    ErrorLog "logs/www.pinefun.club-error.log"
    CustomLog "logs/www.pinefun.club-access.log" common
    <Directory "/var/www/Pine/sites/my_site/public">
        Require all granted
        AllowOverride All
    </Directory>
</VirtualHost>

confファイルの作成が終わったら構文チェックを行います。

apachectl configtest

Syntax OKと表示されれば記述に誤りはありません。

configtest結果

※『AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using centos7.localdomain. Set the 'ServerName' directive globally to suppress this message』という表示は、httpd.confファイルにServerNameというグローバルディレクティブを設定する事で表示されなくなります。

※ここでは本題ではない為説明は省きます。このメッセージが表示されていても特に問題はありません。

Apacheを再起動してバーチャルホスト設定を反映

バーチャルホストの設定が終わったら、以下のコマンドでApacheを再起動し、confファイルを読み込ませてください。

sudo systemctl restart httpd

Windowsのhostsファイル編集

hostsファイルは、クライアントマシン上でだけ利用できるDNS(Domain Name System)のような物で、ドメインとIPアドレスの解決を行ってくれる仕様です。

以下の場所にあるhostsファイルを編集してください。

※管理者権限が必要です。簡単な方法としてはhostsファイルを一旦デスクトップ等に移動して編集してから元の場所に戻してください。

C:\Windows\System32\drivers\etc\hosts
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
#   127.0.0.1       localhost
#   ::1             localhost
    192.168.0.6     pinefun.club    ←この行を追加。IPアドレスはお使いの環境に合わせてください

動作確認

ブラウザでアクセスして確認

これで準備が整いました。

では、http://pinefun.clubにアクセスしてみましょう。

home画面

これが、Pine Frameworkの初期状態の画面です。

左上に出ているhome/get_indexは、homeコマンドに対してHTTPのGETメソッドでリクエストされた場合のindexアクションの実行結果、という意味です。

homeコマンドは、一般に言うところのホームページです。

Pine Frameworkでは一つの機能の纏まりをコマンドと呼びます。一般的なWEBサイトではグローバルメニューがコマンドに相当すると考えれば良いでしょう。

これに対してアクションは各ページで実行される処理です。

この、コマンド、メソッド、アクションについて、『お問い合わせフォーム』を例に上げるて説明すると、以下のようになります。

  • お問い合わせフォーム画面(inquiry/get_index)
  • 入力内容確認画面(inquiry/post_confirm)
  • お問い合わせ完了画面(inquiry/post_done)

サーバーへの更新内容の同期設定と、webpackによる自動バンドル

ここで、VSCode上でのソースコードの編集内容を自動で即時的にサーバーへアップロードするようにSFTP設定ファイルを編集します。

Ctrl+Shift+Pを押して開くメニューから『SFTP: Config』を選択してください。

SFTP: Configメニュー

.vscodeフォルダが作成され、sftp.jsonというSFTP設定ファイルが開かれます。

sftp.json

以下のように設定を行ってください。

{
    "name": "VirtualBox-CentOS7",
    "protocol": "sftp",
    "host": "192.168.0.6",
    "port": 22,
    "username": "vagrant",
    "privateKeyPath": "C:/Vagrant/centos7/.vagrant/machines/default/virtualbox/private_key",
    "remotePath": "/var/www/Pine/",
    "ignore": [
        ".vscode",
        ".git",
        ".DS_Store",
        "caches",
        "node_modules",
        "map",
        "sites/**/public/__com/favicons"
    ],
    "syncOption": {
        "delete": true,
        "skipCreate": false,
        "ignoreExisting": false,
        "update": false
    },
    "uploadOnSave": true,
    "watcher": {
        "files": "**/*",
        "autoUpload": true,
        "autoDelete": true
    }
}

※詳しくは以下を参照してください。

SFTP拡張機能設定

webpackによる自動バンドル

次に、moduleディレクトリ内のJavaScriptやScssファイルが編集された際に、自動でwebpackによるバンドルとpublicフォルダ内への書き出しが行われるように、webpackにmy_siteディレクトリを監視させます。

開いているターミナルで以下のコマンドを実行してください。

npm start site=my_site

npmによるwatch

先程npm build-devでソースコードのバンドルを行った時人は違って、コマンドプロンプトが返って来ておらず、処理実行中(監視中)である事が分かります。

コードの修正が即時的に反映されるか確認

次は、このページのスタイルシートを変更して変更が反映されるか確認してみます。

先程述べたように、www.pinefun.clubでアクセスした際に表示される画面はhomeコマンドのGETメソッド用のindexアクションでした。

まず、homeコマンド用のプログラムディレクトリは以下です。

/Pine/sites/my_site/module/home

homeコマンドディレクトリ

上図の様に、各コマンドディレクトリ内には3つのディレクトリがあります。Pine Frameworkでは、エンジニアとデザイナーが分業を行う事を想定したディレクトリ構成となっています。

  • assets(各コマンド用のリソースを格納するディレクトリ。自由にサブディレクトリ等を作成出来る)
  • logic(主にプログラマが新規作成・更新・削除を行うディレクトリ)
  • views(主にデザイナーが新規作成・更新・削除を行うディレクトリ)

このうち、先程のページのスタイルを定義するScssファイルは viewsディレクトリ内、/home/views/get_index/stylesheets/get_index.scssです。

get_index.scss

ここに、適当なスタイルを書いてみましょう。

例)
body {
    background-color:   black;
    color:              darkgreen;
    font-family:        'Courier New', Courier, monospace;
    height:             2000px;
    font-size:          30px;
}

Scssを編集するとターミナルに処理結果の文字列が流れ、webpackによるバンドル処理が走っているのが分かります。

スタイルの編集と動的バンドル

ブラウザを更新すると、スタイルシートが変更されて背景が黒、文字色が緑に変わっています。

デザインが更新されたホームページ

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

公開日時:2019/12/13 11:03

この章ではまず、前章で登録したタスクを/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を設定すると物理削除となり、テーブルからレコードが物理的に削除されます。

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

お疲れさまでした。

チュートリアル2~データベースの利用とアクションの作成

公開日時:2019/12/13 11:00

さて、前の章ではテンプレートやJavaScript・スタイルシートのコンパイルとバンドル機能を使ってTODOアプリのタスク一覧画面を作成しました。

この章ではPine Frameworkのマイグレーション機能を使い、データベースにテーブルを作成してタスクを記録できるようにします。その後で新規タスクを作成するアクションを作成していきます。

bambooコマンドを使ったデータベースのマイグレーション

TODOアプリでは複数のタスクを記録するデータベーステーブルが欠かせません。ここではまず、tasksテーブルを作成してタスクを記録できるようにしてみましょう。

テーブルの作成にはまず、/sites/pine_site/assets/tabledefinitions内にテーブル定義ファイルを作成します。

テーブル定義ファイルの雛形は/documents/TableDefinitionSample.ymlです。これをコピーしてリネームし、テーブル定義ファイルを作成していきましょう。ファイル名はアッパーキャメルケース単数形で命名します。

今回は、Task.ymlとします。

テーブル定義ファイル

ファイルを作成したらこのファイルを開き、テーブル定義を記述していきます。

---
type:               table
table_comment:      タスク管理テーブル
logging:            true
columns:
    task_id:
        type:       BIGINT
        collation:  utf8mb4_general_ci
        allownull:  false
        default:    null
        ai:         true
        extra:      null
        other:      null
        index:      null
        comment:    タスクID
    title:
        type:       VARCHAR(255)
        collation:  utf8mb4_general_ci
        allownull:  false
        default:    null
        ai:         false
        extra:      null
        other:      null
        index:      null
        comment:    タスク名
    status:
        type:       VARCHAR(64)
        collation:  utf8mb4_general_ci
        allownull:  false
        default:    null
        ai:         false
        extra:      null
        other:      null
        index:      null
        comment:    状態
    timelimit:
        type:       DATETIME
        collation:  utf8mb4_general_ci
        allownull:  false
        default:    null
        ai:         false
        extra:      null
        other:      null
        index:      null
        comment:    タスク期限
primary:
    - task_id
unique:     null
index:      null
fulltext:   null
check:      null
foreign:    null

task_idはプライマリキーであり、ai: trueとすることで、AUTO_INCREMENTのカラムにしてあります。

テーブル定義ファイルの記述が終わったら、bambooコマンドを使ってテーブルを生成します。

bamboo pine_site make datamapper Task

上記はbambooコマンドを使ってpine_siteというサイトに定義されている設定で、tasksテーブルとそのエンティティであるTaskデータモデルを生成する、という意味です。

上記コマンドを実行すると、

[root@localhost command]# bamboo pine_site make datamapper Task
Target: Task
* Created files
/var/www/pine_site/sites/pine_site/assets/datamodels/trunk/Task.191212_101606.back
/var/www/pine_site/sites/pine_site/assets/datamodels/Task.php

* Deleted files
/var/www/pine_site/sites/pine_site/assets/datamodels/Task.php

* Created tables
tasks
zzz_tasks

[Success] メソッドは正常に実行されました。

といったメッセージが表示されるはずです。データベースのスキーマを確認すると、

作成されたテーブル情報1

作成されたテーブル情報2

のようにテーブルが作成されているはずです。

また、このテーブルのエンティティであるTask.phpというデータモデルが、/sites/pine_site/assets/datamodels/内に生成されているでしょう。

生成されたDataModelファイル

このファイルの中身は、

declare(strict_types=1);
namespace pine\bamboo;
use pine as pine;

class Task extends BaseDataModel
{

    //*****************************************/
    // const
    //  BOOLEAN = 'boolean',
    //  INTEGER = 'integer',
    //  DOUBLE  = 'double',
    //  FLOAT   = 'double',
    //  STRING  = 'string',
    //  LOB     = 'string',
    //  DATETIME = 'dateTime';
    //  DATE     = 'date';
    //*****************************************/

    protected $table_type   = DataModel::STRICT;
    protected $logging      = true;

    protected $schema  = [
        "task_id"                   => parent::INTEGER,
        "title"                     => parent::STRING,
        "status"                    => parent::STRING,
        "timelimit"                 => parent::DATETIME
    ];

    protected $primary = ["task_id"];

    public function isValid(): bool
    {
        return true;
    }

}

となっています。

これでタスクの登録が行えるようになりました。では、タスクを登録するアクションを作成していきましょう。

新規タスク登録アクションの作成

ここでは、タスク一覧画面の『新規タスクの作成』ボタンをクリックした後に遷移してくる、新規タスク情報入力画面を作成します。

この場合のリクエストメソッドはGETで、アクション名はnew_taskとします。

pineコマンドを以下のように実行することで、新規アクションとそれに関係するファイルを自動生成できます。

pine pine_site make action home new_task get html

新規アクションの作成

生成されたファイル

これで、GETメソッドによるnew_taskアクションの実行が可能になりました。

ブラウザのURLから/home/new_taskにアクセスしてみましょう。

生成されたファイル

新規アクションに関係するファイルが作成されたら、タスク一覧画面と同様にテンプレートファイルにHTMLを記述し、スタイルを定義します。

/sites/pine_site/module/home/views/templates/get_newtask.twig
<form id="register-task" action="/home/register" method="POST">
    <table>
        <tr>
            <th>タスク名</th>
            <td><input type="text" name="title"></td>
        </tr>
        <tr>
            <th>期限</th>
            <td><input type="text" name="timelimit"></td>
        </tr>
    </table>
    {{ site_hidden_ticket_raw() }}
    <button type="submit">新規タスクを登録する</button>
</form>

上記の{{ site_hidden_ticket_raw() }}という記述は、ワンタイムチケットのhiddenフィールドを出力するためのTwig Functionです。

Pine Frameworkでは二重投稿やCSRF対策のためのワンタイムチケット検査がデフォルトでサポートされており、通常、POSTでのアクションへのアクセスの場合は正しいワンタイムチケットが送信されなかった場合、エラーとなってModelの実行を行いません。

このsite_hidden_ticket_raw()というファンクションは

/sites/pine_site/module/__com/views/SiteCommonView.phpのSiteCommonTwigFunctionsクラス内

に定義されていますのでご確認ください。

新しく作成されたget_newtask.scssはこのままではまだ認識されないので、import.scss内でインポートしてください。

/sites/pine_site/module/home/views/stylesheets/impoert.scss
@import "get_index/get_index.scss";
@import "get_newtask/get_newtask.scss";    // 追記

スタイルシートを記述します。

/sites/pine_site/module/home/views/stylesheets/get_newtask/get_newtask.scss
form#register-task {
    table {
        width:      100%;

        input {
            width:  100%;
        }
    }
    button {
        width:      100%;
    }
}

新規タスク登録フォーム

これで、新規タスク登録フォームが完成しました。

get_newtask.twigテンプレート内の<form/>のactionに/home/registerを設定してあります。methodはPOSTです。

これは、POSTメソッド動作するregisterアクションでフォームからのリクエストを受け取り、新しいタスクを登録する事を想定しています。

では、GETメソッドでのnew_taskアクションを作成した時と同様に、POSTメソッド用のregisterアクションを作成しましょう。

pine pine_site make action home register post html

です。

新規タスク登録アクションの生成

生成されたファイル

フォームからの入力検査用のUMEファイルを設定する

今回はPOSTメソッドでフォームから情報が送られてくるので、これを受け取って検査するバリデーター『UME』を定義します。

UMEファイルは自動生成されており、/sites/pine_site/module/home/logic/umes/PostRegisterUME.phpがそれです。

このクラスのgetValidationDefinitions()関数内に、フォームから送られてくる情報に関する定義を記述しましょう。

/sites/pine_site/module/home/logic/umes/PostRegisterUME.php
    protected function getValidationDefinitions(): array
    {
        return [
            "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
            ],
            "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
            ],
        ];
    }

以上で、バリデーション定義が完了です。

このように入力値に対するバリデーション定義を記述すると、Actionが呼び出された際、Modelの実行に先立って自動的に入力情報のバリデーション処理が適切に行なわれます。

バリデーションによって入力が不適合であった場合はModelの実行は行なわれず、DTOに不適合に関する情報が記録されて処理がViewに渡されます。

入力が適合した場合のみModelの実行を行うためのAction::logic()関数が実行されます。

タスクをデータベースに登録するModelを作成する

ここでは、入力が適合した場合の新規タスク登録処理モデルを作成してみましょう。

まずpineコマンドで、新規タスクを登録するためのModelであるRegisterNewTaskというモデルを作成します。

pine pine_site make model home RegisterNewTask

新規タスク登録用モデルの生成

生成されたモデル

このモデルに、新規タスクを登録する処理を記述しましょう。

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

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

        return true;
    }
}

たったこれだけです。

DTOのメンバ変数Rには、ブラウザから送られたヴァリデーション・整形済みのリクエスト情報が格納されています。

ですから、先ほど作成したテーブルのエンティティであるTaskというDataModelのインスタンスを生成し、そのプロパティに値をセットしてから、Bambooクラスのインスタンス参照である$this->bambooにTaskオブジェクトをINSERTします。

Actoin::logic()内で、上記モデルを実行する

あとはこのRegisterNewTaskModelをregisterアクションで実行するように、PostRegister::logic()内で呼び出しを行います。

/sites/pine_site/module/home/logic/actions/PostRegister.php
    protected function logic(\pine\Dto $dto): bool
    {
        // 新規タスクの登録
        (new RegisterNewTaskModel())->exec($dto);

        return true;
    }

さて、では/home画面から、新規タスクを登録してみましょう。

無事タスクが登録されると下記のような画面が表示され、データベースにレコードが追加されています。

新規タスク登録完了

テーブルに作成されたレコード

正常に登録されなかったりエラーが出た時は…?

まだエラー処理について記述を行っていないため、コードの記述間違い等があるとエラー画面やエラーメッセージが表示されると思います。

テーブルに作成されたレコード

こうした場合は、/sites/pine_site/__logs/ディレクトリ内に日時エラーログが出力されています。こちらを確認して、問題点を修正してください。

また、ヴァリデーションエラーなどが発生している場合、Viewにはエラー情報がDTOに付加されて渡ってきています。

View::draw()メソッド
    /**
     * 画面表示
     *   SiteCommonView::site_common()->CommandCommonView::cmd_common()->View::draw()
     * 
     * @param   \pine\Dto       $dto
     * @param   bool            $action_result
     * @return  bool
     */
    protected function draw(\pine\Dto $dto, bool $action_result): bool
    {
        if($dto->on_error === true) { return $this->site_apology($dto); }       // システムエラー発生時

        return ($dto->response->status === true)
                            ? $this->normal($dto)
                            : $this->normal($dto)
                            ;
    }

上記の$dto->response->statusがfalseの場合、何らかのエラーが発生しています。

この場合は、$dto->response->message、及び$dto->response->verrorプロパティを検査することで、何が起きているのか確認できます。

例えば、

echo $dto->response->message . "<br>\n";
print_r($dto->response->verror);
echo "<br>\n";

といったコードでエラー情報を確認できます。

if($dto->on_error === true) { return $this->site_apology($dto); }       // システムエラー発生時

にあるsite_apology()メソッド実装されていません

これは通常は、サイトで共通の致命的エラー画面が表示されることを想定しています。

/sites/[site_id]/module/__com/views/SiteCommonView.phpの中にsite_apology()関数を作成して、エラー画面を表示する処理を記述してください。

また、エラーログに出力されている

[unkown] 2019/12/12 14:39:14 #EAU345PBP14

といったタイムスタンプの横の#で始まる文字列は、エラーが発生した時間を元に自動生成されるエラートラッキング番号です。

この情報は$dto->response->tracking_numberとしてViewに引き渡されており、__logsに出力されるログ情報と対応しています。

トラッキング番号のフォーマットルールは、

アルファベットは A~Zまでのうちゼロと間違いやすいO(オー)を除いた25文字の文字列を利用

フォーマット: ログ種別@アルファベット1文字
                  + 年@アルファベット2文字
                  + 年間通算日@数字3文字 
                  + 時間@アルファベット1文字 
                  + 分@アルファベット2文字 
                  + 秒@数字2文字

となっています。

エラーの解決などにご利用ください。

この章はここまでです。次の章では今回登録したタスクについて/home画面の一覧に反映するのと、タスク情報の変更を行えるように修正します。

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

記事リンク