【備忘録】DockerでLaravel + Vue環境構築（Nuxtなしver）

## 目的

Nuxtを使用しないプロジェクトに参画するため、vue-routerやvuexの設定等を復習したい。

## 環境

* PHP7.3
* Laravel7.30
* vue 2.6.12

## 手順
* Dockerでローカルの環境構築
* TypeScript（とデコレータ）を導入する
* コード整形ツールを導入する
* browsersyncの設定をし、ブラウザのリロードの手間を省略する
* vuex, vue-routerの設定をする
* 別記事でログイン機能も実装する

## Dockerでローカルの環境構築

### プロジェクト用ディレクトリを用意

```terminal:Terminal
$ mkdir -p ~/workspace/myapp
$ cd ~/workspace/myapp
$ mkdir -p laravel mnt/mysql /docker/{php,nginx}
$ mkdir docker/nginx/{conf.d,logs}
```
### .envの準備

#### 先に.env.exampleを編集してからコピーして.envをつくる

```terminal:myapp
$ touch .env.example .gitignore
```

#### .env.exampleと.gitignoreを編集

```.env:.env.example
PROJECT_PATH=./laravel
TZ=Asia/Tokyo
WEB_PORT=10080
DB_PORT=13306
DB_TESTING_PORT=13307
DB_CONNECTION=mysql
DB_NAME=myapp
DB_USER=willrewrite
DB_PASS=willrewrite
DB_ROOT_PASS=willrewrite
MAILHOG_PORT=18025
MAIL_HOST=mail
MAIL_PORT=1025

COMPOSE_HTTP_TIMEOUT=70
```

```:.gitignore
.env
.idea
# その他個別に追加
```

#### .env.exampleをコピー

```terminal:myapp
$ cp .env.example .env 
```

コピー後、個別のプロジェクトに合わせて.envの環境変数を上書きする

### 必要なファイルを作成

```terminal:myapp
$ touch docker-compose.yml \
docker/php/{php.ini,Dockerfile} \
docker/nginx/{conf.d/default.conf,Dockerfile} \
docker/mysql/mysql_conf/my.cnf \
mnt/mysql/.gitignore
```

### 各ファイルを編集

```yml:docker-compose.yml

version: "3"

services:
  web:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
      args:
        - TZ=${TZ}
    volumes:
      - ./docker/nginx/logs:/etc/nginx/logs
      - ./docker/nginx/conf.d:/etc/nginx/conf.d
      - ${PROJECT_PATH}:/var/www/laravel
    ports:
      - ${WEB_PORT}:80
    links:
      - app
    depends_on:
      - app
  
  app:
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
      args:
        - TZ=${TZ}
    volumes:
      - ${PROJECT_PATH}:/var/www/laravel
    links:
      - db_master
    environment:
      - DB_CONNECTION=${DB_CONNECTION}
      - DB_HOST=db_master
      - DB_DATABASE=${DB_NAME}
      - DB_USERNAME=${DB_USER}
      - DB_PASSWORD=${DB_PASS}
      - TZ=${TZ}
      - MAIL_HOST=${MAIL_HOST}
      - MAIL_PORT=${MAIL_PORT}

  db_master:
    image: mysql:5.7
    volumes:
      - ./mnt/mysql:/var/lib/mysql
      - ./docker/mysql/mysql_conf:/etc/mysql/conf.d
    environment:
      - MYSQL_DATABASE=${DB_NAME}
      - MYSQL_USER=${DB_USER}
      - MYSQL_PASSWORD=${DB_PASS}
      - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASS}
      - TZ=${TZ}
    ports:
      - ${DB_PORT}:3306
```

#### Nginxの設定

```Dockerfile:docker/nginx/Dockerfile
FROM nginx:1.15.7-alpine
# timezone
ARG TZ
COPY ./docker/nginx/conf.d/ /etc/nginx/conf.d/
COPY ./docker/nginx/logs/ /etc/nginx/logs/
COPY ./laravel/ /var/www/laravel/
RUN apk update && apk --update add tzdata && \
  cp /usr/share/zoneinfo/${TZ} /etc/localtime && \
  apk del tzdata && \
  rm -rf /var/cache/apk/*
```

```conf:docker/nginx/conf.d/default.conf
server {
    listen       0.0.0.0:80;
    # server_nameで設定した名前をローカルマシンの/etc/hostsにも設定する
    server_name  www.example.org example.org;
    charset      utf-8;
    client_max_body_size 5M;

    root /var/www/laravel/public;

    index index.php;

    location / {
        access_log  /etc/nginx/logs/access.log main;
        error_log   /etc/nginx/logs/error.log;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass  app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include       fastcgi_params;
    }
}
```

