【これを読めばわかる】Laravel と Vue.js のWEBページを動かすまでにやること

# この記事について

Laravel と Vue.js をつかったWEBサイトをつくる最低限の工程を書いています。
「これを読めばLaravel + Vue.js でページを作れる」となる程度に詳細を書いていきます。

# composer インストール

[Macでインストールする方法](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos)をチェック

# Laravel インストール

以下で、Laravelのインストールとプロジェクト作成ができます。

```bash
composer create-project --prefer-dist laravel/laravel プロジェクト名
```

参考: [laravelインストール公式ページ](https://laravel.com/docs/5.7#installing-laravel)

# npm インストール

```bash
git clone https://github.com/creationix/nvm.git ~/.nvm 
source ~/.nvm/nvm.sh
nvm install v9.5.0
```

# ライブラリをインストール

```bash
cd ライブラリのディレクトリ
npm install
```

# `routes/web.php` を修正

```diff
Route::get('/', function () {
-    return view('welcome');
+    return view('index');
});
```

# index.blade.php を作成

```html:resources/views/index.blade.php
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">

  <meta name="theme-color" content="#282828">
  <meta name="msapplication-navbutton-color" content="#282828">

  <link rel="icon" href="{{ asset('/img/favicon.ico') }}" type="image/x-icon">
  <link rel="shortcut icon" href="{{ asset('/img/favicon.png') }}" type="image/png">
  <link rel="apple-touch-icon" href="{{ asset('/img/favicon.png') }}">

  <title>@yield('title') | {{ config('app.name') }}</title>
  <meta name="description" itemprop="description" content="@yield('description')">
  <meta property="og:type" content="website">
  <meta property="og:locale" content="ja_JP">
  <meta property="og:title" content="@yield('title') | {{ config('app.name') }}">
  <meta property="og:image" content="{{ asset('/img/favicon.png') }}">
  <meta property="og:description" content="@yield('description')">
  <meta property="og:url" content="">
  <meta property="og:site_name" content="">

  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:site" content="@" />
  <meta name="twitter:title" content="@yield('title') | {{ config('app.name') }}">
  <meta name="twitter:description" content="@yield('description')">
  <meta name="twitter:image" content="{{ asset('/img/favicon.png') }}">

  <meta name="csrf-token" content="{{ csrf_token() }}">
  <!-- <script src="{{ asset('js/app.js') }}"></script>
  <link href="{{ asset('css/app.css') }}" rel="stylesheet">
  <link href="{{ asset('css/reset.css') }}" rel="stylesheet"> -->

  <link rel="stylesheet" href="{{ mix('/css/app.css') }}">
</head>
<body>
    <div id="app">
        @yield('content')
    </div>
    <noscript>Javascriptを有効にしてください。</noscript>
    <script src=" {{ mix('js/app.js') }} "></script>
    <!-- @yield('js') -->
</body>
</html>
```

`index.blade.php`のコードで、以下の部分が肝になります。

```html:resources/views/index.blade.php
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ mix('/css/app.css') }}">
```

```html:resources/views/index.blade.php
<script src=" {{ mix('js/app.js') }} "></script>
```

# `app.js`を修正

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

window.Vue = require('vue');
+ import App from './app.vue'

const app = new Vue({
    el: '#app',
+   render: h => h(App),
});
```

参考: [`render: h => h(App)`についてはこちらを参考に](https://qiita.com/teinen_qiita/items/ed1bb1909a17f9ca9daa)

# `app.vue`を作成

```vuejs:resources/js/app.vue
<template>
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-md-8">
        <div class="card card-default">
          <div class="card-header">{{ header }}</div>

          <div class="card-body">{{ body }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log('Component mounted.')
  },
  data() {
    return {
      header: "App Vue Header",
      body: "App Vue Body",
    }
  }
}
</script>
```

# Laravel 開発サーバー起動

```bash
php artisan serve
```

# Vueコンポーネントのコンパイル

```bash
npm run hot
```

# JWT 認証

APIを使うときの認証はJWTを使うことにします。
jwt-auth というライブラリをつかっていきます。

## インストール

```
composer require tymon/jwt-auth 1.0.0-rc3
```

## Laravel 5.4以下の場合は、 `config/app.php` にServiceProviderを追加

```
'providers' => [
    ...
    Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]
```

## configファイルを生成

```
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
```

`config/jwt.php` が作成されます。

## シークレットキーを生成

```
php artisan jwt:secret
```

`.env`に`JWT_SECRET=...`が書き込まれます。

[参考](https://jwt-auth.readthedocs.io/en/develop/laravel-installation/)

## Userモデルを編集

```diff
<?php

