CakePHP4 で作ったAPIを laravel-mix の Vue.jsからよぶ（少し Atomic Design）

[CakePHP4 で「独立したコアレイヤパターン」を適用する](https://qiita.com/katsuhiko/items/62bd661628e179079134) で API を作ったので、画面から呼び出せるようにしようと思います。

ただ、、、僕は Vue.js 初心者っというより、フロントエンドの開発はほとんどやっていないです。
Vuex は、実プロジェクトだと優秀なフロントエンジニアが活用してるんですが、自分は使い方がわかってないので使っていません。

「Atomic Design」の考えを少し取り入れて作ります。

今回は、Javascript で書いていますが、できれば Typescript にしたいなっと思っています。


# この記事でわかること

- サーバーサイドのエンジニアが苦労して Vue.js を使っている状況
- この記事内のソースは以下で公開しています。
    - https://github.com/katsuhiko/cakephp-vue-study


# 事前準備

- [CakePHP4 で「独立したコアレイヤパターン」を適用する](https://qiita.com/katsuhiko/items/62bd661628e179079134) の記事内容のソースから発展させてます。
- docker-compose を実行し、コンテナを立ち上げます。

```sh
docker-compose up -d
```

|  | バージョン |
|:-----------------|:------------------|
| docker           | Docker version 19.03.8, build afacb8b |
| docker-compose   | docker-compose version 1.25.4, build 8d51620a |
| CakePHP4         | 4.0.4 |
| Vue.js           | 2.6.11 |
| vue-router       | 3.1.6 |


# まずは共通的なところ

js ファイルは `./assets/js` 配下に格納します。
この格納位置は、`webpack.mix.js` で指定しています。

```js:webpack.mix.js
const mix = require('laravel-mix');

mix.setPublicPath('webroot')
    .js('assets/js/app.js', 'assets/js')
    .sass('assets/sass/app.scss', 'assets/css')
    .sourceMaps().webpackConfig({devtool: 'source-map'});
```

bootstrap.js は laravel-mix を導入したときから変えていません。

```js:assets/js/bootstrap.js
window._ = require('lodash');

/**
 * We'll load jQuery and the Bootstrap jQuery plugin which provides support
 * for JavaScript based Bootstrap features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

try {
    window.Popper = require('popper.js').default;
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');
} catch (e) {}

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

// import Echo from 'laravel-echo';

// window.Pusher = require('pusher-js');

// window.Echo = new Echo({
//     broadcaster: 'pusher',
//     key: process.env.MIX_PUSHER_APP_KEY,
//     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
//     encrypted: true
// });
```

`vue-router` でルーティングをします。各コンポーネントは後ほど作ります。

```js:assets/js/router.js
import Vue from 'vue'
import VueRouter from 'vue-router'

import Home from './components/HomeComponent.vue'
import TaskList from './components/pages/TaskListPage.vue'
import TaskAdd from './components/pages/TaskAddPage.vue'
import TaskEdit from './components/pages/TaskEditPage.vue'

Vue.use(VueRouter)

// パスとコンポーネントのマッピング
const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/task/list',
    component: TaskList
  },
  {
    path: '/task/add',
    component: TaskAdd
  },
  {
    path: '/task/edit/:id',
    component: TaskEdit
  }
]

// VueRouterインスタンスを作成する
const router = new VueRouter({
  mode: 'history',
  routes
})

// VueRouterインスタンスをエクスポートする
// app.jsでインポートするため
export default router
```

起点となるルートコンポーネント `App.vue` を作ります。

```vue:assets/js/App.vue
<script>
</script>

<template>
  <div>
    <div class="card-body">
      <RouterLink class="navbar" to="/">ホーム</RouterLink>
      |
      <RouterLink class="navbar" to="/task/list">タスク一覧</RouterLink>
      |
      <RouterLink class="navbar" to="/task/add">タスク追加</RouterLink>
    </div>
    <RouterView />
  </div>
</template>
```

これでやっと、app.js を作ります。

```js:assets/js/app.js
require('./bootstrap');

window.Vue = require('vue');

import router from './router'
import App from './App.vue'

const app = new Vue({
    el: '#app',
    router, // ルーティングの定義を読み込む
    components: { App }, // ルートコンポーネントの使用を宣言する
    template: '<App />' // ルートコンポーネントを描画する
});
```


# コンポーネントの作成

コンポーネントを作成します。
「Atomic Design」の template と page をわけるところのみの考えを取り入れます。

## タスク一覧

TaskListTemplate.vue を作ります。ここにはロジックは書かないようにします。
`props` を使って、親コンポーネントとなる page コンポーネントから描画する情報を受け取るようにします。

```vue:assets/js/components/templates/TaskListTemplate.vue
<script>
export default {
  props: {
    tasks: {
      type: Array
    }
  }
};
</script>

<template>
  <div class="task-list">
    <table>
      <thead>
        <tr>
          <th>タスク概要</th>
          <th>アクション</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="task in tasks" :key="task.id">
          <td>{{ task.description }}</td>
          <td>
            <router-link :to="{ path: `/task/edit/${task.id}` }">編集</router-link>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
</style>
```

TaskListPage.vue を作ります。こちらには表示するための情報を取得するロジックを書きます。

```vue:assets/js/components/pages/TaskListPage.vue
<script>
import TaskListTemplate from "../templates/TaskListTemplate.vue";

export default {
  components: {
    TaskListTemplate
  },
  data() {
    return {
      tasks: []
    };
  },
  methods: {
    async fetchTasks() {
      const response = await axios.get(`/api/ca-task/search.json`);
      if (response.status !== 200) {
        return false;
      }
      this.tasks = response.data.data;
    }
  },
  watch: {
    $route: {
      async handler() {
        await this.fetchTasks();
      },
      immediate: true
    }
  }
};
</script>

<template>
  <task-list-template :tasks="tasks"></task-list-template>
</template>
```


## タスク追加

TaskAddTemplate.vue を作ります。ここにはロジックは書かないようにします。
`emit` を使って、親コンポーネントとなる page コンポーネントへ保存されたときの動きを任せます。

```vue:assets/js/components/templates/TaskAddTemplate.vue
<script>
export default {
  data() {
    return {
      task: {
        description: ''
      }
    };
  },
  methods: {
    onSave() {
      this.$emit("save", { task: this.task });
    }
  }
};
</script>

<template>
  <div class="task-add">
    <form @submit.prevent="onSave">

      <div>タスク概要</div>
      <div><textarea v-model="task.description"></textarea></div>

      <div class="form__button">
        <button type="submit">保存</button>
      </div>
    </form>
  </div>
</template>

<style scoped>
</style>
```

TaskAddPage.vue を作ります。こちらには保存処理を書きます。

```vue:assets/js/components/pages/TaskAddPage.vue
<script>
import TaskAddTemplate from "../templates/TaskAddTemplate.vue";

export default {
  components: {
    TaskAddTemplate
  },
  methods: {
    async onSave(event) {
      const response = await axios.post(`/api/ca-task/create.json`, event.task);
      if (response.status !== 200) {
        alert('保存に失敗しました。');
        return;
      }

      alert('保存しました。');
      this.$router.push({ path: '/task/list' });
    }
  }
};
</script>

<template>
  <task-add-template @save="onSave($event)"></task-add-template>
</template>
```


## タスク編集・削除

TaskEditTemplate.vue を作ります。ここにはロジックは書かないようにします。
`props` を使って、編集する前のタスク情報（現在のタスク情報）を取得して、
`emit` を使って、親コンポーネントとなる page コンポーネントへ保存・削除・キャンセルの動きを任せます。

削除確認のアラートは、画面デザインに関するところかなっと思い、こちらに記載することにしました。実際は、Molecules として作成されたモーダルになるかもしれません。 

```vue:assets/js/components/templates/TaskEditTemplate.vue
<script>
export default {
  props: {
    task: {
      type: Object
    }
  },
  methods: {
    onSave() {
      this.$emit("save", { task: this.task });
    },
    onDelete() {
      if (!confirm('削除します。')) {
        return;
      }

      this.$emit("delete", { task: this.task });
    },
    onCancel() {
      this.$emit("cancel", { task: this.task });
    }
  }
};
</script>

<template>
  <div class="task-add">
    <form @submit.prevent="onSave">

      <div>タスク概要</div>
      <div><textarea v-model="task.description"></textarea></div>

      <div class="form__button">
        <button type="submit">保存</button>
        <button type="button" @click.prevent="onDelete">削除</button>
        <button type="button" @click.prevent="onCancel">キャンセル</button>
      </div>
    </form>
  </div>
</template>

<style scoped>
</style>
```

TaskEditPage.vue を作ります。こちらにはタスク一覧・タスク追加で行ったことの集大成のような感じの作りになります。

```vue:assets/js/components/pages/TaskEditPage.vue
<script>
import TaskEditTemplate from "../templates/TaskEditTemplate.vue";

export default {
  components: {
    TaskEditTemplate
  },
  data() {
    return {
      task: {}
    }
  },
  async mounted() {
    const response = await axios.get(`/api/ca-task/view/${this.$route.params.id}.json`);
    if (response.status !== 200) {
      alert('取得に失敗しました。');
       return;
     }

    this.task = response.data.data;
  },
  methods: {
    async onSave(event) {
      const response = await axios.put(`/api/ca-task/update/${this.$route.params.id}.json`, event.task);
      if (response.status !== 200) {
        alert('保存に失敗しました。');
        return;
      }

      alert('保存しました。');
      this.$router.push({ path: '/task/list' });
    },
    async onDelete(event) {
      const response = await axios.delete(`/api/ca-task/delete/${this.$route.params.id}.json`);
      if (response.status !== 204) {
        alert('削除に失敗しました。');
        return;
      }

      alert('削除しました。');
      this.$router.push({ path: '/task/list' });
    },
    onCancel(event) {
      this.$router.push({ path: '/task/list' });
    }
  }
};
</script>

<template>
  <task-edit-template :task="task" @save="onSave($event)" @delete="onDelete($event)" @cancel="onCancel($event)"></task-edit-template>
</template>
```


# さいごに

Validation も考慮していない作りですが、「Atomic Design」の Templates と Pages の考えは上手に取り込めたと思います。
`props` と `emit` を使うことで、画面デザインとロジックをわけることができたと思います。


# 補足）「Atomic Design」の感想

Atoms、Molecules、Organisms っと作っていくと、HTML 以外の別言語を見ている気がしてきます。
でも、実際にコーディングしている人に聞くと、作りやすいみたいですね。
サーバーサイドだと、フレームワークをラップしまくって、独自フレームワークっぽくなってるのはすごく扱いづらいんですが、フロントエンドだとそうでもないみたいなのが面白いです。

どこに差があるんでしょうかね。