#### PHPの設定
```Dockerfile:docker/php/Dockerfile

FROM php:7.3.0RC6-fpm-alpine3.8
# timezone
ARG TZ
WORKDIR /var/www/laravel
COPY ./docker/php/php.ini /usr/local/etc/php/
COPY ./laravel/ /var/www/laravel/

RUN apk upgrade --update && apk add --no-cache \
  coreutils freetype-dev jpeg-dev libjpeg-turbo-dev libpng-dev libmcrypt-dev \
  git vim unzip tzdata \
  libltdl && \
  docker-php-ext-install pdo_mysql mysqli mbstring && \
  docker-php-ext-install -j$(nproc) iconv && \
  docker-php-ext-configure gd \
  --with-freetype-dir=/usr/include/ \
  --with-jpeg-dir=/usr/include/ && \
  docker-php-ext-install -j$(nproc) gd && \
  cp /usr/share/zoneinfo/${TZ} /etc/localtime && \
  apk del tzdata && \
  rm -rf /var/cache/apk/*

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
```

```ini:docker/php/php.ini
log_errors = on
error_log = /var/log/php/php-error.log
upload_max_filesize = 5M
memory_limit = -1
post_max_size = 100M
max_execution_time = 900
max_input_vars = 100000
default_charset = UTF-8

[Date]
date.timezone = ${TZ}
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"
```

#### mysqlの設定

```cnf:docker/mysql/mysql_conf/my.cnf
[client]
default-character-set=utf8

[mysql]
default-character-set=utf8

[mysqldump]
default-character-set=utf8

[mysqld]
character-set-server=utf8
```

#### mysqlのマウントによるファイルはコード管理の対象外
```.gitignore:mnt/mysql/.gitignore
/*
!/.gitignore
```

#### 立ち上げ
```terminal:myapp
$ docker-compose run app composer create-project --prefer-dist "laravel/laravel=7.*" .
$ docker-compose up -d
$ docker-compose exec app ash
```

```terminal:/var/www/laravel
$ php artisan -V // バージョン確認
$ php artisan migrate
$ chmod -R a+rw storage/ bootstrap/cache
$ exit
$ cd laravel
$ npm install
```

### デフォルトの認証機能を使う場合
webpack.mix.jsを上書きされる前に、このタイミングで公式ドキュメントどおりコマンドを打つ

```terminal:~/workspace/myapp
$ docker-compose exec app ash

// バージョン指定しないとエラーが出る
/var/www/laravel # composer require laravel/ui:^2.4
/var/www/laravel # php artisan ui vue --auth
```

### TypeScript（とデコレータ）を導入する

```terminal:~/workspace/myapp/laravel
$ npm i -D vue vue-template-compiler　\
ts-loader \
typescript \
vue-property-decorator \
vue-mixin-decorator

$ mv resources/js resources/ts resources/ts/app.ts
$ touch tsconfig.json resources/ts/vue.shims.d.ts resources/views/index.blade.php
```

```json:tsconfig.json
{
  "files": [
    "resources/ts/vue.shims.d.ts"
  ],
  "compilerOptions": {
    "outDir": "./public/",
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "module": "es2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "target": "es5",
    "lib": [
      "es2016",
      "dom"
    ]
  },
  "include": [
    "resources/ts/**/*"
  ]
}
```

```ts:resources/ts/vue.shims.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
```

```php:routes/web.php
<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/{any?}', function () {
  return view('index');
})->where('any', '.+');

```

```php:resources/views/index.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{ config('app.name') }}</title>
  <!-- Scripts -->
  <script src="{{ mix('js/app.js') }}" defer></script>
  <!-- Fonts -->
  <link rel="dns-prefetch" href="//fonts.gstatic.com">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Merriweather%7CRoboto:400">
  <link rel="stylesheet" href="https://unpkg.com/ionicons@4.2.2/dist/css/ionicons.min.css">
  <!-- Styles -->
  <link href="{{ mix('css/app.css') }}" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
</body>

</html>
```

```resources/ts/app.ts
require('./bootstrap');

import Vue from 'vue'
new Vue({
  el: '#app',
  template: '<h1>Hello world</h1>'
})
```

## コード整形ツールを導入する

