Help us understand the problem. What is going on with this article?

SymfonyとVue RouterでTODOリストをつくる

LaravelでVue Routerを使って、SPAを作るというのを見かけてSymfonyでもできないかと思いやってみました。

Symfony側実装

Symfonyインストール

Symfonyの空プロジェクトを作成します。今回はWebアプリケーションなので、必要なパッケージも合わせてインストールしていきます。

cd /path/to/
composer create-project symfony/skeleton todo
cd todo
composer require maker
composer require api
composer require webpack-encore-bundle
composer require --dev symfony/profiler-pack # デバッグツールバーが必要な場合

makerはSymfony Consoleでのクラス生成ツールです。apiはAPI Platformで、モデルに対してRestful APIを生成することができます。webpack-encore-bundleはWebpack Encoreで、簡単に言うとLaravel Mixです。これでVueをビルドしていきます。

タスク制御API開発

TODOリストのタスクを制御するAPIを開発していきます。まずはタスクのエンティティを作成します。

 bin/console make:entity

 Class name of the entity to create or update (e.g. GrumpyChef):
 > Task

 Mark this class as an API Platform resource (expose a CRUD API for it) (yes/no) [no]:
 > yes

 created: src/Entity/Task.php
 created: src/Repository/TaskRepository.php

 Entity generated! Now let's add some fields!
 You can always add more fields later manually or by re-running this command.

Taskエンティティには以下のプロパティを用意します。

名前 内容
id int ID(自動生成)
title string タスクタイトル

シンプルですね。生成されたエンティティですが、
Mark this class as an API Platform resource をyesにすることで、
通常のEntityにちょっと追記されます。

src/Entity/Task.php
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
+ use Doctrine\ORM\Mapping as ORM;

/**
+  * @ApiResource
 * @ORM\Entity(repositoryClass="App\Repository\TaskRepository")
 */
class Task
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;


@ApiResourceアノテーションを追加されることで、API-Platformが以下のようなAPIを提供してくれます。

URI Method 処理
/api/tasks GET 一覧取得
/api/tasks POST 追加
/api/tasks/{id} GET 1件取得
/api/tasks/{id} PUT 置換
/api/tasks/{id} PATCH 更新
/api/tasks/{id} DELETE 削除

以上で、タスク制御API開発完了です。

Vue用のHTML出力ページ作成

まずはコントローラーを準備します。

bin/console make:controller AppController

生成されたコントローラーを変更していきます。

src/Controller/AppController.php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class AppController extends AbstractController
{
    /**
+     * @Route("{wildcard}", name="app", requirements={"wildcard"="^(?!build|api|_(profiler|wdt)).*"})
-     * @Route("/app", name="app")
     */
    public function index()
    {
        return $this->render('app/index.html.twig');
    }
}

