write ahead log

ロールフォワード用

Laravelで認証とVueの設定を試した時のメモ

必要に駆られて仕方なしに下調べした時のメモ。

  • Laravel11
  • Vue3
  • Vuetify3

あたりを使う。

TODOアプリ作ろうかと思ったけど、設定以降は目新しい事もなく単調なのでjsonplaceholder使った。

フレームワークを追うのは疲れる。

Laravel Sailの導入

環境構築が面倒なので今回はSailを使ってみることにした。

WSL2環境で試した。以下を参考。

Docker Installation Using Sail

以下のコマンドでプロジェクトを作れる。「todo-app」の部分を好きな名前にすればよい。

curl -s "https://laravel.build/todo-app" | bash

時間がかかるが、準備ができるとローカルにLaravelのプロジェクトディレクトリができる。

ターミナル出力の指示通り実行する。

cd todo-app && ./vendor/bin/sail up

sailは以下のようなコマンドが使える。

基本的にはdocker composeのエイリアスっぽい。

コマンド 機能
sail up [-d] コンテナ起動
sail down コンテナ終了
sail start コンテナの再開
sail stop コンテナの停止
sail shell コンテナのシェルに入る
sail test テスト実行
sail artisan コンテナ内でartisanを使う
sail npm コンテナ内でnpmを使う
sail node コンテナ内でnodeを実行
sail exec mysql mysql -u sail -p mysqlクライアントで接続。パスワードはpassword

参考: Laravel sail を使って Laravel9 の環境構築を行う

セッション用テーブルを準備して表示確認

とりあえず初期のマイグレーションを実行して動作確認する。

./vendor/bin/sail artisan migrate

これで以下のURLでアプリケーションへアクセスできる。

http://localhost

認証を追加する

とりあえず標準で入っているやつで追加したい。

コンテナ入って。

./vendor/bin/sail shell

モジュール入れて。

composer require laravel/ui --dev
php artisan ui vue --auth
npm install && npm run dev # 次回以降は ./vendor/bin/sail npm run devの方が便利

以下にアクセスし直して登録ページやログインページを動かしてみる。動くはず。

http://localhost

一応mysqlにデータが入っているかも見てみる。

./vendor/bin/sail exec mysql mysql -u sail -p
> use laravel # laravelデータベースへ接続
> show tables # テーブル一覧を一応確認
> select * from users \G #ユーザーテーブルを確認
> \q

認証済みのユーザーだけが見れるページを確認する

一応確認。

以下は認証済みでないと見れなくなっている。

http://localhost/home

生成されたコントローラーを見ると、middleware('auth')で認証チェックされている。

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

認証機能を変更する

メールアドレスとパスワードではなく、メールアドレスと電話番号を使って認証するようにする。

今回はユーザー登録とログインだけ考えてみる。

なぜこんな変な事をするかと言えば似たような事が仕事の要件になりそうだから。

DBを変更

電話番号のカラムを加えるマイグレーションを作る。

./vendor/bin/sail artisan make:migration add_tel_to_users --table=users

内容は雑だけどこんな感じで。

/**
 * Run the migrations.
 */
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        // nameの後にtelを追加
        $table->string('tel', 25)->after('name');
    });
}

/**
 * Reverse the migrations.
 */
public function down(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn('tel');
    });
}

一応状態を確認して。

./vendor/bin/sail artisan migrate:status

マイグレーションする。

./vendor/bin/sail artisan migrate

一応mysqlつないで確認してみる。

./vendor/bin/sail exec mysql mysql -u sail -p
# パスワードはpassword
> use laravel
> select * from users \G

認証用のコードを変更

ちょっと極端な例だけど、メールアドレスと電話番号でログインできるようにしてみる。

表示部分

とりあえず表示から変えてみる。

以下2ファイルに電話番号の入力欄を追加して、余分なものを消した。

  • resources/views/auth/login.blade.php
  • resources/views/auth/register.blade.php

ユーザーモデルの変更

userモデルのfillableにtel加えただけ。この例だとパスワードとかもホントはいらない。

protected $fillable = [
    'name',
    'tel',
    'email',
    'password',
];

ユーザー登録用コントローラーの変更

バリデーションと登録処理をちょっと変更。

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:255'],
        'tel' => ['required', 'string', 'max:25', 'regex:/^[0-9]+$/'],   # 数字だけにした
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', 'string', 'min:8', 'confirmed'],
    ]);
}