```config:.editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2
```


```terminal:~/workspace/myapp/laravel
$ npm i -D eslint \
eslint-config-prettier \
eslint-plugin-prettier \
prettier \
babel-eslint \
eslint-plugin-vue \
vue-eslint-parser \
@nuxtjs/eslint-config-typescript

$ touch .eslintrc
```

```:.eslintrc
{
  "env": {
    "es6": true,
    "node": true
  },
  "extends": [
    "@nuxtjs/eslint-config-typescript",
    "eslint:recommended",
    "plugin:prettier/recommended",
    "plugin:vue/recommended",
    "prettier/vue"
  ],
  "parser": "vue-eslint-parser",
  "parserOptions": {},
  "rules": {
    "strict": 0,
    "prettier/prettier": [
      "error",
      {
        //ここはprettierの公式ドキュメントのoptionsを見てカスタマイズする
        "singleQuote": true,
        "trailingComma": "es5",
        "semi": false,
        "endOfLine": "lf"
      }
    ]
  }
}
```


## browsersyncの設定
```terminal
$ cd laravel
$ npm i -D browser-sync browser-sync-webpack-plugin
```


```js:webpack.mix.js
/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

const mix = require('laravel-mix')
mix.browserSync('example.org:10080')
  .ts('resources/ts/app.ts', 'public/js')
  .sass('resources/sass/app.scss', 'public/css')
  .version()

const path = require('path')

mix.webpackConfig({
  resolve: {
    extensions: ['.js', '.ts', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': path.resolve('/resources/ts'),
    },
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: { appendTsSuffixTo: [/\.vue$/] },
        exclude: /node_modules/
      }
    ]
  }
})
```


### 一旦画面確認

```terminal:~/workspace/myapp/laravel
$ npm run watch
```

localhost:3000にアクセスし、`Hello world`の初期画面が出ればOK


## Userモデルの移動関連
```terminal:var/www/laravel
/var/www/laravel # mkdir app/Models && \
mv app/User.php app/Models/
```

```php:app/Models/User.php
<?php

// App\Modelsに修正
namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{

  // 以下略
```

```php:database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\User;　　// ここを修正
use Faker\Generator as Faker;
use Illuminate\Support\Str;

// 以下略
```

```php:config/auth.php

<?php

return [

  // 中略
  'providers' => [
    'users' => [
      'driver' => 'eloquent',
      'model' => App\Models\User::class,  // ここを修正
    ],
  ],

  // 以下略
```

## カラムの最大長を設定

```php:app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Schema;

// (中略)

public function boot()
{
  Schema::defaultStringLength(191);
}
```

## vuex, vue-routerの設定をする

```terminal:~/workspace/myapp/laravel
$ npm i -D vue-router vuex
$ mkdir resources/ts/pages
$ touch resources/ts/pages/{Index,Login}.vue \
resources/ts/App.vue \
resources/ts/router.ts
```

```vue:resources/ts/pages/Index.vue
<template>
  <div>Index page.</div>
</template>

```

```vue:resources/ts/pages/Login.vue
<template>
  <div>Login page.</div>
</template>
```

```vue:resources/ts/App.vue
<template>
  <div>
    <main>
      <div class="container">
        <router-view />
      </div>
    </main>
  </div>
</template>
```

```ts:resources/ts/router.ts
import Vue from 'vue'
import VueRouter from 'vue-router'
// ページコンポーネントをインポートする
import Index from './pages/Index.vue'
import Login from './pages/Login.vue'
// VueRouterプラグインを使用する
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    component: Index
  },
  {
    path: '/login',
    component: Login
  }
]
// VueRouterインスタンスを作成する
const router = new VueRouter({
  mode: 'history',    //URL中の#(ハッシュ)を消す
  routes
})
// VueRouterインスタンスをエクスポートする
export default router
```

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

```

### vuexの下準備
```terminal:~/workspace/myapp/laravel
$ mkdir resources/ts/store
$ touch resources/ts/store/index.ts
```

```ts:resources/ts/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    // ここに随時追加
  },
})

export default store
```

```ts:resources/ts/app.ts
import Vue from 'vue'
import router from './router'
import App from './App.vue'
import store from './store'  // 追加

new Vue({
  el: '#app',
  router,
  store,  // 追加
  components: { App },
  template: '<App />'
})

```
### 画面確認

```
$ npm run watch
```

**localhost:3000/login**にアクセスし、**Login page.**の文字が表示されたらOK