Pine Framework 最新チュートリアル

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

簡易掲示板を作る

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

作成するのはなんとなく検索して出てきた『【エクササイズ】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アプリケーション開発をお楽しみください。