protected function create(array $data)
{
    return User::create([
        'name' => $data['name'],
        'tel' => $data['tel'], # 追加した
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);
}

とりあえず登録はできるようになった。

認証処理の変更

仕組みをちょっと調べる

仕組みを調べるにあたって軽く以下を眺めた。

本気で詳細を理解したい人向けのLaravelログイン認証

laravelの認証をカスタマイズできるようになる

ただ、見るべきコードはLaravel11では以下な気がする。

vendor/laravel/ui/auth-backend/

ここにあるAuthenticatesUsers.phpがLoginControllerで使われているtraitのよう。

このtraitのloginをオーバーライドするだけでも好き勝手できそう。

もうちょい良い方法はないかと探すと以下が見つかる。

Laravelの認証カスタマイズに関するメモ

Laravelで自作の認証を作成する

認証するユーザを探すプロバイダを作って提供すればよいらしい。

継承して差し替える元のコードはおそらくこれ。

vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php

ちょっとしんどいな。。。でも正しいアプローチだと思う。

あるいは強引にログインさせてしまうアプローチもあるらしい。

LaravelでユーザーIDのみでログインするのを作る

Laracast - Using Auth::attempt() without password

LoginControllerでオーバーライドして実装してみる

traitのloginをオーバーライドする方法を試してみる。

+use Illuminate\Http\Request;
 use App\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\AuthenticatesUsers;
+use App\Models\User;
+use Illuminate\Validation\ValidationException;
+use Illuminate\Support\Facades\Auth;

 class LoginController extends Controller
 {
@@ -37,4 +41,28 @@ public function __construct()
         $this->middleware('guest')->except('logout');
         $this->middleware('auth')->only('logout');
     }
+
+    /**
+     * 認証を行う
+     * vendor/laravel/ui/auth-backend/AuthenticatesUsers.phpをオーバーライド
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
+     *
+     * @throws \Illuminate\Validation\ValidationException
+     */
+    public function login(Request $request) {
+        $user = User::where([
+            'email' => $request->email,
+            'tel' => $request->tel,
+        ])->first();
+        if ($user) {
+            Auth::loginUsingId($user->id);
+            return redirect('/home');
+        }
+        throw ValidationException::withMessages([
+            'email' => 'メールアドレスか電話番号に誤りがあります',
+        ]);
+    }
+

強引な感じもするが、単純な業務アプリならこのくらいシンプルじゃないと実装しんどい。

TODOアプリの見た目を作る

この辺を参考にしつつやった。ほぼリンクそのままやる。

/app以下をSPAにした。

たらおブログ - Vue3 × Vuetify3 × Laravelでタスク管理アプリ作ってみた

vue3自体は認証を作る時にlaravel-uiと一緒に入ったはずなので、追加で以下を導入する。

  • vue-router
  • vuetify

vue-routerの導入

./vendor/bin/sail shell
npm install vue-router

resources/js/router.jsを用意する。参考サイトそのまんま。

import { createRouter, createWebHistory } from 'vue-router';
import Task from './components/Task.vue';
import Show from './components/Show.vue';

const routes = [
    {
        path: '/app/tasks',
        name: 'tasks',
        component: Task
    },
    {
        path: '/app/tasks/:id',
        name: 'show',
        component: Show
    }
];

const router = createRouter({
    history: createWebHistory(),
    routes
});

export default router;

resources/js/app.jsを編集してrouterを使うようにする。

// これを加えて
import router from './router';

// ExampleComponentはやめてAppを用意することにした
import AppComponent from './App.vue';
app.component('app', AppComponent);

// useを挟む
app.use(router).mount('#app');

次に仮のコンポーネントを用意する。

まずタスク一覧。resources/js/components/Task.vue

<script setup>
</script>

<template>
    <h1>タスク一覧</h1>
</template>

次にタスク詳細。resources/js/components/Show.vue

<script setup>
</script>

<template>
    <h1>タスク詳細</h1>
</template>

最後にルーティングするメインのコンポーネント。resources/js/App.vue

<script setup>
import { RouterView } from 'vue-router';
</script>

<template>
   <RouterView />
</template>

laravelのテンプレートを用意する。

resources/view/app.blade.php

@extends('layouts.app')

@section('content')
    <app></app>
@endsection

Laravelのルーティングを追加する。app以下はvue-routerのページに任せる。

Route::prefix('app')->middleware('auth')->group(function() {
    Route::get('/{any}', function () {
        return view('app');
    })->where('any', '.*');
});

vuetifyを導入する

とりあえず入れる。

./vendor/bin/sail shell
npm install vuetify
npm install @mdi/font -D # アイコンフォント

resources/js/app.jsを編集する。

// Vuetifyのインポートを加えてuseする
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

const vuetify = createVuetify({
  components,
  directives,
  icons: {
    defaultSet: 'mdi', 
  }, 
})

createApp(App).use(vuetify).use(router).mount('#app');

ここまでで設定できた。

resources/js/components/Task.vueとかに適当なコンポーネントを書けばとりあえず動く。

vuetifyはとりあえずdatepickerを出した。この辺りを適当にみる。

https://vuetifyjs.com/en/components/date-pickers/#date-pickers

https://vuetifyjs.com/en/api/v-date-picker/#links

データはjsonplaceholderでとってそのまま出した。

<script setup>
import { onMounted } from 'vue'
import { ref } from 'vue'
import axios from 'axios'

const selectedDate = ref(new Date)
const todos = ref([])
const menu = ref(false)
const handleOpen = () => {
    menu.value = true
}

const getTodos = async () => {
    const todos = await axios.get('https://jsonplaceholder.typicode.com/todos')
    return todos.data
}
onMounted(async () => {
    todos.value = await getTodos()
})
</script>

<template>
    <h1>タスク一覧</h1>
    <label>{{ selectedDate?.toLocaleDateString('ja-jp', 'YYYY-MM-DD') }}</label>
    <button @click="handleOpen">open</button>
    <v-dialog
      v-if="menu"
      v-model="menu"
      :close-on-content-click="true"
      full-screen
    >
        <v-locale-provider locale="ja">
            <v-date-picker
              title="日付選択"
              v-model="selectedDate"
              color="primary"
            ></v-date-picker>
        </v-locale-provider>
    </v-dialog>
    <ul>
        <li v-for="(todo, index) in todos">
            {{ todo.title }}
        </li>
    </ul>
</template>