GNU Artanis における MVC

この記事を読む前に、必ず以下の記事をお読みください。  
[GNU Artanis 紹介](https://qiita.com/RoyMu/items/092aa745567a8404fc30)
GNU Artanis のホームページはこちらです：[https://artanis.dev](https://artanis.dev)

今すぐ始めましょう。

----

MVC は、アプリケーションを以下の3つの主要なコンポーネントに分ける古典的なデザインパターンです: **Model**、**View**、および **Controller**。**Model** はデータの管理を担当し、**View** はデータを表示し、**Controller** はユーザー入力を処理して **Model** と **View** を適切に更新します。

このチュートリアルでは、GNU Artanis を使用して **MVC** を活用する方法を紹介します。簡単なブログシステムを作成し、ユーザーがブログ記事を作成、閲覧、更新、削除できるようにします。**Model** を使用してブログ記事を管理し、**View** を使ってブログ記事を表示し、**Controller** を通じてユーザー入力を処理します。

プロジェクトのコードはこちらです: [https://gitlab.com/NalaGinrut/mvc-blog-example](https://gitlab.com/NalaGinrut/mvc-blog-example)

OS は Ubuntu 24.04 LTS を想定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3973392/16c1269e-347e-20f1-3c0c-deefe911b691.png)

**図1: MVC のアーキテクチャ**

## Artanis アプリの作成とデータベースの設定

```bash
art create
```

conf/artanis.conf を修正してデータベース接続を設定します。

```conf
db.enable = true
db.type = sqlite3
```

例を簡略化するため、SQLite3 をデータベースとして使用します。SQLite3 がインストールされていることを確認してください。

```bash
sudo apt-get install sqlite3
```

## Model の作成
この簡単なブログシステムでは、ブログ記事を管理するために1つのモデルのみが必要です。以下のコマンドでモデルを作成します。

```bash
art draw model article user
# drawing    model article
# working    Models `article.scm'

art draw model user
# drawing    model user
# working    Models `user.scm'
```

2つのモデルを作成しました:

article: app/models/article.scm
user: app/models/user.scm
article.scm を次のように修正します。

```scheme
(create-artanis-model article
  (:deps user)

  (id integer (#:primary-key #:auto-increment))
  (title char-field (#:not-null #:maxlen 128))
  (timestamp bigint (#:unsigned #:not-null))
  (content longtext))
```

Artanis は起動時に、モデル定義に基づいてデータベース内にテーブルを自動作成します。

**user.scm** を次のように修正します。

```scheme
(create-artanis-model user
  (:deps)

  (id integer (#:primary-key #:auto-increment))
  (username char-field (#:not-null #:maxlen 128))
  (password char-field (#:not-null #:maxlen 512))
  (salt char-field (#:not-null #:maxlen 8))
  (email char-field (#:not-null #:maxlen 128)))
```

## デフォルトユーザーの追加

アプリケーションを実行する前に、デフォルトユーザーやウェルカム記事を追加する必要があります。以下のコマンドでマイグレーションを作成します。

```bash
art draw migration user
# drawing    migration user
# working    Migration `user_20241207151014.scm'

art draw migration article
# drawing    migration article
# working    Migration `article_20241207151048.scm'
```
**db/migration/user_*.scm** を以下のように修正します。

```scheme
(define (add-test-user! name password salt)
  (let ((mt (map-table-from-DB (get-conn-from-pool!)))
        (real-pass (string->sha-256 (string-append password salt))))
    (mt 'set 'user
        #:username name
        #:email (format #f "~a@artanis.dev" name)
        #:password real-pass
        #:salt salt))

(migrate-up
  (display "Adding user `admin'...")
  (add-test-user! "Admin" "1234567" "asdf")
  (display "done.\n"))
```

**db/migration/article_*.scm** も修正します。

```scheme
(define (add-welcome-article!)
  (let ((mt (map-table-from-DB (get-conn-from-pool!))))
    (mt 'set 'article
        #:title "Welcome to Artanis"
        #:timestamp (current-time)
        #:content "This is a simple blog system powered by GNU Artanis.")))

(migrate-up
  (display "Adding welcome article...")
  (add-welcome-article!)
  (display "done.\n"))
```

次に以下のコマンドを実行してマイグレーションを適用します。

```bash
art migrate up user
# Loading /home/nalaginrut/Project/mvc-example-blog/conf/artanis.conf...done.
# connection pools are initilizing...DB pool init ok!
# Now the size of connection pool is 64.
# [Migrating user_20241207151014.scm]
# Creating table `user' defined in model
# ......
# Done.
# Adding user `admin'...done.

art migrate up article
# Loading /home/nalaginrut/Project/mvc-example-blog/conf/artanis.conf...done.
# connection pools are initilizing...DB pool init ok!
# Now the size of connection pool is 64.
# [Migrating article_20241207151048.scm]
# Creating table `user' defined in model
# ......
# Done.
# Creating table `article' defined in model
# ......
# Done.
# Adding welcome article...done.
```

## トラブルシューティング
以下のエラーが発生する場合があります。

```bash
Throw to key `artanis-err' with args `(500 ... "database is locked")'.
```

この場合、SQLite3 で Write-Ahead Logging (WAL) モードを有効にします。

```bash
sudo apt install sqlite-utils
sqlite-utils enable-wal blog.db
```

# コントローラとビューを作成  

## インデックス  

### 最初に、デフォルトでインデックスを作成します:

```bash
art draw controller index
# drawing    controller index
# working    Controllers `index.scm'
# create     app/controllers/index.scm
# working    Views `index'
```

---

### `app/controllers/index.scm` を以下のように変更します:

```scheme
(import (app models article)
        (srfi srfi-1)) ; for fold

(define blog-title "Artanis blog-engine")
(define footer
  (tpl->html
   `(div (@ (id "footer"))
         (p "Artanis blog-engine based on "
            (a (@ (href "https://github.com/HardenedLinux/artanis"))
               "GNU Artanis")
            "."))))

(define (show-all-articles articles)
  (fold
   (lambda (x prev)
     (cons `(div (@ (class "post"))
                 (h2 ,(result-ref x "title"))
                 (p (@ (class "post-date"))
                    ,(strftime "%Y-%m-%d"
                               (localtime
                                (result-ref x "timestamp" #:decode? #f))))
                 (p ,(result-ref x "content")))
           prev))
   '() articles))

(define (article-get-all)
  (cond
   (($article 'get #:ret 'all #:order-by '(timestamp desc))
    => show-all-articles)
   (else '())))

(index-define
 ""
 (lambda (rc)
   (let ((all-posts (tpl->html (article-get-all))))
     (view-render "index" (the-environment)))))

(get "/" (lambda (rc) (redirect-to rc "/index")))
```

### コントローラの解説
- `(index-define your_url handler)`
この関数は、`index` コントローラのアクションを定義します。最終的なURLは `/index/your_url` になります。この例では URL が空文字列 `""` なので、最終的なURLは `/index` になります。

- `(view-render path (the-environment))`
ビューテンプレートをレンダリングするために使用します。第1引数はビューのテンプレート名、第2引数は常に `(the-environment)` です。これは GNU Guile ランタイムで使用される環境情報を展開する特別なマクロです。

- `app/views/index/index.html.tpl`
ビューパスは自動的にこのパスに配置されます。

便利のため、ルートURLをインデックスコントローラにリダイレクトする設定を追加しています。

### データモデルについて
`article` モデルをインポートした後、`$article` という特別な関数が利用可能になります。これはテーブル `article` のリレーショナルマッピングを行います。この関数は、GNU Artanis がモデル定義に基づいて自動生成したもので、命名規則は `$model_name` です。

他のフレームワークでよく見られるオブジェクトリレーショナルマッピング (ORM) は、オブジェクト指向プログラミング (OOP) をリレーショナルデータベーステーブルにマッピングします。一方、GNU Artanis は関数型プログラミングのパラダイムを使用してデータベーステーブルを Scheme クロージャにマッピングします。これにより、より柔軟で軽量な実装が可能になります。

### ビューテンプレートを作成

次に、ビューのテンプレート `app/views/index/index.html.tpl` を作成します:

```html
<html>
    <@include header.html.tpl %>

    <body>
        <div id="container">
            <div id="main">
                <%= all-posts %>
            </div>
            <div id="sidebar">
                <form method="get" id="dashboard" action="/dashboard/admin">
                    <div><button>dashboard</button></div>
                </form>

                <div id="navigation">
                    <h3>Navigation</h3>
                    <ul>
                    </ul>
                </div>
            </div>
        </div>
        <%= footer %>
    </body>
</html>
```

このようにして、index コントローラとビューが完成します。何か追加の解説が必要であればお知らせください！

## Dashboard

新しい記事を投稿するためにダッシュボードが必要です。正直に言うと、これは非常に簡略化されており、投稿の編集や削除機能はありません。これらは練習問題としてお任せします。

```bash
art draw controller dashboard
# drawing    controller dashboard
# working    Controllers `dashboard.scm'
# create     app/controllers/dashboard.scm
# working    Views `dashboard
```


`app/controllers/dashboard.scm` を以下の定義で修正します：

```scheme
(import (app models article))

(define (gen-login-page rc)
  (let ((failed (params rc "failed")))
    (view-render "login" (the-environment))))

(dashboard-define
 "admin"
 (options #:with-auth gen-login-page)
 (lambda (rc)
   (view-render "admin" (the-environment))))

(dashboard-define
 "login"
 (options #:session #t)
 gen-login-page)

(dashboard-define
 "new_post"
 (method post)
 (options #:with-auth gen-login-page
          #:from-post #t)
 (lambda (rc)
   ($article 'set #:title (:from-post rc 'get "title")
             #:content (:from-post rc 'get "content")
             #:timestamp (current-time)
             #:user_id 0)
   (redirect-to rc "/")))

(dashboard-define
 "auth"
 (method post)
 (options #:auth '(table user "username" "password")
          #:session #t)
 (lambda (rc)
   (cond
    ((or (:session rc 'check) (:auth rc))
     (let ((referer (get-referer rc #:except "/dashboard/login*"))) ;
       (if referer
           (redirect-to rc referer)
           (redirect-to rc "/dashboard/admin"))))
    (else (redirect-to rc "/dashboard/login?login_failed=true")))))
```

### 解説

- `#:with-auth gen-login-page` ログインページを指定します。ユーザーがログインしていない場合、ログインページにリダイレクトされます。
- `#:session #t`: セッションを有効にします。GNU Artanis は自動的にユーザーのセッションを作成します。
- `#:from-post #t`: リクエストから POST データを取得するために使用されます。

## Login view

次に、テンプレート `app/views/dashboard/admin.tpl` を作成します：

```html
<html>
    <@include header.html.tpl %>

    <body>
        <p>記事を編集してください</p>
        <form id="post_article" action="/dashboard/new_post" method="POST">
            <p>タイトル:</p>
            <input type="text" name="title"/></br>
            <p>内容:</p>
            <textarea name="content" rows="25" cols="38">何かを書いてください</textarea></br>
            <input type="submit" value="投稿する"/>
        </form>
    </body>
</html>
```

# HTML header

最後に、デフォルトで使用する共通の `pub/header.html.tpl` を作成します：

```html
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title> GNU Artanis blog engine </title>
    <@css common.css %>
</head>
```

# Public ディレクトリ
GNU Artanis では、**pub** ディレクトリは画像、CSS、JavaScript ファイルなどの静的ファイルを保存するために使用されます。
pub ディレクトリ内のファイルはブラウザから直接アクセス可能です。

# ダッシュボードへのログイン
ダッシュボードコントローラーを作成した後、新しい記事を投稿するためにダッシュボードにログインする必要があります。
デフォルトのユーザー名は Admin、パスワードは 1234567 です。

# アプリケーションの実行
これでブログシステムが完成しました！アプリケーションを実行してみましょう。
```bash
art work
```

その後、ブラウザを開いて以下にアクセスしてください：
http://localhost:3000

# フィードバックをお待ちしております
メールは以下にお送りください：artanis@gnu.org  
または、GitLab の issue に投稿してください：  
[https://gitlab.com/hardenedlinux/artanis](https://gitlab.com/hardenedlinux/artanis)


以上。