はじめに
こんにちは、@pazworld です。
今年、BEAR.Sundayに出会いました。すべてがリソースでできていてURIでアクセスできることや、すっきりとしたディレクトリ構成、コンポーネントをDIで柔軟に入れ替えられることなど、設計の素晴らしさに感銘を受け、PHP入門をしながら勉強しました。
今年勉強したことのまとめとして、BEAR.SundayとHTMXを使った1行メモアプリをSPAで作成しました。これからBEAR.Sundayを勉強しようとしている方への参考になれば幸いです。
(SPA = Single Page Application: 最初に読み込んだWebページからページ移動せず動作するWebアプリケーション)
BEAR.Sunday
BEAR.SundayはPHPで書かれたWebフレームワークです。MVCを採用する多くのWebフレームワークと異なり、BEAR.Sundayはリソースで構成されていて、各リソースはURIを持ち、HTTPの動詞(GET/POST/PUT/DELETE)でアクセスします。
DIとAOPによって様々な機能を組み合わせて使うことができます。昔のバージョンから互換性を維持していることも特徴です。
HTMX
HTMXは簡単にいうとWebページの一部をサーバから送られてきたHTMLで置き換えるJavaScriptライブラリです。似ている技術にはRails 7から導入されたHotwireのTurbo Frameがあります。昔のWebでiframeを使っていた方には「ページの好きな部分をiframe化するもの」というとわかりやすいかもしれません。
ひとたびHTMXライブラリを読み込むと、その後はJavaScriptを使わずにHTMLタグだけで動的なアプリケーションを作ることができます。
今回作るもの
今回作るのは1行メモアプリで、次のような構成になっています。
ブラウザでアクセスするとメモの一覧を表示します。一覧には次のような行が含まれます。
- 通常行: メモが表示される行です
- 編集行: メモを編集できる行です
- 追加入力行: メモを追加できる行です
ファイル構成
作成・修正するファイルの配置は次の通りです。
MyVendor.HtmxMemo
├ bin
│ └ setup.php
├ public
│ └ index.php
├ src
│ ├ Module
│ │ ├ AppModule.php
│ │ └ HtmlModule.php
│ └ Resource
│ ├ App
│ │ ├ Memo.php
│ │ └ Memos.php
│ └ Page
│ ├ AddMemo.php
│ ├ EditMemo.php
│ ├ Index.php
│ └ Memo.php
└ var
├ db
│ └ database.sqlite3
└ qiq
└ template
├ layout
│ └ base.php
└ Page
├ AddMemo.php
├ EditMemo.php
├ Index.php
└ Memo.php
プロジェクトの作成
PHP 8以上とComposer 2が必要です。GitHub上に今回作成するソース一式がありますのでご参照ください。
composerでプロジェクトを作成します。途中で作成者とプロジェクト名を聞かれますので入力します。
composer create-project bear/skeleton MyVendor.HtmxMemo
...
What is the vendor name ?
(MyVendor):MyVendor ←入力する
What is the project name ?
(MyProject):HtmxMemo ←入力する
...
Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? ←Enterを押す
作成したプロジェクトディレクトリに入り、使用するライブラリを追加します。
cd MyVendor.HtmxMemo
composer require --dev bear/devtools:^1.0
composer require -W ray/aura-sql-module
composer require bear/qiq-module
データベースの準備
データベースはSQLite3を使用します。
セットアップスクリプト bin/setup.php
の内容を次のようにします。
<?php
declare(strict_types=1);
chdir(dirname(__DIR__));
passthru('rm -rf ./var/tmp/*');
// データベースディレクトリ作成
$dbdir = dirname(__DIR__) . '/var/db';
if (!is_dir($dbdir)) {
mkdir($dbdir);
}
// データベース設定
$dbpath = $dbdir . '/database.sqlite3';
$pdo = new \PDO('sqlite:' . $dbpath);
// テーブルを削除
$pdo->exec('DROP TABLE IF EXISTS memos');
// テーブルを作成
$pdo->exec('CREATE TABLE memos (id INTEGER PRIMARY KEY, title TEXT)');
// 初期値を登録
$pdo->exec('INSERT INTO memos (title) VALUES ("ねずみ")');
$pdo->exec('INSERT INTO memos (title) VALUES ("うし")');
$pdo->exec('INSERT INTO memos (title) VALUES ("とら")');
実行するとデータベースが作成され、サンプルデータが3件登録されます。
php bin/setup.php
setup.phpを実行するといつでもデータを初期化できます。
Appリソースの作成
BEAR.SundayにはAppとPageの2種類のリソースがあります。Appリソースはサブドメインの問題解決やドメインストレージへのアクセスを担当します。(なぜ PageリソースとAppリソースがあるのか)
Appリソースとして次の2種類を作成します。
- Memos: メモ全体を表す
- Memo: 単一メモを表す
AppModule
AppModuleはAppリソースで使うリソースを設定します。src/Module/AppModule.php
を修正し、作成したデータベースをAppリソースで使えるようにします。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Module;
use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;
+use Ray\AuraSqlModule\AuraSqlModule;
use function dirname;
class AppModule extends AbstractAppModule
{
protected function configure(): void
{
(new Dotenv())->load(dirname(__DIR__, 2));
+ $dbpath = dirname(__DIR__) . '/../var/db/database.sqlite3';
+ $this->install(
+ new AuraSqlModule(
+ 'sqlite:' . $dbpath
+ )
+ );
$this->install(new PackageModule());
}
}
データベースへのアクセスには Ray.AuraSqlModule を使用しています。
Memos
Memosはメモ一覧の取得、新規メモの追加をするAppリソースです。
- GET時: メモの一覧を取得します
- POST時: 新規メモを追加します
src/Resource/App/Memos.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\App;
use BEAR\Resource\ResourceObject;
use Ray\AuraSqlModule\AuraSqlInject;
class Memos extends ResourceObject
{
use AuraSqlInject;
public function onGet(): static
{
$memos = $this->pdo->fetchAll('SELECT * FROM memos');
$this->body = $memos;
return $this;
}
public function onPost(string $title): static
{
$sql = 'INSERT INTO memos (title) VALUES (:title)';
$values = ['title' => $title];
$this->pdo->perform($sql, $values);
$id = $this->pdo->lastInsertId();
$this->code = 201;
$this->headers['location'] = '/memo?id=' . $id;
return $this;
}
}
今回はインフラストラクチャのレイヤーを分けず、PDO (AuraSqlのExtendedPdo) を使ってデータベースとやりとりしています。
クラス内で use AuraSqlInject
することで、AppModuleで設定したデータベースに接続した状態のPDOオブジェクトが $this->pdo
に格納されます。
GET時にonGet
が、POST時にonPost
が呼び出されます。$this->body
がレスポンスボディを表します。onPost
ではレスポンスコード($this->code
)に201を指定し、ヘッダのLocation ($this->headers['location']
)に新しく作成されたメモリソースのURIを指定することで、作成したメモ・リソースを指し示しています。
Memo
Memoは指定されたメモの取得、メモの修正、メモの削除をするAppリソースです。
- GET時: メモを取得します
- PUT時: メモを修正します
- DELETE時: メモを削除します
src/Resource/App/Memo.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\App;
use BEAR\Resource\ResourceObject;
use Ray\AuraSqlModule\AuraSqlInject;
class Memo extends ResourceObject
{
use AuraSqlInject;
public function onGet(int $id): static
{
$sql = 'SELECT * FROM memos WHERE id=:id';
$bind = ['id' => $id];
$memo = $this->pdo->fetchOne($sql, $bind);
$this->body = $memo;
return $this;
}
public function onPut(int $id, string $title): static
{
$sql = 'UPDATE memos SET title=:title WHERE id=:id';
$bind = ['id' => $id, 'title' => $title];
$this->pdo->perform($sql, $bind);
return $this;
}
public function onDelete(int $id): static
{
$sql = 'DELETE FROM memos WHERE id=:id';
$bind = ['id' => $id];
$this->pdo->perform($sql, $bind);
return $this;
}
}
GET時にonGet
が、POST時にonPost
が、DELETE時にonDelete
が呼び出されます。
動作テスト
PHP内蔵サーバを起動します。
php -S 127.0.0.1:8080 bin/app.php
ブラウザでhttp://127.0.0.1:8080/memos
にアクセスするとデータベースに格納されたメモがhal/json形式で表示されます。(Dockerなど外部からアクセスする場合にはアドレスをhttp://0.0.0.0:8080/memos
にする必要があるかもしれません)
コマンドラインからcurlを使うとリソースを操作できます。たとえば次のようにするとABCというメモを追加できます。
curl -i -X POST http://127.0.0.1:8080/memos -d 'title
=ABC'
HTML化の準備
ここまででAppリソースの内容をhal/json形式で返すようになりました。次にPageリソースの内容をHTML形式で返すための準備をします。
HtmlModule
HtmlModuleはPageリソースで使うリソースを設定します。src/Module/HtmlModule.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Module;
use BEAR\Package\AbstractAppModule;
use BEAR\QiqModule\QiqModule;
class HtmlModule extends AbstractAppModule
{
protected function configure(): void
{
$this->install(new QiqModule($this->appMeta->appDir . '/var/qiq/template'));
}
}
HTMLを作成するテンプレートライブラリとしてBEAR.QiqModuleを使用しています。これはテンプレートエンジンQiqをBEAR.SundayのDIで使えるようにする働きをします。
テンプレートエンジンQiq
Qiqは2021年にリリースされた新しいテンプレートエンジンです。PHP構文を基本としていて、PHPのコードを書くことができます。
Qiqのテンプレートをvendor以下からコピーします。
cp -r vendor/bear/qiq-module/var/qiq var
レイアウトファイルvar/qiq/template/layout/base.php
を次の内容に変更します。
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HtmxMemo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<style>
.div-box {
width: 50em;
margin: auto;
}
.table-memo {
width: 100%;
}
.td-button {
width: 0;
white-space: nowrap;
}
tr.htmx-swapping td {
opacity: 0;
transition: opacity 0.5s ease-out;
}
</style>
<script src="https://unpkg.com/htmx.org@1.8.4"></script>
</head>
<body>
{{= getContent() }}
</body>
</html>
テーブルやボタンの見栄えをよくするために Bulma を使用しています。BulmaはFlexboxをベースにしたCSSフレームワークです。JavaScriptを含まない純粋なCSSのみのフレームワークとなっています。
次の行でBulmaを読み込んでいます。
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
また、次の行でHTMXを読み込んでいます。
<script src="https://unpkg.com/htmx.org@1.8.4"></script>
コンテキストをHTMLにする
index.php
でコンテキストをHALからHTMLに変更します。これによって/index
にアクセスした時にhal-jsonではなくHTMLが返されるようになります。
<?php
declare(strict_types=1);
use MyVendor\HtmxMemo\Bootstrap;
require dirname(__DIR__) . '/autoload.php';
-exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));
+exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'html-app' : 'prod-html-app', $GLOBALS, $_SERVER));
コンテキストはハイフンで区切られ、右から順番に適用されます。html
はHtmlModule
を、app
はAppModule
を表し、html-app
はAppModule
→HtmlModule
の順に適用することを意味しています。
Pageリソースの作成
PageリソースはAppリソースを操作して情報を集め、クライアントが要求する形式(HTML)で情報を返します。
Pageリソースとして次の4種類を作成します。
- Index: メモアプリのページを表す
- AddMemo: メモを追加入力する行を表す
- EditMemo: メモを編集する行を表す
- Memo: メモを表示する行を表す
Index
Indexはメモアプリの基本となるページを表示するPageリソースです。
src/Resource/Page/Index.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
class Index extends ResourceObject
{
use ResourceInject;
public function onGet(): static
{
$memos = $this->resource->get('app://self/memos');
$this->body['memos'] = $memos;
return $this;
}
}
対応するメソッドはGETだけです。GET時にonGet
が呼び出されます。onGet
の中ではAppリソースであるMemosをGETして、その結果をレスポンスボディのmemos
キーに格納しています。
クラス内で use ResourceInject
することで、他のリソースを$this->resource->get('app://self/memos')
のように呼び出すことができるようになります。ここではapp://self/memos
がAppリソースのMemosを表します。
次にテンプレートファイルを作成します。あるPageリソースに対応するQiqテンプレートファイルはvar/qiq/template/Page
以下に同じファイル名で置きます。今回はIndexリソースですので、テンプレートファイルはvar/qiq/template/Page/Index
になります。
{{ $this->setLayout('layout/base') }}
<div class="box div-box">
<h1 class="title">1行メモ</h1>
<table class="table table-memo">
<thead>
<tr>
<th>タイトル</th>
<th></th>
</tr>
</thead>
<tbody hx-target="closest tr" hx-swap="outerHTML">
<!-- 通常行の表示 -->
{{ foreach ($this->memos as $memo): }}
{{ $id = (string) $memo['id'] }}
{{ $title = $memo['title'] }}
<tr>
<td>
<input type="hidden" name="id" value="{{h $id }}"/>
{{h $title }}
</td>
<td class="td-button">
<button class="button is-warning" hx-get="/edit-memo?id={{h $id }}">編集</button>
<button class="button is-danger" hx-delete="/memo" hx-include="closest tr" hx-swap="outerHTML swap:1s">削除</button>
</td>
</tr>
{{ endforeach }}
<!-- 追加入力行の表示 -->
<tr>
<td>
<input class="input" name="title" type="text"/>
</td>
<td class="td-button">
<button class="button is-primary" hx-post="/add-memo" hx-include="closest tr">追加</button>
</td>
</tr>
</tbody>
</table>
</div>
Qiqでは{{ }}
内をPHPコードとして実行します。リソースで$this->body['memos']
で格納した値はQiqテンプレート内では$this->memos
で取得することができます。
{{ }}
は実行のみで結果を表示しません。HTMLとして表示するためには{{h }}
という構文を使います。{{h $id }}
で変数$id
の中身がテンプレート内に出力されます。
動作テスト
PHP内蔵サーバを起動します。Appリソースの時と異なり、bin/app.php
ではなく-t public
と指定します。
php -S 127.0.0.1:8080 -t public
ブラウザでhttp://127.0.0.1:8080
にアクセスするとメモアプリが表示されます。(表示だけでまだ動作はしません)
HTMXへの指示
先ほど追加したQiqテンプレートファイル var/qiq/template/Page/Index
を見てください。次のような行があります。
<tbody hx-target="closest tr" hx-swap="outerHTML">
HTMXではhx-〜
という属性で動作を制御します。
-
hx-target
: サーバから送られてきたHTMLで置き換える対象を指定する -
hx-swap
:hx-target
で指定したタグのどの部分を置き換えるか指定する
hx-〜
は子要素に受け継がれます。hx-target="closest tr"
は「最も近い<tr>
タグ」を表します。テーブルの子要素の<tr>
内のボタンなどでHTTPリクエストが発生すると、ボタンを囲む<tr>
が置き換え対象になります。
hx-swap="outerHTML"
は<tr>
タグそのものを置き換えるという指定になります。もしhx-swap="innerHTML"
とすると<tr>
タグの子要素のみが置き換わります。
下の方の追加入力行のあたりに「追加」ボタンを表す次のような行があります。
<button class="button is-primary" hx-post="/add-memo" hx-include="closest tr">追加</button>
ここで指定されているhx-〜
属性は次の2つです。
-
hx-post
: 指定したURIにPOSTすることを指定する -
hx-include
: 同時に送るタグを指定する
hx-post="/add-memo"
は/add-memo
というURIにPOSTすることを意味しています。
hx-include="closest tr"
は同じ<tr>
タグの子要素にある<input>
タグを送ることを意味しています。少し上にメモを入力する<input>
タグがありますので、この内容も一緒に送られます。
/add-memo
はAddMemoリソースを指し示します。
AddMemo
それではAddMemoリソースを作りましょう。src/Resource/Page/AddMemo.php
を作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
class AddMemo extends ResourceObject
{
use ResourceInject;
public function onPost(string $title): static
{
$ro = $this->resource->post('app://self/memos', ['title' => $title]);
$uri = 'app://self' . $ro->headers['location'];
$memo = $this->resource->get($uri);
$this->body['memo'] = $memo;
$this->code = 200;
return $this;
}
}
対応するメソッドはPOSTだけです。POST時にonPost
が呼び出されます。onPost
の中ではAppリソースであるMemosにメモ内容をPOSTします。そのレスポンスに含まれるLocationヘッダの値を使い、追加したメモ・リソースをGETした内容をレスポンスボディのmemo
キーに格納しています。レスポンスコードを200にしているのは、新規作成を表す201ではHTMXがうまく動かなかったためです。
対応するQiqテンプレートも作成します。
{{ $id = (string) $this->memo['id'] }}
{{ $title = $this->memo['title'] }}
<!-- 追加した通常行の表示 -->
<tr>
<td>
<input type="hidden" name="id" value="{{h $id }}"/>
{{h $title }}
</td>
<td class="td-button">
<button class="button is-warning" hx-get="/edit-memo?id={{h $id }}">編集</button>
<button class="button is-danger" hx-delete="/memo" hx-include="closest tr" hx-swap="outerHTML swap:1s">削除</button>
</td>
</tr>
<!-- 追加入力行の表示 -->
<tr>
<td>
<input class="input" name="title" type="text"/>
</td>
<td class="td-button">
<button class="button is-primary" hx-post="/add-memo" hx-include="closest tr">追加</button>
</td>
</tr>
追加したメモを表示する通常行と、新たに入力するための追加入力行が返されます。Indexテンプレートにあった追加入力行の<tr>
タグは、この2行分のタグで置き換えられます。
動作テスト
PHP内蔵サーバを起動します。
php -S 127.0.0.1:8080 -t public
ブラウザでhttp://127.0.0.1:8080
にアクセスするとメモアプリが表示されます。追加入力行のテキストボックスに文字列を入力し「追加」ボタンをクリックすると、新しいメモが追加されます。
EditMemo
Indexテンプレートに編集ボタンを表す次のような行があります。
<button class="button is-warning" hx-get="/edit-memo?id={{h $id }}">編集</button>
このボタンを押すとhx-get
によってGETリクエストを発行します。URIは/edit-memo
にid=1
のような引数をつけたものです。/edit-memo
はEditMemoリソースを表します。
EditMemoリソースを作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
class EditMemo extends ResourceObject
{
use ResourceInject;
public function onGet(int $id): static
{
$memo = $this->resource->get('app://self/memo', ['id' => $id]);
$this->body['memo'] = $memo;
return $this;
}
public function onPut(int $id, string $title): static
{
$this->resource->put('app://self/memo', ['id' => $id, 'title' => $title]);
$this->code = 303;
$this->headers['location'] = "/memo?id=$id";
$this->body['memo'] = null;
return $this;
}
}
GET時にonGet
が、PUT時にonPut
が呼び出されます。onGet
の中では指定されたメモをAppリソースであるMemoからGETし、レスポンスボディのmemo
キーに格納しています。
onPut
の中では指定されたメモの内容をAppリソースであるMemoにPUTしています。その後、レスポンスコード303とLocation
レスポンスヘッダによってそのメモを表すURIにリダイレクトしています。
対応するテンプレートも作成します。
{{ if (isset($this->memo)): }}
{{ $id = (string) $this->memo['id'] }}
{{ $title = $this->memo['title'] }}
<!-- 編集行の表示 -->
<tr>
<td>
<input type="hidden" name="id" value="{{h $id }}"/>
<input class="input" type="text" name="title" value="{{h $title }}"/>
</td>
<td class="td-button">
<button class="button is-primary" hx-put="/edit-memo" hx-include="closest tr">登録</button>
<button class="button" hx-get="/memo?id={{h $id }}">キャンセル</button>
</td>
</tr>
{{ endif }}
このテンプレートによってメモの編集行が返されます。
次の行が「登録」ボタンを表しています。
<button class="button is-primary" hx-put="/edit-memo" hx-include="closest tr">登録</button>
ボタンのクリック時にhx-put
によって/edit-memo
にPUTします。hx-include="closest tr"
が指定されているため、同じ<tr>
タグの小要素である次の2行の<input>
タグのデータもサーバに送られます。
<input type="hidden" name="id" value="{{h $id }}"/>
<input class="input" type="text" name="title" value="{{h $title }}"/>
PUT後には/memo
にリダイレクトしますが、その際にもQiqテンプレートはレンダリングされます。この無駄なレンダリングを避けるため、EditMemoリソースのonPut
の中で次のようにmemo
キーの中身をNULLにしています。
$this->body['memo'] = null;
そして、EditMemoテンプレートの先頭で、次の行によってmemo
がセットされていない場合にレンダリングをスキップしています。
{{ if (isset($this->memo)): }}
編集行で「編集」ボタンではなく「キャンセル」ボタンがクリックされた場合は、元の通常行を表示します。
<button class="button" hx-get="/memo?id={{h $id }}">キャンセル</button>
Memo
最後に通常行を表すMemoリソースを作成します。
<?php
declare(strict_types=1);
namespace MyVendor\HtmxMemo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
class Memo extends ResourceObject
{
use ResourceInject;
public function onGet(int $id): static
{
$memo = $this->resource->get('app://self/memo', ['id' => $id]);
$this->body['memo'] = $memo;
return $this;
}
public function onDelete(int $id): static
{
$this->resource->delete('app://self/memo', ['id' => $id]);
$this->body['memo'] = null;
return $this;
}
}
GET時にonGet
が、DELETE時にonDelete
が呼び出されます。onGet
の中では指定されたメモをAppリソースであるMemoからGETし、レスポンスボディのmemo
キーに格納しています。
onDelete
の中ではAppリソースであるMemoに対してDELETEを発行しています。
対応するMemoテンプレートも作成します。
{{ if (isset($this->memo)): }}
{{ $id = (string) $this->memo['id'] }}
{{ $title = $this->memo['title'] }}
<!-- 通常行の表示 -->
<tr>
<td>
<input type="hidden" name="id" value="{{h $id }}"/>
{{h $title }}
</td>
<td class="td-button">
<button class="button is-warning" hx-get="/edit-memo?id={{h $id }}">編集</button>
<button class="button is-danger" hx-delete="/memo" hx-include="closest tr" hx-swap="outerHTML swap:1s">削除</button>
</td>
</tr>
{{ endif }}
メモの内容を表示し、「編集」「削除」のボタンを作成しています。
「削除」ボタンではhx-delete
を使ってDELETEリクエストを発行しています。
<button class="button is-danger" hx-delete="/memo" hx-include="closest tr" hx-swap="outerHTML swap:1s">削除</button>
hx-swap="outerHTML swap:1s"
は<tbody>
から継承したhx-swap
指定を上書きしています。swap:1s
は1秒後に行を削除する指定です。削除時の効果はvar/qiq/template/layout/base.php
で次のように「スーッと消える」ように設定しています。
tr.htmx-swapping td {
opacity: 0;
transition: opacity 0.5s ease-out;
}
動作テスト
PHP内蔵サーバを起動します。
php -S 127.0.0.1:8080 -t public
ブラウザでhttp://127.0.0.1:8080
にアクセスするとメモアプリが表示されます。追加、編集、削除ができます。HTMXによってページ遷移せずに一部が書き換わるSPAアプリケーションになっています。
おわりに
今回はわかりやすくするために似たようなHTMLを何回も書いていますが、Qiqテンプレートのパーシャルを使うと同じ記述をまとめて共通ファイルに抽出することができます。また、Appリソース自体が直接データベースアクセスをしていますが、インフラストラクチャ・レイヤーを担当する様々なデータベースモジュールをDIでインジェクトして使うことができます。
BEAR.Sundayではすべてがリソースであり、リソース間をHTTPのメソッド(GET/PUT/POST/DELETE)で接続しています。また、リソースやテンプレートの構造がそのまま配置に反映されるためすっきりとした構成になっています。そのことをサンプルプログラムで示せたのではないかと思います。
HTMXを使うとブラウザからGET/POSTだけでなくPUT/DELETEも発行できます。サーバから送られてきたHTMLで画面の一部を置き換える方式のため、JavaScriptを書かなくてもSPAのようなインタラクティブなアプリケーションを作成することができます。また、サーバから選択肢としてリンクURIを送ることができるため、ハイパーメディアコントロールを維持できます。これらの点でHTMXはBEAR.Sundayとの相性が非常によいと思います。
最後に、私がBEAR.Sundayを理解するまで辛抱強く様々な質問に答えてくださいましたBEAR.Sunday作者の郡山さん(@koriym)、ならびにこの素晴らしいフレームワークの構築に協力されている皆さんに感謝いたします。ありがとうございます。