ここが一番のポイントです。 ルーティングの変更になります。API-Platformが提供する/api/***、ビルドしたスクリプトが格納される/build、Symfonyのデバッグツールバーで利用される/_profiler, /_wdt 以外のURIは全てAppController::index()が実行されるようにします。つづいて、twig(テンプレート)を変更していきます。twigファイルはごそっと変えます。

templates/app/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Hello AppController!{% endblock %}

{% block stylesheets %}
  {{ encore_entry_link_tags('app') }}
{% endblock %}
{% block body %}
  <div id="app"></div>
{% endblock %}
{% block javascripts %}
  {{ encore_entry_script_tags('app') }}
{% endblock %}

stylesheets, javascriptsでwebpack-encoreでビルドするファイルを読み込むために、{{ encore_entry_link_tags('app') }}{{ encore_entry_script_tags('app') }}を記述します。あとはbodyブロックにVueを実行するdivタグ、<div id="app"></div>を記述します。

これで、Vue用のHTMLは準備できました。

Vue側実装

では、今回の要であるVue側の実装をしていきます。

インストール

まずはもろもろインストールします。

yarn add vue vuex bootstrap-vue bootstrap axios
yarn add vue-router vue-loader vue-template-compiler --dev

ストア機能を使いたいのでvuex、HTTP通信のためのaxios、見た目を整えたいのでbootstrap, bootstra-vueをインストールします。
続いて、Vue側のルーティング用にvue-routerと、ロード、テンプレートコンパイルのためにvue-loader, vue-template-compilerをインストールします。

ストア実装

インストールが終わったら、まずはストアから作ってきます。

assets/js/stores/modules/task.js
import axios from 'axios';

const task = {
    state: {
        tasks: []
    },
    mutations: {
        addTask(state, task) {
            state.tasks.push(task);
        },
        deleteTask(state, index) {
            state.tasks.splice(index, 1);
        }
    },
    actions: {
        async fetchTasks({commit}) {
            const list = await axios.get('/api/tasks', {
                data: {},
                headers: {'Accept': 'application/json'}
            }).then(res => {
                commit('setTasks', res.data);
            });
        },
        async addTask({state, commit}, task) {
            await axios.post('/api/tasks', task, {headers: {'Accept': 'application/json'}})
                .then(res => {
                    commit('addTask', res.data);
                });
        },
        async deleteTask({state, commit}, task) {
            const index = state.tasks.indexOf(task);
            await axios.delete('/api/tasks/' + task.id, {headers: {'Accept': 'application/json'}})
                .then(res => {
                    commit('deleteTask', index);
                });
        }
    }
};

export default task;

タスク用のストアです。先ほどSymfonyで作成したAPIを呼び出して一覧取得、追加、削除の処理を記載しています。

assets/js/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import task from './modules/task'
Vue.use(Vuex);

export default new Vuex.Store({
    modules: {
        task
    }
});

こちらはstoreのindexです。上記のtask.jsをインポートして、ストアのモジュールとして設定しています。

コンポーネント実装

次にコンポーネントを実装します。

assets/js/components/Tasks.vue
<template>
    <div class="container">
        <div class="row">
            <h2>TODOリスト!</h2>
        </div>
        <div class="form-group">
            <label for="title">タスク</label>
            <input id="title" type="text" class="form-control" name="title" v-model="title">
        </div>
        <div class="form-group">
            <button @click="add">登録</button>
        </div>
        <ul class="list-group">
            <li v-for="task in tasks" class="list-group-item d-flex justify-content-between align-items-center">
                {{ task.title }}
                <button class="btn btn-danger" @click="deleteTask(task)">削除</button>
            </li>
        </ul>
    </div>
</template>

<script>
    export default {
        name: "Tasks",
        data() {
            return {
                title: null
            };
        },
        computed: {
            tasks() {
                // ストアのタスク一覧を出力
                return this.$store.state.task.tasks;
            }
        },
        methods: {
            // ストアのタスク一覧取得アクション実行
            async getTasks() {
                await this.$store.dispatch('fetchTasks');
            },
            // ストアのタスク追加アクションを実行
            async add()
            {
                await this.$store.dispatch('addTask', {title: this.title});
            },
            // ストアの削除アクションを実行
            async deleteTask(task)
            {
                await this.$store.dispatch('deleteTask', task);
            }
        },
        // 描画時にタスク一覧を取得
        mounted() {
            this.getTasks();
        }
    }
</script>

<style scoped>

</style>

本当は、フォームやタスクリスト内のタスクは別コンポーネントにした方がいいと思いますが、まだVueよくわからないので、ひとつにしちゃいました。このコンポーネントは

  • リスト取得
  • タスク追加
  • タスク削除

の機能があります。そもそもの機能はストアに実装しているので、ストアのプロパティやアクションを参照しています。

ルーティング設定

続いてVue Routerによるルーティングを行っていきます。

assets/js/routes.js
import Vue from 'vue';
import VueRouter from "vue-router";

import Tasks from "./components/Tasks";

Vue.use(VueRouter);
const router = new VueRouter({
    mode: 'history',
    routes: [
        {path: '/tasks', name: 'tasks', component: Tasks}
    ]
});

export default router;

/tasksに先ほど作成したTasksコンポーネントを割り当てます。これで、/tasksにアクセスした際に、Tasksコンポーネントが実行されます。

View実装

Viewの実装です。

assets/js/views/App.vue
<template>
    <router-view>

    </router-view>
</template>

<script>
    export default {
        name: "App"
    }
</script>

<style scoped>

</style>

ルーティングされたコンポーネントを表示するViewを作成します。

アプリの設定

最後にアプリの設定を行います。まずはwebpackの設定に追記します。

webpack.config.js
var Encore = require('@symfony/webpack-encore');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
     */
    .addEntry('app', './assets/js/app.js')
    //.addEntry('page1', './assets/js/page1.js')
    //.addEntry('page2', './assets/js/page2.js')

    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables @babel/preset-env polyfills
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

    // enables Sass/SCSS support
    //.enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes(Encore.isProduction())

    // uncomment if you're having problems with a jQuery plugin
    //.autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')

+    // Enable VueJs
+    .enableVueLoader()

;

module.exports = Encore.getWebpackConfig();

Vueを有効にします。

assets/js/app.js
/*
 * Welcome to your app's main JavaScript file!
 *
 * We recommend including the built version of this JavaScript file
 * (and its CSS file) in your base layout (base.html.twig).
 */

const axios = require('axios');

// any CSS you import will output into a single css file (app.css in this case)
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import '../css/app.css';

// Need jQuery? Install it with "yarn add jquery", then uncomment to import it.
// import $ from 'jquery';

import Vue from 'vue';
import Routes from './routes.js';
import App from './views/App';
import store from './stores/index';
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue';
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);


const app = new Vue({
    el: '#app',
    router: Routes,
    render: h => h(App),
    store
});

export default app;

作ってきたものをインポートして、appに紐付けます。これで開発完了です。

実行

DB設定

SQLiteで動作するよう、.envを設定します。
今回はローカル環境でのみSQLiteにしたかったので、.env.localを設定します。

env.local
APP_ENV=dev
DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db

設定したら、DBとテーブルを作成します。

bin/console doctrine:database:create
bin/console doctrine:schema:update --force

ビルド&サーバ起動

DBが設定できたら、ローカルでの実行はPHPのビルトインサーバを起動し、Vueをビルドします。

symfony server:start -d # もしくは php -S localhost:8000 -t public/
yarn run watch # yarn encore dev --watch

起動したら、http://localhost:8000/tasks にアクセスします。

Screen Shot 2020-02-27 at 18.10.10.png

タスクを登録したり、登録したタスクが削除できたりしました。
(見た目が悪かったのでassets/css/app.cssのbackground-colorをwhiteにしました)

これで、完成です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした