CommandBox+ColdBoxでCRUDアプリを作ってみる

#CommandBox+ColdBoxでCRUDアプリを作ってみる

普段仕事ではフレームワークをあまり使わない環境なんですが、モダンな開発環境が構築できそうなColdBoxを勉強してみることにしました。英語アレルギーの体に鞭打ちつつ（Google翻訳に頼りつつ）。

HMVCフレームワークについて右も左もわからないので、とりあえずユーザー一覧をCRUDするアプリを作ってみようと思います。

##ColdBoxってなに？

> ColdBoxは、ColdFusion（CFML）用の規約ベースのHMVC開発フレームワークです。

[ColdBox公式ドキュメント](https://coldbox.ortusbooks.com/)より引用

##まずCommandBoxをインストール

CommandBoxというCLIを導入します。
https://www.ortussolutions.com/products/commandbox

導入編→[CommandBoxを使ってみる](https://qiita.com/namitan/items/75b9ea2c7c9e77046f8e)

##アプリのベースを作る

適当なフォルダを作って、その中にMyAppというアプリを作成します。

```shell:CommandBox
> mkdir study --cd
> install coldbox
> coldbox create app MyApp
> server start --rewritesEnable
```

「Welcome to ColdBox!」と書かれたアプリがブラウザで立ち上がったと思います。

##ハンドラーを作成

usersハンドラーにアクションindex,list,detail,add,edit,removeを作成します。

```shell:CommandBox
> coldbox create handler name="users" actions="index,list,detail,add,edit,remove"
```

ブラウザを更新すると、「Registered Event Handlers」にusersというリンクができていると思います。クリックしてもまだタイトルしか表示されません。これから作っていきます。

##モデルを作成

UserServiceサービスにメソッドgetAll,getUser,addUser,editUser,removeUserを作成します。

```shell:CommandBox
> coldbox create model name="UserService" methods="getAll,getUser,addUser,editUser,removeUser" persistence="singleton"
```

###UserServiceをハンドラーのプロパティに追加

```cfscript:handlers/users.cfc
component{ 
  property name="userService" inject="UserService";
```

##コンストラクタにデータのモックを作成

おためしなので今回データベースは使いません。

```cfscript:models/UserService.cfc
UserService function init(){
  variables.data = [
    { id=1, name="Taro Suzuki" },
    { id=2, name="Ichiro Tanaka" },
    { id=3, name="Hanako Yamada" }
  ];
  return this;
}
```

##一覧画面を作成します

###メソッド getAll()を編集

データ全件を取得するメソッドを用意します。

```cfscript:models/UserService.cfc
function getAll(){
  return variables.data;
}
```

###ハンドラーのindexアクションを編集

indexはlistに飛ばすだけにします。

```cfscript:handlers/users.cfc
function index( event, rc, prc ){
  // list画面に飛ばします
  setNextEvent( "users/list" );
}
```

###listアクションを編集

getAll()で取得した配列をrc構造体に格納します。使用するビューは`users/list`です。

```cfscript:handlers/users.cfc
function list( event, rc, prc ){
  rc.aUsers = userService.getAll();
  event.setView( "users/list" );
}
```

###ビュー list.cfmを編集

rcに格納されたユーザー一覧のデータを表示します。
`event.buildLink(to="users.detail",queryString="id=#user.id#")`は`/users/detail?id={id}`の意味で、detailアクションは後ほど作ります。
表の3列目が空ですが、後で作る削除ボタン用です。

```html:views/users/list.cfm
<cfoutput>
<h1>users.list</h1>
<table class="table table-striped">
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>#html.nbs()#</th>
    </tr>
  </thead>
  <tbody>
    <cfloop array="#rc.aUsers#" index="user">
      <tr>
        <td>#html.href( href=event.buildLink(to="users.detail",queryString="id=#user.id#"), text=user.id, noBaseURL=true )#</td>
        <td>#encodeForHTML(user.name)#</td>
        <td></td>
      </tr>
    </cfloop>
  </tbody>
</table>
</cfoutput>
```

###画面を見てみよう

まず`http://127.0.0.1:{port}/`の下のほうにある`Execute`ボタンを押して、編集したUserServiceサービスを再読み込みします。この作業はUserServiceを書き換えるたびに行ってください。
![2020-03-16_231314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/dce30b8d-5891-6d66-83a2-53f7985ce0ef.png)

次に、画面上のほうにある`users`リンクをクリックします。
![2020-03-16_231618.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/9f1cb01a-5c58-3168-545a-f700cb677a01.png)

ユーザー一覧が表示されました。
![2020-03-16_232023.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/5ade8bd7-4a2e-7f87-061a-88e595e4cf53.png)

##詳細画面を作成します

###detailアクションを編集

一覧画面の番号リンクをクリックするとdetailアクションにパラメータidが渡ってきますので、当該IDのユーザーデータを取得するコードを書きます。

```cfscript:handlers/users.cfc
function detail( event, rc, prc ){
  rc.User = userService.getUser( event.getValue("id","") );
  event.setView( "users/detail" );
}
```

###メソッド getUser()を編集

上で呼び出したgetUser()の中身を書きます。データをIDで検索し、当該ユーザーのデータを返します。

```cfscript:models/UserService.cfc
function getUser(id){
  var n = arrayFind(variables.data, (x)=>{ return (x.id == id) ? true : false ; });
  return variables.data[n];
}
```

###ビュー detail.cfmを編集

rc構造体に入れたユーザーデータを表示します。フォームのpost先は`users/edit`です。

```html:views/users/detail.cfm
<cfoutput>
<h1>users.detail</h1>
<form action="#event.buildLink(to="users.edit")#" method="POST">
  <div class="form-group row">
    <label for="staticID" class="col-sm-2 col-form-label">ID</label>
    <div class="col-sm-10">
      <input type="text" readonly class="form-control-plaintext" id="staticID" value="#encodeForHTMLAttribute(rc.User.id)#">
    </div>
  </div>
  <div class="form-group row">
    <label for="username" class="col-sm-2 col-form-label">Name</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="username" value="#encodeForHTMLAttribute(rc.User.name)#">
    </div>
  </div>
  <div class="form-group row">
    <div class="col-sm-12">
      <input type="submit" class="btn btn-primary float-right" value="submit">
    </div>
  </div>
</form>
</cfoutput>
```

list画面で番号リンクをクリックし、detail画面を確認します。当該ユーザーのIDとNameが表示されました。
![2020-03-16_233227.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/4686df3b-f8bb-0bef-0ad2-a7ce30ee09a2.png)

##更新処理を作成します

###editアクションを編集

フォームに入力された値をeditUser()に渡します。エラーの場合はdetail画面に戻るようにしています。

```cfscript:handlers/users.cfc
function edit( event, rc, prc ){
  rc.User.id = event.getValue("staticID","");
  rc.User.name = event.getValue("username","");
  if( userService.editUser( rc.User.id, rc.User.name ) ){
    event.setView( "users/edit" );
  }else{
    event.setView( "users/detail" );
  }
}
```

###メソッド editUser()を編集

上で呼び出したeditUser()の中身を書きます。データをIDで検索し、当該ユーザーのデータを書き換えます。

```cfscript:models/UserService.cfc
function editUser(id, name){
  var n = arrayFind(variables.data, (x)=>{ return (x.id == id) ? true : false ; });
  if( n != 0 ){
    variables.data[n].name = name;
    return true;
  }else{
    return false;
  }
}
```

###ビュー edit.cfmを編集

更新完了の画面です。

```html:views/users/edit.cfm
<cfoutput>
<h1>users.edit</h1>
#html.meta( name="Refresh", content="3;URL=#event.buildLink(to="users.list")#", type="equiv" )#
<div class="alert alert-success" role="alert"><strong>success</strong> - Updated!</div>
</cfoutput>
```

##新規登録処理を作成します

###listアクションを編集

新規登録フォームを作る前に、nameフィールドの値を用意しておきます。

```cfscript:handlers/users.cfc
function list( event, rc, prc ){
  rc.aUsers = userService.getAll();
  rc.User.name = event.getValue("username",""); // 追加
  event.setView( "users/list" );
}
```

###ビュー list.cfmを編集

一覧の下に新規登録フォームを追加します。フォームのpost先は`users/add`です。

```html:views/users/list.cfm
<!--- tableの下にフォームを追加 --->
<form action="#event.buildLink(to="users.add")#" method="POST">
  <div class="form-group row">
    <label for="username" class="col-sm-2 col-form-label">Name</label>
    <div class="col-sm-10">
      <input type="text" class="form-control" id="username" name="username" value="#encodeForHTMLAttribute(rc.User.name)#">
    </div>
  </div>
  <div class="form-group row">
    <div class="col-sm-12">
      <input type="submit" class="btn btn-primary float-right" value="add">
    </div>
  </div>
</form>
</cfoutput>
```

こんな感じ。
![2020-03-16_235801.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/434db1ea-301e-1d8e-1d47-7818cced4986.png)

###addアクションを編集

フォームに入力された値をaddUser()に渡します。エラーの場合はlist画面に戻るようにしています。

```cfscript:handlers/users.cfc
function add( event, rc, prc ){
  rc.User.name = event.getValue("username","");
  if( userService.addUser( rc.User.name ) ){
    event.setView( "users/add" );
  }else{
    event.setView( "users/list" );
  }
}
```

###メソッド addUser()を編集

上で呼び出したaddUser()の中身を書きます。IDを採番し、ユーザーデータを追加します。

```cfscript:models/UserService.cfc
function addUser(name){
  if(arrayLen(variables.data) == 0){
    var id = 1;
  }else{
    var id = arrayLast(variables.data).id + 1;
  }
  arrayAppend(variables.data, { id=id, name=name })
  return true;
}
```

###ビュー add.cfmを編集

登録完了の画面です。

```html:views/users/add.cfm
<cfoutput>
<h1>users.add</h1>
#html.meta( name="Refresh", content="3;URL=#event.buildLink(to="users.list")#", type="equiv" )#
<div class="alert alert-success" role="alert"><strong>success</strong> - Added!</div>
</cfoutput>
```

##削除処理を作成します

###ビュー list.cfmを編集

空欄だった3列目に、削除ボタンを追加します。リンク先は`users/remove`で、パラメータidを渡します。

```html:views/users/list.cfm
<!--- tbodyの3列目を以下のように変更 --->
<td>#html.href( href=event.buildLink(to="users.remove",queryString="id=#user.id#"), text="remove", noBaseURL=true, class="btn btn-danger btn-sm" )#</td>
```

こんな感じ。
![2020-03-16_235916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106969/5be467b4-039f-5d33-d780-b898d43723a5.png)

###removeアクションを編集

パラメータのIDをremoveUser()に渡します。エラーの場合はlist画面に戻るようにしています。

```cfscript:handlers/users.cfc
function remove( event, rc, prc ){
  if( userService.removeUser( event.getValue("id","") ) ){
    event.setView( "users/remove" );
  }else{
    event.setView( "users/list" );
  }
}
```

###メソッド removeUser()を編集

上で呼び出したremoveUser()の中身を書きます。当該IDのユーザーデータを削除します。

```cfscript:models/UserService.cfc
function removeUser(id){
  var n = arrayFind(variables.data, (x)=>{ return (x.id == id) ? true : false ; });
  if( n != 0 ){
    arrayDeleteAt(variables.data, n);
    return true;
  }else{
    return false;
  }
}
```

###ビュー remove.cfmを編集

削除完了の画面です。

```html:views/users/remove.cfm
<cfoutput>
<h1>users.remove</h1>
#html.meta( name="Refresh", content="3;URL=#event.buildLink(to="users.list")#", type="equiv" )#
<div class="alert alert-success" role="alert"><strong>success</strong> - Removed!</div>
</cfoutput>
```

##日本語が文字化けする場合

コンポーネントの場合は

```
cfprocessingdirective(pageencoding="utf-8");
```

ビューやレイアウトの場合は

```
<cfprocessingdirective pageencoding="utf-8">
```

を書いておくと文字化けしません。

あと、`layouts/Main.cfm`のhtmlタグのlang属性も"ja"に変えておくとよいでしょう。

##おわりに

エラー処理なんかは全く入れていませんが、CRUDの流れだけはなんとかつかめた感じです。

`coldbox create`でモデルやハンドラーを作成するとTestBoxのモジュールも作成されますが、今回無視してしまいました。近いうちBDDについてもちゃんと勉強したいと思います。。。