namespace App;

+ use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

- class User extends Authenticatable
+ class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // Rest omitted for brevity

+    /**
+     * Get the identifier that will be stored in the subject claim of the JWT.
+     *
+     * @return mixed
+     */
+    public function getJWTIdentifier()
+    {
+        return $this->getKey();
+    }

+    /**
+     * Return a key value array, containing any custom claims to be added to the JWT.
+     *
+     * @return array
+     */
+    public function getJWTCustomClaims()
+    {
+        return [];
+    }
}
```

## APIのGuardを編集

```diff
'defaults' => [
-    'guard' => 'web',
+    'guard' => 'api',
    'passwords' => 'users',
],
...
'guards' => [
  'web' => [
    'driver' => 'session',
    'provider' => 'users',
  ],
  'api' => [
-    'driver' => 'token',
+    'driver' => 'jwt',
    'provider' => 'users',
  ],
],
```

## 認証routeを追加

```php:routes/api.php
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
  Route::post('login', 'AuthController@login');
  Route::post('logout', 'AuthController@logout');
  Route::post('refresh', 'AuthController@refresh');
  Route::post('me', 'AuthController@me');
});
```

## `AuthController`作成

```bash
php artisan make:controller AuthController
```

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
  /**
   * Create a new AuthController instance.
   *
   * @return void
   */
  public function __construct()
  {
    $this->middleware('auth:api', ['except' => ['login']]);
  }

  /**
   * Get a JWT via given credentials.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function login()
  {
    $credentials = request(['uid', 'password']);

    if (! $token = auth()->attempt($credentials)) {
      return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
  }

  /**
   * Get the authenticated User.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function me()
  {
    return response()->json(auth()->user());
  }

  /**
   * Log the user out (Invalidate the token).
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function logout()
  {
    auth()->logout();

    return response()->json(['message' => 'Successfully logged out']);
  }

  /**
   * Refresh a token.
   *
   * @return \Illuminate\Http\JsonResponse
   */
  public function refresh()
  {
    return $this->respondWithToken(auth()->refresh());
  }

  /**
   * Get the token array structure.
   *
   * @param  string $token
   *
   * @return \Illuminate\Http\JsonResponse
   */
  protected function respondWithToken($token)
  {
    return response()->json([
      'access_token' => $token,
      'token_type' => 'bearer',
      'expires_in' => auth()->factory()->getTTL() * 60
    ]);
  }
}
```

## 認証する

`http://example.dev/api/auth/login` で認証されるようになりました。


# Laravel APIで uid を認証

## vue.js でIDトークンを取得

```
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
  // Send token to your backend via HTTPS
  // ...
}).catch(function(error) {
  // Handle error
});
```

[参照](https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja#retrieve_id_tokens_on_clients)

## Vue.js から Laravel API にIDトークンを送信

`Authorization: Bearer {IDトークン}` をヘッダーにつけて送信する。

## Laravel 側でIDトークンを確認

php用のfirebase サードパーティ sdk

```
composer require kreait/firebase-php ^4.18
```

[参考](https://firebase-php.readthedocs.io/en/stable/overview.html#installation)

public key をキャッシュするため`symfony/cache`をインストール

```
composer require symfony/cache
```

上で作成した認証用の`AuthController`を変更していきたいと思います。

```php
use Kreait\Firebase\Factory;
use Kreait\Firebase\ServiceAccount;
use Symfony\Component\Cache\Simple\FilesystemCache;

class AuthController extends Controller
{
  ... 

  public function login()
  {
    // email じゃなくて uid を使いたいので email を削除
    $credentials = request(['password']);

    // AuthorizationヘッダーからFirebaseのIDトークンを取得
    $headers = getallheaders();
    if (!isset($headers['Authorization'])) {
      return response()->json(['error' => self::ERROR_NO_ID_TOKEN], 401);
    }
    $authorization = $headers['Authorization'];
    $explodedAuthorization = explode(' ', $authorization);
    $idTokenString = $explodedAuthorization[1];
    if (!$idTokenString) {
      return response()->json(['error' => self::ERROR_NO_ID_TOKEN], 401);
    }

    // IDトークンを確認してUIDを取得
    $cache = new FilesystemCache();
    $serviceAccount = ServiceAccount::fromJsonFile('/google-service-account.json');
    $firebase = (new Factory)->withServiceAccount($serviceAccount)->withVerifierCache($cache)->create();
    try {
      $verifiedIdToken = $firebase->getAuth()->verifyIdToken($idTokenString);
    } catch (InvalidToken $e) {
        return response()->json(['error' => $e->getMessage()], 401);
    }
    $credentials['uid'] = $verifiedIdToken->getClaim('sub');

    // ユーザー未登録なら登録
    $user = User::where('uid', '=', $credentials['uid'])->first();
    if (!$user) {
      $result = User::create([
        'uid' => $credentials['uid'],
        'password' => Hash::make($credentials['password']),
      ]);
    }

    // JWT取得
    if (!$token = auth()->attempt($credentials)) {
      return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $this->respondWithToken($token);
  }
```


<!-- # Passportを使ったAPI通信

Vue.jsでDBとやりとりをするためには、LaravelのAPIとやりとりします。
LaravelのAPIを使うには認証を入れるのがベターです。
LaravelのAPI認証は、Passportを使います。

## インストール

```bash
composer require laravel/passport
```

## Laravel Passport関連のテーブルが作成

```bash
php artisan migrate
```

## 暗号キー生成

```bash
php artisan passport:install
```

## `app/User.php` 編集

```diff
+ use Laravel\Passport\HasApiTokens;
// ...
 
class User extends Authenticatable
{
-    use Notifiable;
+    use HasApiTokens, Notifiable;

    // ...
}
```

## `app/Providers/AuthServiceProvider.php` 編集

- アクセストークンを発行
- アクセストークンを失効
- クライアントとアクセストークンの管理

```diff
+ use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    // ...

    public function boot()
    {
        $this->registerPolicies();

+        Passport::routes();
    }
}
```

## `config/auth.php` 編集

```diff
'guards' => [
  'web' => [
    'driver' => 'session',
    'provider' => 'users',
  ],
  'api' => [
-    'driver' => 'token',
+    'driver' => 'passport',
    'provider' => 'users',
  ],
],
``` -->

# 管理画面

[laravel-admin](https://laravel-admin.org/)という管理画面ライブラリを使います。

## 初期設定
```
# composer
composer require encore/laravel-admin
# assets, config(config/admin.php) を作成
php artisan vendor:publish --provider="Encore\Admin\AdminServiceProvider"
# install
php artisan admin:install
```

## コントローラー作成
```
# コントローラ作成
php artisan admin:make UserController --model=App\\User
# app/Admin/routes.php にルートを追加
$router->resource('demo/users', UserController::class);
```

メニューにパスを追加するには、`admin/auth/menu`にアクセスして追加する。

[参考](https://laravel-admin.org/docs/#/en/installation)

## ファイルアップロード設定
ファイルのアップロード先のルートパスと、ファイルのアクセスURLを設定。
`public/uploads`にファイルがアップロードされ、`http://xxxx/uploads`でファイルにアクセスできるようにする。

```php:config/filesystems.php
'disks' => [
    ... ,

    'admin' => [
        'driver' => 'local',
        'root' => public_path('uploads'),
        'visibility' => 'public',
        'url' => env('APP_URL').'/uploads',
    ],
],
```

以下を設定すると、画像ファイルは`public/uploads/image`に、そのほかのファイルは`public/uploads/file`にアップロードされます。
それぞれ`http://xxxx/uploads/image`、`http://xxxx/uploads/file`でアクセスできます。

```php:config/admin.php
'upload'  => [

    'disk' => 'admin',

    'directory'  => [
        'image'  => 'image',
        'file'   => 'file',
    ],
],
```

アップロード用のフォームを作成する場合は、コントローラーを編集します。

```php:ExamleController.php
protected function form()
{
    $form = new Form(new Example);
    // uploads/image 直下にアップロードしたい場合
    $form->image('image_url', '画像');
    // uploads/image/xxxx にアップロードしたい場合
    $form->image('image_url', '画像')->move('image/xxxx');
}
```

`move`を使うと、`config/filesystems.php`の`disks.admin.root`以下のパスを指定することになるので、`image`を定数にしてこんな風にするのが良いかと思います。

```php:config/admin.php
'upload'  => [
    'directory'  => [
        'image'  => env('UPLOAD_IMAGE_PATH'),
```

```php:ExamleController.php
$form->image('image_url', '画像')->move(env('UPLOAD_IMAGE_PATH') . '/xxxx');
```

アップロードしたファイルの表示は以下のようにします。

```php:ExamleController.php
protected function grid()
{
    $grid = new Grid(new Example);
    $grid->image_url('画像')->image();
    ...
}
```

[参考](https://laravel-admin.org/docs/#/en/model-form-upload)