Blogブログ

laravel

2024.08.31

laravel11を使ってwebサービス開発するための入門解説

目次

larave11いつの間にかでていたので、書かないとなと思って半年がすぎました。
laravel10に引き続きすぐにバージョンあがりやがって。。

あまりlaravel11の解説などは話題になっていないのは、
新規の追加機能はそこまでないからみたいですね。

でもlaravel11の解説や入門で探すと..

laravel11の解説でいいなと思ったサイトその1

kinstaさんというサイトがわかりやすくシンプルに解説をしていました。
もう差分はkinstaさんのlaravel11の解説を見ればいいかと思います

Laravel 11のもう一つの重要な変更点として、PHP 8.2が最低要件に設定されています。Laravel 11では、PHP 8.2がデフォルトとなり、PHP 8.3も予定されています。PHPエコシステムの最新の進歩に合わせることで、LaravelでPHPの最新(および近々リリースされる)の言語機能と最適化の互換性を保つことができます。

php8.2以上になったことで、
phpバージョンアップ大変なんだよねって方は多いと思いますが、
docker-composeなどで管理していると
コンテナのphpのバージョン変えるだけですからね。
すごく簡単な時代になりました。

laravel11の解説で良いなと思ったサイトその2

他にも検索してみるとおもいっきしpdfだったんですけどsocymさんがLaravel11_Support_Guide_1_3.pdfの中でイラストを交えて解説していました。(ライオンと犬が合わさったようなキャラクターが解説しています)

あれ。。検索してみるとyoutubeでもこのキャラクターが…

猫なのかライオンなのか、、凛々しいキャラクターがlaravel11を解説している..
愛着湧きます。

laravel11の解説入門で良いなと思ったサイトその3

たぶんみなさん見ていると思いますがキータの中ではhitochiさんのlaravel11の解説が一番わかりやすいかな。
php8の知識があった方がわかりやすいという記載があったのですが
php8からは、ユニオン型が使えるので、たしかにlaravel11でも可読性のためによく使ってるイメージです。
例えば、

class UserController extends Controller
{
    public function store(Request $request): int|float
    {
        $value = $request->input('value');
        return $value;
    }
}

こんな感じにできるのはphp8の恩恵です。

あとは、
クラスの初期化に関しても
コンストラクタの引数から直接クラスのプロパティを宣言および初期化することができるのもphp8の恩恵なのでlaravel11でも使っていけます。

class Point {
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
    ) {}
}

laravel11の解説入門で良いなと思ったサイトその4

takedaさんという方が書かれているlaravel11の解説はエンジニアとして一番スマートで分かりやすかったです。
感動するレベルでわかりやすいのでおすすめです。

laravel11の移行の概要は..

  1. 雛形を変更した箇所:
    • その実装を所定の場所に移動し、元のファイルを削除。
  2. 変更していない箇所:
    • 単に元のファイルを削除。

laravel11の新形式の概要

Laravel 11では、設定の多くがbootstrap/app.phpに集約され、以下のように変更されました。

  • ミドルウェアの設定:
    • Laravel 10の各ミドルウェアクラスはbootstrap/app.phpに集約。
  • サービスプロバイダ:
    • config/app.phpからbootstrap/providers.phpに移動。

laravel11の移行手順

  1. 最初のステップ:
    • bootstrap/app.phpを新形式に変更。
  2. ミドルウェア設定:
    • 各ミドルウェアの設定をbootstrap/app.phpに移動。
    • 例: 認証済判定、クッキーの暗号化、メンテナンスモードなど。
  3. ルートやコマンドの設定:
    • ルート設定やタスクスケジュールの設定をbootstrap/app.phpに移動。
  4. サービスプロバイダの統合:
    • AppServiceProviderに統合。

laravel11の主な機能の移行先

  1. ルートの保護/認証済判定:
    • app/Http/Middleware/Authenticate.php ➔ bootstrap/app.php
  2. クッキーと暗号化:
    • app/Http/Middleware/EncryptCookies.php ➔ bootstrap/app.php
  3. メンテナンスモード:
    • app/Http/Middleware/PreventRequestsDuringMaintenance.php ➔ bootstrap/app.php
  4. ルーティング:
    • app/Providers/RouteServiceProvider.php ➔ bootstrap/app.php
  5. タスクスケジュール:
    • app/Console/Kernel.php ➔ routes/console.php
  6. 認可:
    • app/Providers/AuthServiceProvider.php ➔ app/Providers/AppServiceProvider.php
  7. エラー処理:
    • app/Exceptions/Handler.php ➔ bootstrap/app.php

Laravel 11では、ぱっとみスリムな構造を導入していて、
必要な設定のみを少数のファイルに集約したという感じだとわかります。
これにより、アプリケーションの初期状態をスリムに保ちつつ、カスタマイズの自由度を高めることができます。

これを見て自分もlaravel11で基本的な流れを解説しながらマッチングアプリを作ってみようかなと思いました。

まじでゼロから環境構築とサービスの開発をしてみようと思います。

ちなみに差分とかはどうでもよく単純に11使って書いていくだけです。

dockerさえあるならたぶん同じことができると思うので、
laravel11の入門としてなぞってみてもらえればと思います。

きっと動きます。

何を作ろうかと社内で話と時に
「20代女性社員2名からマッチングアプリを作りたい」という切実な要望を受けたこともあり、
じゃあそれの触りの部分だけlaravel11で作ってみようと思いました。

ちなみに私たちは、
バンコクにて新しい子会社を作りました。

その現地担当の志望者は梶村です。

今梶村はバンコクに引っ越しました(本日の9時の便で。)

というわけで、バンコクよりお届けします。

まずはlaravel11入門解説のために、 docker-compose.ymlから作りましょう。

まずプロジェクトフォルダを作ってそこに docker-compose.ymlを書きます。

version: "3"
volumes:
  php-fpm-socket:
  mysql-data:

services:
  mysql:
    image: mysql:8.0.36
    container_name: laravel_mysql
    volumes:
      - mysql-data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: user
      MYSQL_PASSWORD: password

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: laravel_phpmyadmin
    restart: always
    ports:
      - "8889:80"
    environment:
      PMA_ARBITRARY: 1
      PMA_HOST: mysql
      PMA_USER: root
      PMA_PASSWORD: root
    depends_on:
      - mysql

  php:
    build:
      context: .
      dockerfile: ./php/Dockerfile
    container_name: laravel_php
    volumes:
      - ./nginx/html:/usr/share/nginx/html
      - ./apps:/var/www/html:delegated
      - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini
      - type: volume
        source: php-fpm-socket
        target: /var/run/php-fpm
        volume:
          nocopy: true
    depends_on:
      - mysql

  nginx:
    image: nginx:latest
    container_name: laravel_nginx
    volumes:
      - ./nginx/conf.d/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
      - ./apps:/var/www/html:delegated
      - ./nginx/log/access.log:/var/log/nginx/access.log
      - ./nginx/log/error.log:/var/log/nginx/error.log
      - type: volume
        source: php-fpm-socket
        target: /var/run/php-fpm
        volume:
          nocopy: true
    restart: always
    ports:
      - "80:80"
    depends_on:
      - php

  redis:
    image: redis:latest
    container_name: laravel_redis
    volumes:
      - ./redis/data:/data
    ports:
      - "6379:6379"

そして、
docker-compose.ymlの同階層に phpフォルダ nginxフォルダ redisフォルダを作ります。

nginx/conf.d/にnginx.confを作り、

user  www-data;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

で保存してください。

あとは nginx/conf.d/default.confに

server {
    listen 80;

    root /var/www/html/public;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php index.html index.htm;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ [^/]\.php(/|$) {
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include       fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

さらに
nginx/log/access.log

nginx/log/error.log のファイルを作ってください。

そして、
phpフォルダの中にDockerfileというファイルを作り

FROM php:8.2-fpm
MAINTAINER docker-php

RUN mkdir /var/run/php-fpm
VOLUME ["/var/run/php-fpm"]

RUN apt-get update && \
  apt-get install -y \
  vim \
  wget \
  lsb-release \
  libicu-dev \
  mariadb-client \
  git \
  zip \
  unzip \
  libpng-dev \
  libsodium-dev \
  libzip-dev \
  libjpeg-dev \
  libfreetype6-dev \
  cron \
  curl \
  gnupg \
  apt-transport-https && \
  docker-php-ext-install pdo_mysql mysqli intl pcntl sodium gd zip sockets && \
  docker-php-ext-configure gd --with-freetype --with-jpeg && \
  docker-php-ext-install -j$(nproc) gd

# Install Composer
WORKDIR /var/www/html
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Install Yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
  apt-get update && apt-get install -y yarn

# Install Node.js
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
  apt-get install -y nodejs

# Add crontab file in the cron directory
RUN echo '*/5 * * * * root /path/to/your_command' >> /etc/crontab

COPY ./php/php-fpm.d/zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-www.conf


にしてください。

そして
/php/php-fpm.d/zzz-docker.conf
を作り、

[www]
listen = /var/run/php-fpm/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

そしてdocker-compose up -dを実行してみます。

めっちゃ長いインストールの果てに
Dockerコンテナが正常に起動して、サービスが立ち上がったと思います。

今リアルタイムでやりながら書いてるので 動いたと思います。

docker psで、laravel11用のコンテナのコンテナIDを見つけて docker exec -it xxxxxx /bin/bash でログインしてください。 ログインをしたら..

https://macocci7.net/blog/2024/03/19/【laravel11】サクッと開発環境構築(ubuntucomposersqlite)/

このサイトにサイトに一撃コマンドが書いてあったのですが

composer create-project --prefer-dist "laravel/laravel:^11" .

私もちょっとだけ変えて、
このコマンドでlaravel11の環境構築は一撃で終わりました。

では開発に入る前に .envファイルを

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:N+C9YO66I5Xpq4eqdf8uQkvaQr0bXsYxgz5wOa1+lyc=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://127.0.0.1

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

# Database configuration
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=root

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database

CACHE_STORE=database
CACHE_PREFIX=

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"

このように直してからはじめましょう。 今回のdocker-compose.ymlに合うように書き直しました。

書き直したら、 php artisan migrateを行います。 これで準備は完了

では、laravel11の入門解説として、adminのログインとCRUDを作ってみます。

laravel11になってもこれまでと同様のやり方でroutingはかけます。 app/routes/web.phpにroutingを書くことも同じです。

パスワードリセットもadminのCRUDもlaravel11になったからといって特別変わったことはありません。 web.phpを記載します。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\Auth\AdminLoginController;
use App\Http\Controllers\Auth\AdminForgotPasswordController;
use App\Http\Controllers\Auth\AdminResetPasswordController;

Route::prefix('admin')->group(function () {
    
    // Admin ログインルート
    Route::get('login', [AdminLoginController::class, 'showLoginForm'])->name('admin.login');
    Route::post('login', [AdminLoginController::class, 'login'])->name('admin.login.submit');
    Route::post('logout', [AdminLoginController::class, 'logout'])->name('admin.logout');

    // Admin パスワードリセットルート
    Route::get('password/reset', [AdminForgotPasswordController::class, 'showLinkRequestForm'])->name('admin.password.request');
    Route::post('password/email', [AdminForgotPasswordController::class, 'sendResetLinkEmail'])->name('admin.password.email');
    Route::get('password/reset/{token}', [AdminResetPasswordController::class, 'showResetForm'])->name('admin.password.reset');
    Route::post('password/reset', [AdminResetPasswordController::class, 'reset'])->name('admin.password.update');

    // Admin ダッシュボードルート
    Route::get('dashboard', [AdminController::class, 'index'])->name('admin.dashboard')->middleware('auth:admin');

    // Admin 管理ルート
    Route::get('admins', [AdminController::class, 'index'])->name('admin.admins.index')->middleware('auth:admin');
    Route::get('admins/create', [AdminController::class, 'create'])->name('admin.admins.create')->middleware('auth:admin');
    Route::post('admins', [AdminController::class, 'store'])->name('admin.admins.store')->middleware('auth:admin');
    Route::get('admins/{admin}', [AdminController::class, 'show'])->name('admin.admins.show')->middleware('auth:admin');
    Route::get('admins/{admin}/edit', [AdminController::class, 'edit'])->name('admin.admins.edit')->middleware('auth:admin');
    Route::put('admins/{admin}', [AdminController::class, 'update'])->name('admin.admins.update')->middleware('auth:admin');
    Route::delete('admins/{admin}', [AdminController::class, 'destroy'])->name('admin.admins.destroy')->middleware('auth:admin');

    // パーミッション管理ルート
    Route::get('permissions', [AdminController::class, 'permissions'])->name('admin.permissions.index')->middleware('auth:admin');
    Route::post('permissions', [AdminController::class, 'updatePermissions'])->name('admin.permissions.update')->middleware('auth:admin');
});

ポイントはいままで通り、 routingに関わるコントローラーのファイルはuserで読み込みます。

laravel11でのroutingの解説

Route::get(‘admins’, なんちゃらりん): これはおもに表示系のURLを作るときに書きます。

Route::prefix('admin')->group(function () {
     Route::get('admins', ...):, ...):なんちゃら〜
}

のように、 prefixとしてadminの中にRoute::get(‘admins’, …):があるので、 そのURLは /admin/admins にアクセスするとこの後ろにあるコントローラーのアクションが呼び出されるという意味です。 例えば、 Route::get(‘admins’, [AdminController::class, ‘index’])->name(‘admin.admins.index’)->middleware(‘auth:admin’); ならAdminコントローラーのindexアクションが実行されるということです。 またそのURLをviewで生成するときはname(‘admin.admins.index’)のように admin.admin.indexと名付けるよという意味になっています。

全てのURL設定に->middleware(‘auth:admin’);がついてますが これはログインチェックをするために後ほど解説します。

Route::get(‘<<<URLのルール>>>, [<<<コントローラー名>>>, ‘<<アクション名>>’])->name(‘<<viewなどでのURL生成の時のキー>>’);

のように覚えるとわかりやすいと思います。

ではadminのCURDを通してlaravel11のマイグレーションとmodelの入門解説をしていきます。

yamamotoyuuya@yamamotoyuuyanoMacBook-Pro laravel11 % docker ps
CONTAINER ID   IMAGE                          COMMAND                   CREATED          STATUS          PORTS                    NAMES
41f62f4fb785   nginx:latest                   "/docker-entrypoint.…"   31 minutes ago   Up 31 minutes   0.0.0.0:80->80/tcp       laravel_nginx
fbb012ddef13   laravel11-php                  "docker-php-entrypoi…"   31 minutes ago   Up 31 minutes   9000/tcp                 laravel_php
cf8bb8ea1aa8   phpmyadmin/phpmyadmin:latest   "/docker-entrypoint.…"   8 days ago       Up 31 minutes   0.0.0.0:8889->80/tcp     laravel_phpmyadmin
98e38268a021   redis:latest                   "docker-entrypoint.s…"   8 days ago       Up 31 minutes   0.0.0.0:6379->6379/tcp   laravel_redis
96f2d9d1cb98   mysql:8.0.36                   "docker-entrypoint.s…"   8 days ago       Up 31 minutes   3306/tcp, 33060/tcp      laravel_mysql
yamamotoyuuya@yamamotoyuuyanoMacBook-Pro laravel11 % docker exec -it fbb012ddef13  /bin/bash

まずこんな感じでコンテナIDを確認し、 コンテナにログインします。

そして、

マイグレーションコマンドでlaravel11でDBの設計図であるマイグレーションファイルの雛形を作成する

まず、コンテナにログインした場所でこのコマンドを実行します。


php artisan make:migration create_admins_table --create=admins

php artisan xxxxxxという書き方でlaravel11でもこれまで同様にさまざまなファイルを生成するコマンドが用意されています。 今回はmake:migrationからやっていきましょう。

マイグレーションファイルがapp/database/migrations/に生成されます。

作成されたマイグレーションファイル(database/migrations/xxxx_xx_xx_xxxxxx_create_admins_table.php)を以下のように編集します。全文書き換えてOK。


<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

class CreateAdminsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id(); // 主キー
            $table->string('name'); // 管理者の名前
            $table->string('email')->unique(); // 管理者のメールアドレス
            $table->string('password'); // パスワード
            $table->enum('permission', ['status1', 'status2', 'status3', 'status4', 'status5']); // 権限ステータス
            $table->timestamp('email_verified_at')->nullable(); // メール確認日時
            $table->rememberToken(); // Remember token
            $table->softDeletes(); // ソフトデリート
            $table->timestamps(); // 作成日時と更新日時
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

ステータスがenum型を使っていますが、 これは後ほど例えば $adminsWithStatus1 = Admin::where(‘permission’, ‘status1’)->get(); のように、whereメソッドで簡単に検索できたりするために使いました。 softDelete()とやるとlaravel11では、deleted_atカラムが追加されて削除されたデータはこのdeleted_atのカラムに削除日時が入るので論理的に削除されたんだと認識することができるということです。

モデルファイル

次に、Adminモデルを作成するコマンドを実行します。


php artisan make:model Admin

こんな感じでモデルファイルを作ります。

モデルファイルを作ってみよう

作成されたモデルファイル(app/Models/Admin.php)を以下のように編集します。


<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\SoftDeletes;
use Illuminate\\Foundation\\Auth\\User as Authenticatable;
use Illuminate\\Notifications\\Notifiable;

class Admin extends Authenticatable
{
    use HasFactory, Notifiable, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'permission',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

この設定で、admins テーブルのマイグレーションと Admin モデルが作成され、ソフトデリートもサポートされます。次に、以下のコマンドを実行してマイグレーションを適用します。


php artisan migrate

これで、admins テーブルがデータベースに作成され、Admin モデルが使用できるようになります。

今回127.0.0.1:8889でphpmyadminにアクセスできるようにしてるので、adminsテーブルが本当にできているかみてみます。

できていますね。

ここで一息、今我々はバンコクにいますが、
タイは日本人1名に対して3名のタイ人を雇用しなければいけません。
正直、3名もいらないよねと思いながらも日系企業はみんな採用しています。

我々も採用しないとなぁと思っていたところ
梶村「スラムで採用してきまぁす」

100人声をかけて採用を成功させました。

というわけで続きを書いていきます。


ではadminを作成するためのコントローラーを書いていきましょう。

laravel11でもartisanコマンドで作っていきます。 まずはログインのところのコントローラーから

php artisan make:controller Auth/AdminLoginController
php artisan make:controller Auth/AdminForgotPasswordController
php artisan make:controller Auth/AdminResetPasswordController

で作れますので 3つのコントローラーを作りましょう。

AdminLoginController

<?php

namespace App\\Http\\Controllers\\Auth;

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

class AdminLoginController extends Controller
{
    /**
     * 管理者ログインフォームを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function showLoginForm()
    {
        return view('admin.login'); // 修正ポイント
    }

    /**
     * 管理者のログインリクエストを処理します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
        ]);

        if (Auth::guard('admin')->attempt($request->only('email', 'password'), $request->filled('remember'))) {
            return redirect()->intended(route('admin.dashboard'));
        }

        return back()->withErrors([
            'email' => '提供された認証情報は記録と一致しません。',
        ]);
    }

    /**
     * 管理者のログアウト処理を行います。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function logout(Request $request)
    {
        Auth::guard('admin')->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return redirect()->route('admin.login');
    }
}

AdminForgotPasswordController

<?php

namespace App\\Http\\Controllers\\Auth;

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

class AdminForgotPasswordController extends Controller
{
    /**
     * パスワードリセットリンクのリクエストフォームを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function showLinkRequestForm()
    {
        return view('admin.password.email'); // 修正ポイント
    }

    /**
     * 指定された管理者にリセットリンクを送信します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function sendResetLinkEmail(Request $request)
    {
        $request->validate(['email' => 'required|email']);

        $status = Password::broker('admins')->sendResetLink(
            $request->only('email')
        );

        return $status === Password::RESET_LINK_SENT
            ? back()->with(['status' => __($status)])
            : back()->withErrors(['email' => __($status)]);
    }
}

AdminResetPasswordController

<?php

namespace App\\Http\\Controllers\\Auth;

use App\\Http\\Controllers\\Controller;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Password;
use Illuminate\\Support\\Str;
use Illuminate\\Support\\Facades\\Hash;
use Illuminate\\Auth\\Events\\PasswordReset;

class AdminResetPasswordController extends Controller
{
    /**
     * 指定されたトークンでパスワードリセットビューを表示します。
     *
     * @param  string  $token
     * @return \\Illuminate\\View\\View
     */
    public function showResetForm($token)
    {
        return view('admin.password.reset', ['token' => $token]); // 修正ポイント
    }

    /**
     * 管理者のパスワードをリセットします。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function reset(Request $request)
    {
        $request->validate([
            'token' => 'required',
            'email' => 'required|email',
            'password' => 'required|string|confirmed|min:8',
        ]);

        $status = Password::broker('admins')->reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function ($admin, $password) {
                $admin->forceFill([
                    'password' => Hash::make($password),
                    'remember_token' => Str::random(60),
                ])->save();

                event(new PasswordReset($admin));
            }
        );

        return $status === Password::PASSWORD_RESET
            ? redirect()->route('admin.login')->with('status', __($status))
            : back()->withErrors(['email' => [__($status)]]);
    }
}

laravel11ではこれまで同様認証にガードを使うので config/auth.php

'guards' => [
    // その他のガード
    'admin' => [
        'driver' => 'session',
        'provider' => 'admins',
    ],
],

'providers' => [
    // その他のプロバイダー
    'admins' => [
        'driver' => 'eloquent',
        'model' => App\\Models\\Admin::class,
    ],
],

そしたら次にadminのダッシュボードとCRUDの コントローラーを作っていきます。

php artisan make:controller AdminController

<?php
namespace App\\Http\\Controllers;

use App\\Models\\Admin;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Hash;

class AdminController extends Controller
{
    /**
     * 管理者ダッシュボードを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function index()
    {
        $admins = Admin::all();
         return view('admin.admins.index', compact('admins'));
    }

    /**
     * 新しい管理者作成フォームを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function create()
    {
        return view('admin.admins.create');
    }

    /**
     * 新しい管理者を保存します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:admins',
            'password' => 'required|string|min:8|confirmed',
            'permission' => 'required|in:status1,status2,status3,status4,status5',
        ]);

        Admin::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
            'permission' => $request->permission,
        ]);

        return redirect()->route('admin.admins.index')->with('success', '管理者を作成しました。');
    }

    /**
     * 指定された管理者の詳細を表示します。
     *
     * @param  \\App\\Models\\Admin  $admin
     * @return \\Illuminate\\View\\View
     */
    public function show(Admin $admin)
    {
        return view('admin.admins.show', compact('admin'));
    }

    /**
     * 指定された管理者の編集フォームを表示します。
     *
     * @param  \\App\\Models\\Admin  $admin
     * @return \\Illuminate\\View\\View
     */
    public function edit(Admin $admin)
    {
        return view('admin.admins.edit', compact('admin'));
    }

    /**
     * 指定された管理者を更新します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @param  \\App\\Models\\Admin  $admin
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function update(Request $request, Admin $admin)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:admins,email,' . $admin->id,
            'password' => 'nullable|string|min:8|confirmed',
            'permission' => 'required|in:status1,status2,status3,status4,status5',
        ]);

        $admin->update([
            'name' => $request->name,
            'email' => $request->email,
            'password' => $request->password ? Hash::make($request->password) : $admin->password,
            'permission' => $request->permission,
        ]);

        return redirect()->route('admin.admins.index')->with('success', '管理者を更新しました。');
    }

    /**
     * 指定された管理者を削除します。
     *
     * @param  \\App\\Models\\Admin  $admin
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function destroy(Admin $admin)
    {
        $admin->delete();

        return redirect()->route('admin.admins.index')->with('success', '管理者を削除しました。');
    }

    /**
     * 管理者の権限管理ページを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function permissions()
    {
        $admins = Admin::all();
        return view('admin.permissions', compact('admins'));
    }

    /**
     * 管理者の権限を更新します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function updatePermissions(Request $request)
    {
        $request->validate([
            'permissions' => 'required|array',
            'permissions.*' => 'required|in:status1,status2,status3,status4,status5',
        ]);

        foreach ($request->permissions as $id => $permission) {
            $admin = Admin::findOrFail($id);
            $admin->update(['permission' => $permission]);
        }

        return redirect()->route('admin.permissions.index')->with('success', '管理者の権限を更新しました。');
    }
}

ではlaravel11でadminログイン部分とダッシュボードとCRUDのviewを作っていきます。

adminのviewなので今回は adminLTEを使っていきます。

コンテナの中ではなくて、ローカルのプロジェクトのルートディレクトリで

npm install admin-lte --save

を実行してadminLTEのコードを持ってきてから、

cp -r node_modules/admin-lte/dist public/admin-lte

これで、そのコードをpublic配下に移動します。

ではphp artisan make:viewで必要なviewを全部作ります。

php artisan make:view admin.layout
php artisan make:view admin.dashboard
php artisan make:view admin.login
php artisan make:view admin.password.email
php artisan make:view admin.password.reset
php artisan make:view admin.admins.index
php artisan make:view admin.admins.create
php artisan make:view admin.admins.show
php artisan make:view admin.admins.edit
php artisan make:view admin.permissions
php artisan make:view admin.navbar
php artisan make:view admin.sidebar
php artisan make:view admin.footer

こんな感じで生成されたら

コードをかいていきます

resources/views/admin/layout.blade.php


<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Admin Panel</title>
    <link rel="stylesheet" href="{{ asset('admin-lte/css/adminlte.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/fontawesome-free/css/all.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/icheck-bootstrap/icheck-bootstrap.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/select2/css/select2.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/select2-bootstrap4-theme/select2-bootstrap4.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/bootstrap4-duallistbox/bootstrap-duallistbox.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/bs-stepper/css/bs-stepper.min.css') }}">
    <link rel="stylesheet" href="{{ asset('admin-lte/plugins/dropzone/min/dropzone.min.css') }}">
</head>

<body class="hold-transition sidebar-mini">
    <div class="wrapper">
        <!-- Navbar -->
        @include('admin.navbar')

        <!-- Sidebar -->
        @include('admin.sidebar')

        <!-- Content Wrapper. Contains page content -->
        <div class="content-wrapper">
            @yield('content')
        </div>

        <!-- Footer -->
        @include('admin.footer')
    </div>

    <!-- jQuery CDN -->
    <script src="<https://code.jquery.com/jquery-3.6.0.min.js>"></script>
    <!-- Bootstrap 4 -->
    <script src="{{ asset('admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
    <!-- AdminLTE App -->
    <script src="{{ asset('admin-lte/js/adminlte.min.js') }}"></script>
    <!-- Select2 -->
    <script src="{{ asset('admin-lte/plugins/select2/js/select2.full.min.js') }}"></script>
    <!-- Bootstrap4 Duallistbox -->
    <script src="{{ asset('admin-lte/plugins/bootstrap4-duallistbox/jquery.bootstrap-duallistbox.min.js') }}"></script>
    <!-- BS-Stepper -->
    <script src="{{ asset('admin-lte/plugins/bs-stepper/js/bs-stepper.min.js') }}"></script>
    <!-- Dropzone -->
    <script src="{{ asset('admin-lte/plugins/dropzone/min/dropzone.min.js') }}"></script>
</body>

</html>

resources/views/admin/dashboard.blade.php


@extends('admin.layout')

@section('content')
<div class="container">
    <h1>Admin Dashboard</h1>
    <!-- ダッシュボードの内容をここに追加 -->
</div>
@endsection

resources/views/admin/login.blade.php


<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Admin Login</title>
    <link rel="stylesheet" href="{{ asset('admin-lte/css/adminlte.min.css') }}">
</head>

<body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
            <a href="#"><b>Admin</b>LTE</a>
        </div>
        <div class="card">
            <div class="card-body login-card-body">
                <p class="login-box-msg">ログインしてセッションを開始</p>
                <form action="{{ route('admin.login.submit') }}" method="post">
                    @csrf
                    <div class="input-group mb-3">
                        <input type="email" name="email" class="form-control" placeholder="Email" required autofocus>
                        <div class="input-group-append">
                            <div class="input-group-text">
                                <span class="fas fa-envelope"></span>
                            </div>
                        </div>
                    </div>
                    <div class="input-group mb-3">
                        <input type="password" name="password" class="form-control" placeholder="Password" required>
                        <div class="input-group-append">
                            <div class="input-group-text">
                                <span class="fas fa-lock"></span>
                            </div>
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-8">
                            <div class="icheck-primary">
                                <input type="checkbox" name="remember" id="remember">
                                <label for="remember">
                                    Remember Me
                                </label>
                            </div>
                        </div>
                        <div class="col-4">
                            <button type="submit" class="btn btn-primary btn-block">ログイン</button>
                        </div>
                    </div>
                </form>
                <p class="mb-1">
                    <a href="{{ route('admin.password.request') }}">パスワードを忘れた場合</a>
                </p>
            </div>
        </div>
    </div>

    <script src="{{ asset('admin-lte/plugins/jquery/jquery.min.js') }}"></script>
    <script src="{{ asset('admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
    <script src="{{ asset('admin-lte/js/adminlte.min.js') }}"></script>
</body>

</html>

resources/views/admin/password/email.blade.php


@extends('admin.layout')

@section('content')
<div class="login-box">
    <div class="login-logo">
        <a href="#"><b>Admin</b>LTE</a>
    </div>
    <div class="card">
        <div class="card-body login-card-body">
            <p class="login-box-msg">パスワードリセットリンクを送信</p>
            <form action="{{ route('admin.password.email') }}" method="post">
                @csrf
                <div class="input-group mb-3">
                    <input type="email" name="email" class="form-control" placeholder="Email" required autofocus>
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-envelope"></span>
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-12">
                        <button type="submit" class="btn btn-primary btn-block">パスワードリセットリンクを送信</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>
@endsection

resources/views/admin/password/reset.blade.php


@extends('admin.layout')

@section('content')
<div class="login-box">
    <div class="login-logo">
        <a href="#"><b>Admin</b>LTE</a>
    </div>
    <div class="card">
        <div class="card-body login-card-body">
            <p class="login-box-msg">パスワードをリセットする</p>
            <form action="{{ route('admin.password.update') }}" method="post">
                @csrf
                <input type="hidden" name="token" value="{{ $token }}">
                <div class="input-group mb-3">
                    <input type="email" name="email" class="form-control" placeholder="Email" required autofocus>
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-envelope"></span>
                        </div>
                    </div>
                </div>
                <div class="input-group mb-3">
                    <input type="password" name="password" class="form-control" placeholder="Password" required>
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-lock"></span>
                        </div>
                    </div>
                </div>
                <div class="input-group mb-3">
                    <input type="password" name="password_confirmation" class="form-control" placeholder="Confirm Password" required>
                    <div class="input-group-append">
                        <div class="input-group-text">
                            <span class="fas fa-lock"></span>
                        </div>
                    </div>
                </div>
                <div class="row">
                    <div class="col-12">
                        <button type="submit" class="btn btn-primary btn-block">パスワードをリセットする</button>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>
@endsection

resources/views/admin/admins/index.blade.php

@extends('admin.layout')

@section('content')
<div class="container">
    <h1>Manage Admins</h1>
    <a href="{{ route('admin.admins.create') }}" class="btn btn-primary mb-3">Create New Admin</a>
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>Name</th>
                <th>Email</th>
                <th>Permission</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($admins as $admin)
            <tr>
                <td>{{ $admin->name }}</td>
                <td>{{ $admin->email }}</td>
                <td>{{ $admin->permission }}</td>
                <td>
                    <a href="{{ route('admin.admins.edit', $admin->id) }}" class="btn btn-sm btn-warning">Edit</a>
                    <form action="{{ route('admin.admins.destroy', $admin->id) }}" method="POST" style="display:inline;">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-sm btn-danger">Delete</button>
                    </form>
                </td>
            </tr>
            @endforeach
        </tbody>
    </table>
</div>
@endsection

resources/views/admin/admins/create.blade.php


@extends('admin.layout')

@section('content')
<div class="container">
    <h1>Create Admin</h1>

    <form action="{{ route('admin.admins.store') }}" method="POST">
        @csrf
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" class="form-control" id="name" name="name" required>
        </div>

        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" class="form-control" id="email" name="email" required>
        </div>

        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" name="password" required>
        </div>

        <div class="form-group">
            <label for="password_confirmation">Confirm Password</label>
            <input type="password" class="form-control" id="password_confirmation" name="password_confirmation" required>
        </div>

        <div class="form-group">
            <label for="permission">Permission</label>
            <select class="form-control" id="permission" name="permission" required>
                <option value="status1">Status 1</option>
                <option value="status2">Status 2</option>
                <option value="status3">Status 3</option>
                <option value="status4">Status 4</option>
                <option value="status5">Status 5</option>
            </select>
        </div>

        <button type="submit" class="btn btn-primary">Create</button>
    </form>
</div>
@endsection

resources/views/admin/admins/show.blade.php


@extends('admin.layout')

@section('content')
<div class="container">
    <h1>Admin Details</h1>
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">{{ $admin->name }}</h5>
            <p class="card-text">Email: {{ $admin->email }}</p>
            <p class="card-text">Permission: {{ $admin->permission }}</p>
        </div>
    </div>
    <a href="{{ route('admin.admins.index') }}" class="btn btn-primary mt-3">Back to List</a>
</div>
@endsection

resources/views/admin/admins/edit.blade.php


@extends('admin.layout')

@section('content')
<div class="container">
    <h1>Edit Admin</h1>

    <form action="{{ route('admin.admins.update', $admin->id) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" class="form-control" id="name" name="name" value="{{ $admin->name }}" required>
        </div>

        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" class="form-control" id="email" name="email" value="{{ $admin->email }}" required>
        </div>

        <div class="form-group">
            <label for="password">Password (leave blank to keep current password)</label>
            <input type="password" class="form-control" id="password" name="password">
        </div>

        <div class="form-group">
            <label for="password_confirmation">Confirm Password</label>
            <input type="password" class="form-control" id="password_confirmation" name="password_confirmation">
        </div>

        <div class="form-group">
            <label for="permission">Permission</label>
            <select class="form-control" id="permission" name="permission" required>
                <option value="status1" {{ $admin->permission == 'status1' ? 'selected' : '' }}>Status 1</option>
                <option value="status2" {{ $admin->permission == 'status2' ? 'selected' : '' }}>Status 2</option>
                <option value="status3" {{ $admin->permission == 'status3' ? 'selected' : '' }}>Status 3</option>
                <option value="status4" {{ $admin->permission == 'status4' ? 'selected' : '' }}>Status 4</option>
                <option value="status5" {{ $admin->permission == 'status5' ? 'selected' : '' }}>Status 5</option>
            </select>
        </div>

        <button type="submit" class="btn btn-primary">Update</button>
    </form>
</div>
@endsection

resources/views/admin/permissions.blade.php


@extends('admin.layout')

@section('content')
<div class="content-header">
    <div class="container-fluid">
        <div class="row mb-2">
            <div class="col-sm-6">
                <h1 class="m-0">パーミッション管理</h1>
            </div>
        </div>
    </div>
</div>

<div class="content">
    <div class="container-fluid">
        <div class="row">
            <div class="col-12">
                <div class="card">
                    <div class="card-body">
                        <form action="{{ route('admin.permissions.update') }}" method="POST">
                            @csrf
                            <table class="table table-bordered">
                                <thead>
                                    <tr>
                                        <th>ID</th>
                                        <th>名前</th>
                                        <th>メール</th>
                                        <th>現在の権限</th>
                                        <th>新しい権限</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach ($admins as $admin)
                                        <tr>
                                            <td>{{ $admin->id }}</td>
                                            <td>{{ $admin->name }}</td>
                                            <td>{{ $admin->email }}</td>
                                            <td>{{ $admin->permission }}</td>
                                            <td>
                                                <select name="permissions[{{ $admin->id }}]" class="form-control" required>
                                                    <option value="status1" {{ $admin->permission == 'status1' ? 'selected' : '' }}>Status1</option>
                                                    <option value="status2" {{ $admin->permission == 'status2' ? 'selected' : '' }}>Status2</option>
                                                    <option value="status3" {{ $admin->permission == 'status3' ? 'selected' : '' }}>Status3</option>
                                                    <option value="status4" {{ $admin->permission == 'status4' ? 'selected' : '' }}>Status4</option>
                                                    <option value="status5" {{ $admin->permission == 'status5' ? 'selected' : '' }}>Status5</option>
                                                </select>
                                            </td>
                                        </tr>
                                    @endforeach
                                </tbody>
                            </table>
                            <button type="submit" class="btn btn-primary mt-3">更新</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

resources/views/admin/navbar.blade.php

<nav class="main-header navbar navbar-expand navbar-white navbar-light">
    <!-- Left navbar links -->
    <ul class="navbar-nav">
        <li class="nav-item">
            <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
        </li>
        <li class="nav-item d-none d-sm-inline-block">
            <a href="{{ route('admin.dashboard') }}" class="nav-link">Home</a>
        </li>
    </ul>

    <!-- Right navbar links -->
    <ul class="navbar-nav ml-auto">
        <li class="nav-item">
            <form action="{{ route('admin.logout') }}" method="POST">
                @csrf
                <button type="submit" class="btn btn-link nav-link">Logout</button>
            </form>
        </li>
    </ul>
</nav>

resources/views/admin/sidebar.blade.php

<aside class="main-sidebar sidebar-dark-primary elevation-4">
    <!-- Brand Logo -->
    <a href="{{ route('admin.dashboard') }}" class="brand-link">
        <img src="{{ asset('admin-lte/img/AdminLTELogo.png') }}" alt="AdminLTE Logo" class="brand-image img-circle elevation-3" style="opacity: .8">
        <span class="brand-text font-weight-light">Admin Panel</span>
    </a>

    <!-- Sidebar -->
    <div class="sidebar">
        <!-- Sidebar user panel (optional) -->
        <div class="user-panel mt-3 pb-3 mb-3 d-flex">
            <div class="image">
                <img src="{{ asset('admin-lte/img/user2-160x160.jpg') }}" class="img-circle elevation-2" alt="User Image">
            </div>
            <div class="info">
                <a href="#" class="d-block">Admin</a>
            </div>
        </div>

        <!-- Sidebar Menu -->
        <nav class="mt-2">
            <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
                <li class="nav-item">
                    <a href="{{ route('admin.dashboard') }}" class="nav-link">
                        <i class="nav-icon fas fa-tachometer-alt"></i>
                        <p>Dashboard</p>
                    </a>
                </li>
                <li class="nav-item">
                    <a href="{{ route('admin.admins.index') }}" class="nav-link">
                        <i class="nav-icon fas fa-users"></i>
                        <p>Manage Admins</p>
                    </a>
                </li>
                <li class="nav-item">
                    <a href="{{ route('admin.permissions.index') }}" class="nav-link">
                        <i class="nav-icon fas fa-user-shield"></i>
                        <p>Manage Permissions</p>
                    </a>
                </li>
            </ul>
        </nav>
        <!-- /.sidebar-menu -->
    </div>
    <!-- /.sidebar -->
</aside>

resources/views/admin/footer.blade.php

<footer class="main-footer">
    <strong>Copyright &copy; 2024 <a href="#">Your Company</a>.</strong>
    All rights reserved.
    <div class="float-right d-none d-sm-inline-block">
        <b>Version</b> 3.1.0
    </div>
</footer>

これで、管理者のログイン、パスワードリセット、ダッシュボード、管理者のCRUD、およびパーミッション管理に必要なすべてのビューが作成されました。必要なレイアウトやコンテンツはAdminLTEを使用して構築しています。これで、管理者機能が正しく動作するはずです。

ここで一息、
バンコクで梶村の指示に従い身を粉にして働くおじさんたちはこちらです。

(全員肥満体質 / ここまでやって検索一位が欲しいのか)

ログインのためのはじめのadminユーザを作る

1. シーディングの作成

まず、シーディングファイルを作成します。


php artisan make:seeder AdminSeeder

2. database/seeders/AdminSeeder.phpを編集

以下のコードを追加して、初期管理者ユーザを作成します。


<?php

namespace Database\\Seeders;

use Illuminate\\Database\\Seeder;
use App\\Models\\Admin;
use Illuminate\\Support\\Facades\\Hash;

class AdminSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Admin::create([
            'name' => 'Yamamoto',
            'email' => 'yamamoto@field.asia',
            'password' => Hash::make('password'),
            'permission' => 'status1',
        ]);
    }
}

3. シーダーの登録

database/seeders/DatabaseSeeder.phpファイルを編集して、AdminSeederを登録します。


<?php

namespace Database\\Seeders;

use Illuminate\\Database\\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // 他のシーダーがある場合はここに追加
        $this->call(AdminSeeder::class);
    }
}

4. シーディングの実行

以下のコマンドを実行して、データベースに初期管理者ユーザを追加します。


php artisan db:seed

これで、yamamoto@field.asiaというメールアドレスとpasswordというパスワードを持つ初期管理者ユーザが作成されます。管理者ページにログインして動作を確認してください。

ここまででadminLTEの画像がなかったりくずれていたので、 ちょっと対策を考えました。

ローカルのプロジェクトのルートで

npm install admin-lte @fortawesome/fontawesome-free bootstrap icheck-bootstrap select2 select2-bootstrap4-theme bootstrap4-duallistbox bs-stepper dropzone --save

全部をダウンロードしてから、

手動で必要そうなもののフォルダを作り

mkdir -p public/admin-lte/plugins/fontawesome-free
mkdir -p public/admin-lte/plugins/bootstrap
mkdir -p public/admin-lte/plugins/icheck-bootstrap
mkdir -p public/admin-lte/plugins/select2
mkdir -p public/admin-lte/plugins/select2-bootstrap4-theme
mkdir -p public/admin-lte/plugins/bootstrap4-duallistbox
mkdir -p public/admin-lte/plugins/bs-stepper
mkdir -p public/admin-lte/plugins/dropzone
mkdir -p public/admin-lte/img

そこにコピーします。

# AdminLTEのファイルをコピー
cp -r node_modules/admin-lte/dist/* public/admin-lte

# Font Awesomeのファイルをコピー
cp -r node_modules/@fortawesome/fontawesome-free/* public/admin-lte/plugins/fontawesome-free

# Bootstrapのファイルをコピー
cp -r node_modules/bootstrap/dist/* public/admin-lte/plugins/bootstrap

# iCheck Bootstrapのファイルをコピー
cp -r node_modules/icheck-bootstrap/* public/admin-lte/plugins/icheck-bootstrap

# Select2のファイルをコピー
cp -r node_modules/select2/dist/* public/admin-lte/plugins/select2

# Select2 Bootstrap 4テーマのファイルをコピー
cp -r node_modules/select2-bootstrap4-theme/dist/* public/admin-lte/plugins/select2-bootstrap4-theme

# Bootstrap 4 Duallistboxのファイルをコピー
cp -r node_modules/bootstrap4-duallistbox/dist/* public/admin-lte/plugins/bootstrap4-duallistbox

# BS Stepperのファイルをコピー
cp -r node_modules/bs-stepper/dist/* public/admin-lte/plugins/bs-stepper

# Dropzoneのファイルをコピー
cp -r node_modules/dropzone/dist/* public/admin-lte/plugins/dropzone

# 画像ファイルのコピー
curl -o public/admin-lte/img/user2-160x160.jpg <https://adminlte.io/themes/v3/dist/img/user2-160x160.jpg>
curl -o public/admin-lte/img/AdminLTELogo.png <https://adminlte.io/themes/v3/dist/img/AdminLTELogo.png>

dashbordとかがまだadminのindexだったりはしますが これで見れるようにはなり ログインとdashbordのとadminのCRUDはできました。

では次はlaravel11で、

エリアと職業のマスタをCRUDし、 ユーザ側で会員登録がそのエリアと職業を選択して登録し、adminがそれを見れるというところまでやっていこうと思います。

さらにいうと、 そのユーザが自分の友達を登録して、 その友達と合う人を引き合わせて紹介して チャットできるようにするというところまでやろうと思います。

ここで、一息、
バンコクのタラートノイという場所は街全体にアートが施されていて
観光としてとてもおすすめです。日曜日にいくのがおすすめ。

ではlaravel11の開発の続きで、
ユーザ「推薦者」の登録をしていきます。
userがuserのLPか何かの画面から会員登録をして userがログインをしてダッシュボードにいくというところまで やってみましょう。

会員登録時にメールを飛ばす必要があるのですが、 メール系をローカルの開発環境で扱うには、

laravel11の開発入門でローカルでメールを送信する方法

sendgridもいいですが、 https://mailtrap.io/を使ってローカルのメール送信をやってみましょう。

会員登録をしましょう。

で、それが終わったら、

Email testingのstart tesingをクリックしてください。

そしてこのセレクトボックスでlaravelを選択すると おもいっきりSMTPの情報が書かれています。

そしてそれを.envにコピーします。

こういう感じになります。

MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=2322ce6647c723
MAIL_PASSWORD=ebc27440c41091
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="info@appnamedayo.com"
MAIL_FROM_NAME="${APP_NAME}"

laravel11の開発でUserを作る時の

マイグレーションファイルの作成

ユーザ登録用のマイグレーションファイルを作成します。 メールが飛んで、アクティベーションを作るのと 免許証をアップロードしてあとからadminが承認して承認ユーザだけがログインできるようなものを作ろうと思います、


php artisan make:migration create_users_table

database/migrations/xxxx_xx_xx_create_users_table.phpに以下のコードを追加します。


<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('nickname');
            $table->string('phone');
            $table->string('email')->unique();
            $table->tinyInteger('gender');
            $table->string('license_image')->nullable();
            $table->text('introduction')->nullable();
            $table->string('occupation');
            $table->string('password');
            $table->boolean('approved')->default(false);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

マイグレーションを実行します。(php artisan migrate)

あれ。。。

エラーになりました。

みてみると「もうusersテーブルあるよ」ってことです。 どうやらデフォルトでusersテーブルがあるようです。 たしかにマイグレーションファイルを確認すると

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};

もともとこれがありました。

ということで、 必要な項目だけ追加するマイグレーションファイルを作ることにします。

さきほど実行して作ったマイグレーションファイルは削除して もう一度作りましょう。

php artisan make:migration add_fields_to_users_table –table=users

で生成されたマイグレーションに、

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('nickname')->after('id');
            $table->string('phone')->after('nickname');
            $table->tinyInteger('gender')->after('email');
            $table->string('license_image')->nullable()->after('gender');
            $table->text('introduction')->nullable()->after('license_image');
            $table->string('occupation')->after('introduction');
            $table->boolean('approved')->default(false)->after('password');
            $table->string('activation_token', 60)->nullable()->after('remember_token'); // ここに追加
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['nickname', 'phone', 'gender', 'license_image', 'introduction', 'occupation', 'approved']);
        });
    }
};

これを記載します。

そしてもう一度、

できましたね。

元々テーブルがある場合のカラム追加の解説になったかと思います。

Userモデルの作成

Userモデルを作成し、fillable属性を設定します。


php artisan make:model User

となると本来このコマンドでUserモデルができますが、 デフォルトであるようなので、/app/Models/User.phpをみると 確かにありますね。

app/Models/User.phpに以下のコードを追加します。


<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Foundation\\Auth\\User as Authenticatable;
use Illuminate\\Notifications\\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name', 'nickname', 'phone', 'email', 'gender', 'license_image', 'introduction', 'occupation', 'password', 'approved', 'activation_token'
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password', 'remember_token', 'activation_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

もともとのモデルの要素をかえずに今回追加したカラムを追加しました。

ルーティングの設定

routes/web.phpにルーティングを追加します。


use App\\Http\\Controllers\\UserController; //これは上に書いてね。

Route::get('user/signup', [UserController::class, 'showSignupForm'])->name('user.signup');
Route::post('user/signup', [UserController::class, 'registerUser'])->name('user.register');
Route::get('user/activate/{token}', [UserController::class, 'activateUser'])->name('user.activate');
Route::get('user/dashboard', [UserController::class, 'showDashboard'])->middleware('auth')->name('user.dashboard');

コントローラーの作成

ユーザ登録用のコントローラーを作成します。 modelまで用意されているのにユーザ側はデフォルトではないのかよと思いながら 作っていきます。


php artisan make:controller UserController

app/Http/Controllers/UserController.phpに以下のコードを追加します。

<?php

namespace App\\Http\\Controllers;

use Illuminate\\Http\\Request;
use App\\Models\\User;
use Illuminate\\Support\\Facades\\Mail;
use Illuminate\\Support\\Facades\\Hash;
use Illuminate\\Support\\Str;
use App\\Mail\\ActivationMail;

class UserController extends Controller
{
    public function showSignupForm()
    {
        return view('user.signup');
    }

    public function registerUser(Request $request)
    {
        $request->validate([
            'nickname' => 'required|string|max:255',
            'name' => 'required|string|max:255',
            'phone' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'gender' => 'required|integer',
            'license_image' => 'required|file|image|max:2048',
            'introduction' => 'nullable|string',
            'occupation' => 'required|string',
        ]);

        $licenseImagePath = $request->file('license_image')->store('licenses', 'public');

        $user = User::create([
            'nickname' => $request->nickname,
            'name' => $request->name,
            'phone' => $request->phone,
            'email' => $request->email,
            'gender' => $request->gender,
            'license_image' => $licenseImagePath,
            'introduction' => $request->introduction,
            'occupation' => $request->occupation,
            'password' => Hash::make(Str::random(10)),
        ]);

        $token = Str::random(60);
        $user->update(['activation_token' => $token]);

        Mail::to($user->email)->send(new ActivationMail($user, $token));

        return redirect()->route('user.signup')->with('success', 'ご登録ありがとうございます。確認のためのメールをお送りしました。');
    }

    public function activateUser($token)
    {
        $user = User::where('activation_token', $token)->firstOrFail();
        $user->update(['approved' => true, 'activation_token' => null]);

        return redirect()->route('password.reset', $passwordResetToken)->with('success', 'アカウントが承認されました。パスワードを設定してください。');
    }

    public function showDashboard()
    {
        return view('user.dashboard');
    }
}

メールのためのクラスの作成

メール送信のためのクラスを作成します。


php artisan make:mail ActivationMail

app/Mail/ActivationMail.phpに以下のコードを追加します。

<?php

namespace App\\Mail;

use Illuminate\\Bus\\Queueable;
use Illuminate\\Mail\\Mailable;
use Illuminate\\Queue\\SerializesModels;

class ActivationMail extends Mailable
{
    use Queueable, SerializesModels;

    public $user;
    public $token;

    public function __construct($user, $token)
    {
        $this->user = $user;
        $this->token = $token;
    }

    public function build()
    {
        return $this->from(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME'))
                    ->subject('推薦マッチに登録いただきましてありがとうございます。')
                    ->view('emails.activation')
                    ->with(['user' => $this->user, 'token' => $this->token]);
    }
}

FROMのメールはenvから 件名はアクティベーションメールの時のみ、 メールテンプレートはviews/emails/activation.blade.php にあるよと。 ->with([‘user’ => $this->user, ‘token’ => $this->token]);はメールテンプレートに変数を渡すやり方です。基本的な機能なのでよく使います。

ビューの作成

resources/views/user/signup.blade.phpに以下のコードを追加します。

php artisan make:view user.signup

を実行して作りましょう。


@extends('layouts.guest')

@section('content')
<form action="{{ route('user.register') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <label for="name">お名前:</label>
    <input type="text" name="name" required><br>
    <label for="nickname">ニックネーム:</label>
    <input type="text" name="nickname" required><br>
    <label for="phone">電話番号:</label>
    <input type="text" name="phone" required><br>
    <label for="email">メール:</label>
    <input type="email" name="email" required><br>
    <label for="gender">性別:</label>
    <select name="gender" required>
        <option value="1">男</option>
        <option value="0">女</option>
    </select><br>
    <label for="license_image">免許証アップロード:</label>
    <input type="file" name="license_image" required><br>
    <label for="introduction">自己紹介:</label>
    <textarea name="introduction"></textarea><br>
    <label for="occupation">職業:</label>
    <select name="occupation" required>
        @foreach(config('occupations') as $occupation)
            <option value="{{ $occupation }}">{{ $occupation }}</option>
        @endforeach
    </select><br>
    <button type="submit">登録</button>
</form>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
    {{ session('success') }}
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
@endif
@endsection

ログインしていないときのlayoutsを作成しました。

views/layouts/guest.blade.php

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ config('app.name', '出会い系アプリ') }}</title>
  <link rel="stylesheet" href="<https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css>">
  <link rel="stylesheet" href="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css>">
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
    }

    .header,
    .footer {
      background-color: #f8f9fa;
      padding: 10px 20px;
      text-align: center;
    }

    .header h1 {
      margin: 0;
      font-size: 24px;
    }

    .content {
      padding: 20px;
    }

    .footer p {
      margin: 0;
      font-size: 14px;
    }
  </style>
</head>

<body>

  <div class="header">
    <h1>{{ config('app.name', '出会い系アプリ') }}</h1>
  </div>

  <div class="content">
    @yield('content')
  </div>

  <div class="footer">
    <p>&copy; {{ date('Y') }} 出会い系アプリ. All rights reserved.</p>
  </div>

  <script src="<https://code.jquery.com/jquery-3.2.1.slim.min.js>"></script>
  <script src="<https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js>"></script>
  <script src="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js>"></script>
</body>

</html>

php artisan make:view emails.activationを実行。

resources/views/emails/activation.blade.phpに以下のコードを追加します。


<div>
    <p>{{ $user->nickname }}様</p>
    <p>推薦マッチに登録いただきましてありがとうございます。</p>
    <p>わたしたちはとても良い人なのに、忙しさや環境によって出会いがない方々のためのサービスを提供しています。</p>
    <p>以下のリンクをクリックして、アカウントを有効化してください。</p>
    <a href="{{ route('user.activate', $token) }}">アカウントを有効化</a>
</div>

ログイン後のダッシュボードのviewも作りましょう。

php artisan make:view dashboard

resources/views/dashboard.blade.phpに以下のコードを追加します。


@extends('layouts.user')

@section('content')
<div>
    <h1>ダッシュボード</h1>
    <p>ここには今後の機能が表示されます。</p>
</div>
@endsection

views/layouts/user.blade.phpにレイアウトを追加します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ config('app.name', '出会い系アプリ') }}</title>
    <link rel="stylesheet" href="<https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css>">
    <link rel="stylesheet" href="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css>">
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
        }
        .header, .footer {
            background-color: #f8f9fa;
            padding: 10px 20px;
            text-align: center;
        }
        .header h1 {
            margin: 0;
            font-size: 24px;
        }
        .nav-menu {
            display: flex;
            justify-content: space-around;
            background-color: #343a40;
            color: white;
        }
        .nav-menu .nav-item {
            color: white;
            text-decoration: none;
            padding: 10px 15px;
            display: block;
            cursor: pointer;
        }
        .nav-menu .nav-item:hover {
            background-color: #495057;
        }
        .content {
            padding: 20px;
        }
        .footer p {
            margin: 0;
            font-size: 14px;
        }
    </style>
</head>
<body>

<div class="header">
    <h1>{{ config('app.name', '出会い系アプリ') }}</h1>
</div>

<div class="nav-menu">
    <span class="nav-item"><i class="fas fa-home"></i> ホーム</span>
    <span class="nav-item"><i class="fas fa-user"></i> プロフィール</span>
    <span class="nav-item"><i class="fas fa-envelope"></i> メッセージ</span>
    <span class="nav-item"><i class="fas fa-cog"></i> 設定</span>
    <span class="nav-item" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"><i class="fas fa-sign-out-alt"></i> ログアウト</span>
    <form id="logout-form" action="#" method="POST" style="display: none;">
        @csrf
    </form>
</div>

<div class="content">
    @yield('content')
</div>

<div class="footer">
    <p>&copy; {{ date('Y') }} 出会い系アプリ. All rights reserved.</p>
</div>

<script src="<https://code.jquery.com/jquery-3.2.1.slim.min.js>"></script>
<script src="<https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js>"></script>
<script src="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js>"></script>
</body>
</html>

職業はadminでマスタ化しようか迷ったんですが、configでいきます。

config/occupations.phpを作成し、以下の内容を追加します。
laravel11でもこのあたりの指定は同じです。


<?php

return [
  'management' => '管理職',
  'professional' => '専門職',
  'technical' => '技術職',
  'creative' => 'クリエイティブ職',
  'educational' => '教育職',
  'medical' => '医療・福祉職',
  'sales_service' => '販売・サービス職',
  'office' => '事務職',
  'public_servant' => '公務員',
  'transport_logistics' => '運輸・物流職',
  'agriculture_forestry_fisheries' => '農林水産職',
  'student' => '学生',
  'other' => 'その他',
];

ここまでで、会員登録するとメールが飛んできて、 アクティベーションメールをクリックするとuserのapproveが1となり有効になるというところまで解説できました。

Zenさん曰く

Laravel10まではconfigファイルはLaravel/laravelのconfigディレクトリにありましたが、
それがlaravel/frameworkの方に移動しました。

reffectさん曰く

Laravel 11では、configディレクトリに10個のファイルがあります。
一方、Laravel 10では、configディレクトリに15個のファイルがあります。
Laravel 11では、ファイル数が減少し5つのファイルが削除されています。例えば、hashing.phpが削除されています。Laravel 11では、変更の頻度が低い設定ファイルはデフォルトでは存在しません。しかし、必要な場合はコマンドを使ってファイルを作成できます。
例えば、hashing.phpを作成する場合
php artisan config:publish hashing
このコマンドを実行すると、configディレクトリにhashing.phpファイルが追加されます。どのようなコマンドを利用するとconfigファイルが作成できるかはマニュアルで確認できますが、php artisan config:publishコマンドを実行するだけで、作成可能なファイルの選択肢が表示されます。

とのことなので、おもいっきり10のやり方で書いてしまってますので、
みておいてください。

ここから、userテーブルにパスワードリセットのカラムを用意して アクティベーション後にパスワードを自分で決めさせてログインできるよう解説していきます。

php artisan make:migration add_password_reset_token_to_users_table --table=users

パスワードリセットのところだけのマイグレーションです。 userに追加する感じで。

<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('password_reset_token', 60)->nullable()->after('activation_token');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('password_reset_token');
        });
    }
};

php artisan migrateで、実際にユーザテーブルを更新しましょう

ルーティングに


// パスワード更新ページ
Route::get('user/reset-password/{token}', [UserController::class, 'showResetPasswordForm'])->name('password.reset');

// パスワード更新処理
Route::post('user/reset-password', [UserController::class, 'resetPassword'])->name('password.update');

// ログインページ
Route::get('login', [UserController::class, 'showLoginForm'])->name('login');
Route::post('login', [UserController::class, 'login']);

これを追加です。

Userコントローラーにこれを追加してください

    public function showResetPasswordForm($token)
    {
        return view('user.reset-password', ['token' => $token]);
    }

    public function resetPassword(Request $request)
    {
        $request->validate([
            'token' => 'required',
            'password' => 'required|string|min:8|confirmed',
        ]);

        $user = User::where('password_reset_token', $request->token)->firstOrFail();
        $user->update([
            'password' => Hash::make($request->password),
            'password_reset_token' => null,
        ]);

        return redirect()->route('login')->with('success', 'パスワードが更新されました。ログインしてください。');
    }

    public function showLoginForm()
    {
        return view('auth.login');
    }

    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
        ]);

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return redirect()->intended('dashboard')->with('success', 'ログインに成功しました。');
        }

        return back()->withErrors([
            'email' => '提供された認証情報は記録と一致しません。',
        ])->onlyInput('email');
    }

    public function showDashboard()
    {
        return view('user.dashboard');
    }

コントローラーの一覧上に use Illuminate\Support\Facades\Auth; を追記してAuth(認証)のライブラリを使えるようにしておきましょう。

viewを作るために

php artisan make:view user.reset-password

php artisan make:view auth.login

を実行してviewを作ります。

パスワードはこんな感じでOKです。

@extends('layouts.guest')

@section('content')
<form action="{{ route('password.update') }}" method="POST">
    @csrf
    <input type="hidden" name="token" value="{{ $token }}">
    <div class="form-group">
        <label for="password">新しいパスワード:</label>
        <input type="password" name="password" class="form-control" required>
    </div>
    <div class="form-group">
        <label for="password_confirmation">パスワード確認:</label>
        <input type="password" name="password_confirmation" class="form-control" required>
    </div>
    <button type="submit" class="btn btn-primary">更新</button>
</form>
@endsection

ログインは

@extends('layouts.guest')

@section('content')
<form action="{{ route('login') }}" method="POST">
    @csrf
    <div class="form-group">
        <label for="email">メールアドレス:</label>
        <input type="email" name="email" class="form-control" required>
    </div>
    <div class="form-group">
        <label for="password">パスワード:</label>
        <input type="password" name="password" class="form-control" required>
    </div>
    <button type="submit" class="btn btn-primary">ログイン</button>
</form>
@endsection

です。

UserのModelの $fillableの配列と $hiddenの配列にpassword_reset_tokenを追加するのを忘れないようにしましょう。

最後にルートに

// 認証が必要なルートグループ
Route::middleware(['auth'])->group(function () {
    Route::get('dashboard', [UserController::class, 'showDashboard'])->name('user.dashboard');
});

を追加して ログインしたらダッシュボードにいくためのroutingを作っておきます。

(前のdashbordの記述は消しておきましょう。グループの中に書いていくほうが合理的です。)

これで、laravel11の基本的な実装で、登録からログインまで解説完了。

実際にやってみてもできるようになったと思うので、 確認してみます。

では会員登録したユーザの免許証を見てadminが承認するという機能を作ってみましょう。

このあたりはlaravel11の解説というよりも、
もはやただのlaravelの開発の基本的解説です。

まずroutingのadminのgroupの中に

    //adminがユーザを管理する
    Route::get('users', [UserController::class, 'adminIndex'])->name('admin.users.index');
    Route::get('users/{user}', [UserController::class, 'adminShow'])->name('admin.users.show');
    Route::post('users/{user}/approve', [UserController::class, 'approveUser'])->name('admin.users.approve');

を追加して admin/usersのようなURLに対して、 Userコントローラーでadminの閲覧や承認用のアクションのためのルーティングを作ります。

UserControllerの中に追加するアクションは

    /**
     * 管理者用のユーザ一覧を表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function adminIndex(Request $request)
    {
        $query = User::query();

        // 検索フィルタ
        if ($request->filled('name')) {
            $query->where('name', 'like', '%' . $request->name . '%');
        }
        if ($request->filled('approved')) {
            $query->where('approved', $request->approved);
        }

        // 並び替え
        if ($request->filled('sort_by')) {
            $query->orderBy($request->sort_by, $request->get('sort_order', 'asc'));
        } else {
            $query->orderBy('created_at', 'desc');
        }

        $users = $query->paginate(10);

        return view('admin.users.index', compact('users'));
    }

    /**
     * 管理者用のユーザ詳細を表示します。
     *
     * @param  \\App\\Models\\User  $user
     * @return \\Illuminate\\View\\View
     */
    public function adminShow(User $user)
    {
        return view('admin.users.show', compact('user'));
    }

    /**
     * ユーザを承認します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @param  \\App\\Models\\User  $user
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function approveUser(Request $request, User $user)
    {
        $user->update(['approved' => true]);

        return redirect()->route('admin.users.index')->with('success', 'ユーザを承認しました。');
    }

です。

次にviewですが。 resources/views/admin/users/index.blade.php に

@extends('admin.layout')

@section('content')
<div class="container">
    <h1>ユーザ一覧</h1>

    <form method="GET" action="{{ route('admin.users.index') }}" class="mb-3">
        <div class="form-row">
            <div class="col">
                <input type="text" name="name" class="form-control" placeholder="名前" value="{{ request('name') }}">
            </div>
            <div class="col">
                <select name="approved" class="form-control">
                    <option value="">承認ステータス</option>
                    <option value="1"{{ request('approved') == '1' ? ' selected' : '' }}>承認済み</option>
                    <option value="0"{{ request('approved') == '0' ? ' selected' : '' }}>未承認</option>
                </select>
            </div>
            <div class="col">
                <button type="submit" class="btn btn-primary">検索</button>
            </div>
        </div>
    </form>

    <table class="table">
        <thead>
            <tr>
                <th><a href="{{ route('admin.users.index', ['sort_by' => 'name', 'sort_order' => request('sort_order') === 'asc' ? 'desc' : 'asc']) }}">名前</a></th>
                <th>性別</th>
                <th><a href="{{ route('admin.users.index', ['sort_by' => 'approved', 'sort_order' => request('sort_order') === 'asc' ? 'desc' : 'asc']) }}">承認ステータス</a></th>
                <th><a href="{{ route('admin.users.index', ['sort_by' => 'created_at', 'sort_order' => request('sort_order') === 'asc' ? 'desc' : 'asc']) }}">登録日時</a></th>
                <th>更新日時</th>
                <th>詳細</th>
                <th>承認</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($users as $user)
                <tr>
                    <td>{{ $user->name }}</td>
                    <td>{{ $user->gender == 1 ? '男性' : '女性' }}</td>
                    <td>{{ $user->approved ? '承認済み' : '未承認' }}</td>
                    <td>{{ $user->created_at }}</td>
                    <td>{{ $user->updated_at }}</td>
                    <td><a href="{{ route('admin.users.show', $user) }}" class="btn btn-primary">詳細</a></td>
                    <td>
                        @if (!$user->approved)
                            <form method="POST" action="{{ route('admin.users.approve', $user) }}">
                                @csrf
                                <button type="submit" class="btn btn-success">承認</button>
                            </form>
                        @endif
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>

    {{ $users->withQueryString()->links() }}
</div>
@endsection

そして、

resources/views/admin/users/show.blade.php には

@extends('layouts.app')

@section('content')
<div class="container">
    <h1>{{ $referral->nickname }} の詳細</h1>
    <p><strong>活動エリア:</strong> {{ $referral->areas }}</p>
    <p><strong>紹介:</strong> {{ $referral->introduction }}</p>
    <p><strong>仕事関係の紹介:</strong> {{ $referral->work_introduction }}</p>

    <h2>マッチした紹介者</h2>
    <ul>
        @foreach ($matchedReferrals as $matchedReferral)
            <li>
                <p><strong>名前:</strong> {{ $matchedReferral->nickname }}</p>
                <p><strong>活動エリア:</strong> {{ $matchedReferral->areas }}</p>
                <p><strong>紹介:</strong> {{ $matchedReferral->introduction }}</p>
                <p><strong>仕事関係の紹介:</strong> {{ $matchedReferral->work_introduction }}</p>
            </li>
        @endforeach
    </ul>
</div>
@endsection

を追加します。

http://127.0.0.1/admin/users にアクセスすると。。。

できました。

ではユーザが自分の友人を勝手に登録して同じエリアあたりの人がいたらそれをマッチした人として表示する機能を作ります

1. Migrationの作成

まず、新しい「紹介したい人」テーブルを作成するためのマイグレーションを作成します。


php artisan make:migration create_referrals_table --create=referrals

database/migrations/xxxx_xx_xx_xxxxxx_create_referrals_table.php ファイルを以下のように編集します。

<?php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

class CreateReferralsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('referrals', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('nickname');
            $table->string('areas'); // 複数の活動エリアをカンマ区切りで保存
            $table->text('introduction');
            $table->text('work_introduction');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('referrals');
    }
}

2. Modelの作成

新しいモデルを作成します。


php artisan make:model Referral

app/Models/Referral.php ファイルを以下のように編集します。


<?php
namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class Referral extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id', 'nickname', 'areas', 'introduction', 'work_introduction'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

3. Controllerの作成

新しいコントローラーを作成します。


php artisan make:controller ReferralController

app/Http/Controllers/ReferralController.php ファイルを以下のように編集します。

<?php

namespace App\\Http\\Controllers;

use App\\Models\\Referral;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Auth;

class ReferralController extends Controller
{
    /**
     * 紹介したい人の登録フォームを表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function create()
    {
        return view('referrals.create');
    }

    /**
     * 紹介したい人を保存します。
     *
     * @param  \\Illuminate\\Http\\Request  $request
     * @return \\Illuminate\\Http\\RedirectResponse
     */
    public function store(Request $request)
    {
        $request->validate([
            'nickname' => 'required|string|max:255',
            'areas' => 'required|string|max:255',
            'introduction' => 'required|string|max:1000',
            'work_introduction' => 'required|string|max:300',
        ]);

        Referral::create([
            'user_id' => Auth::id(),
            'nickname' => $request->nickname,
            'areas' => $request->areas,
            'introduction' => $request->introduction,
            'work_introduction' => $request->work_introduction,
        ]);

        return redirect()->route('referrals.my')->with('success', '紹介したい人を登録しました。');
    }

    /**
     * 紹介した人の一覧を表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function myReferrals()
    {
        $userId = Auth::id();
        $referrals = Referral::where('user_id', $userId)->get();
        $matchedCounts = [];

        foreach ($referrals as $referral) {
            $areas = explode(',', $referral->areas);
            $matchedCounts[$referral->id] = 0;

            foreach (Referral::where('user_id', '!=', $userId)->get() as $otherReferral) {
                $otherAreas = explode(',', $otherReferral->areas);
                $commonAreas = array_intersect($areas, $otherAreas);

                if (!empty($commonAreas)) {
                    $matchedCounts[$referral->id]++;
                }
            }
        }

        return view('referrals.my', compact('referrals', 'matchedCounts'));
    }

    /**
     * 紹介者の詳細を表示します。
     *
     * @param  \\App\\Models\\Referral  $referral
     * @return \\Illuminate\\View\\View
     */
    public function show(Referral $referral)
    {
        if ($referral->user_id != Auth::id()) {
            abort(403, 'Unauthorized action.');
        }

        $areas = explode(',', $referral->areas);
        $matchedReferrals = [];

        foreach (Referral::where('user_id', '!=', Auth::id())->get() as $otherReferral) {
            $otherAreas = explode(',', $otherReferral->areas);
            $commonAreas = array_intersect($areas, $otherAreas);

            if (!empty($commonAreas)) {
                $matchedReferrals[] = $otherReferral;
            }
        }

        return view('referrals.show', compact('referral', 'matchedReferrals'));
    }

    /**
     * 紹介した人のマッチング一覧を表示します。
     *
     * @return \\Illuminate\\View\\View
     */
    public function index()
    {
        return $this->myReferrals();
    }
}

4. Routingの追加

routes/web.php ファイルを以下のように編集します。

ここまでの全てのルーティングは

use Illuminate\\Support\\Facades\\Route;
use App\\Http\\Controllers\\AdminController;
use App\\Http\\Controllers\\Auth\\AdminLoginController;
use App\\Http\\Controllers\\Auth\\AdminForgotPasswordController;
use App\\Http\\Controllers\\Auth\\AdminResetPasswordController;
use App\\Http\\Controllers\\UserController;
use App\\Http\\Controllers\\ReferralController;
use App\\Http\\Controllers\\FlyerController;

Route::prefix('admin')->group(function () {
    // Admin ログインルート
    Route::get('login', [AdminLoginController::class, 'showLoginForm'])->name('admin.login');
    Route::post('login', [AdminLoginController::class, 'login'])->name('admin.login.submit');
    Route::post('logout', [AdminLoginController::class, 'logout'])->name('admin.logout');

    // Admin パスワードリセットルート
    Route::get('password/reset', [AdminForgotPasswordController::class, 'showLinkRequestForm'])->name('admin.password.request');
    Route::post('password/email', [AdminForgotPasswordController::class, 'sendResetLinkEmail'])->name('admin.password.email');
    Route::get('password/reset/{token}', [AdminResetPasswordController::class, 'showResetForm'])->name('admin.password.reset');
    Route::post('password/reset', [AdminResetPasswordController::class, 'reset'])->name('admin.password.update');

    // Admin ダッシュボードルート
    Route::get('dashboard', [AdminController::class, 'index'])->name('admin.dashboard')->middleware('auth:admin');

    // Admin 管理ルート
    Route::get('admins', [AdminController::class, 'index'])->name('admin.admins.index')->middleware('auth:admin');
    Route::get('admins/create', [AdminController::class, 'create'])->name('admin.admins.create')->middleware('auth:admin');
    Route::post('admins', [AdminController::class, 'store'])->name('admin.admins.store')->middleware('auth:admin');
    Route::get('admins/{admin}', [AdminController::class, 'show'])->name('admin.admins.show')->middleware('auth:admin');
    Route::get('admins/{admin}/edit', [AdminController::class, 'edit'])->name('admin.admins.edit')->middleware('auth:admin');
    Route::put('admins/{admin}', [AdminController::class, 'update'])->name('admin.admins.update')->middleware('auth:admin');
    Route::delete('admins/{admin}', [AdminController::class, 'destroy'])->name('admin.admins.destroy')->middleware('auth:admin');

    // パーミッション管理ルート
    Route::get('permissions', [AdminController::class, 'permissions'])->name('admin.permissions.index')->middleware('auth:admin');
    Route::post('permissions', [AdminController::class, 'updatePermissions'])->name('admin.permissions.update')->middleware('auth:admin');

    // Admin がユーザを管理するルート
    Route::get('users', [UserController::class, 'adminIndex'])->name('admin.users.index')->middleware('auth:admin');
    Route::get('users/{user}', [UserController::class, 'adminShow'])->name('admin.users.show')->middleware('auth:admin');
    Route::post('users/{user}/approve', [UserController::class, 'approveUser'])->name('admin.users.approve')->middleware('auth:admin');
});

// ユーザの会員登録、ログイン、パスワードリセットルート
Route::get('user/signup', [UserController::class, 'showSignupForm'])->name('user.signup');
Route::post('user/signup', [UserController::class, 'registerUser'])->name('user.register');
Route::get('user/activate/{token}', [UserController::class, 'activateUser'])->name('user.activate');
Route::get('user/reset-password/{token}', [UserController::class, 'showResetPasswordForm'])->name('password.reset');
Route::post('user/reset-password', [UserController::class, 'resetPassword'])->name('password.update');

// ユーザのログインページ
Route::get('login', [UserController::class, 'showLoginForm'])->name('login');
Route::post('login', [UserController::class, 'login']);

// 認証が必要なルートグループ
Route::middleware(['auth'])->group(function () {
    Route::get('dashboard', [UserController::class, 'showDashboard'])->name('user.dashboard');

    // 紹介したい人のルート
    Route::get('referrals', [ReferralController::class, 'index'])->name('referrals.index');
    Route::get('referrals/create', [ReferralController::class, 'create'])->name('referrals.create');
    Route::post('referrals', [ReferralController::class, 'store'])->name('referrals.store');
    Route::get('referrals/{referral}', [ReferralController::class, 'show'])->name('referrals.show');
    Route::get('my-referrals', [ReferralController::class, 'myReferrals'])->name('referrals.my');
});

これです。

5. Viewの作成

resources/views/referrals/create.blade.php

@extends('layouts.user')

@section('content')
<div class="container">
    <h1>紹介したい人を登録する</h1>

    <form method="POST" action="{{ route('referrals.store') }}">
        @csrf
        <div class="form-group">
            <label for="nickname">ニックネーム</label>
            <input type="text" name="nickname" id="nickname" class="form-control" required>
        </div>

        <div class="form-group">
            <label for="areas">活動エリア(都道府県 市区町村)</label>
            <input type="text" name="areas" id="areas" class="form-control" placeholder="例: 東京, 大阪" required>
        </div>

        <div class="form-group">
            <label for="introduction">この人の紹介</label>
            <textarea name="introduction" id="introduction" class="form-control" rows="4" required></textarea>
        </div>

        <div class="form-group">
            <label for="work_introduction">この人の仕事関係の紹介</label>
            <textarea name="work_introduction" id="work_introduction" class="form-control" rows="2" required></textarea>
        </div>

        <button type="submit" class="btn btn-primary">登録</button>
    </form>
</div>
@endsection

resources/views/referrals/index.blade.php

@extends('layouts.user')

@section('content')
<div class="container">
    <h1>マッチした紹介者一覧</h1>

    @foreach ($matchedReferrals as $referralId => $matched)
        <h2>紹介者ID: {{ $referralId }}</h2>
        <ul>
            @foreach ($matched as $referral)
                <li>
                    ニックネーム: {{ $referral->nickname }},
                    活動エリア: {{ $referral->areas }},
                    紹介: {{ $referral->introduction }},
                    仕事紹介: {{ $referral->work_introduction }}
                </li>
            @endforeach
        </ul>
    @endforeach
</div>
@endsection

resouces/views/referrals/my.blade.php

@extends('layouts.user')

@section('content')
<div class="container">
    <h1>自分が紹介した人の一覧</h1>
    <table class="table">
        <thead>
            <tr>
                <th>名前</th>
                <th>マッチした数</th>
                <th>詳細</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($referrals as $referral)
                <tr>
                    <td>{{ $referral->nickname }}</td>
                    <td>{{ $matchedCounts[$referral->id] }}</td>
                    <td><a href="{{ route('referrals.show', $referral) }}" class="btn btn-primary">詳細</a></td>
                </tr>
            @endforeach
        </tbody>
    </table>
</div>
@endsection

show.blade.phpも作ります、

@extends('layouts.user')

@section('content')
<div class="container">
  <h1>{{ $referral->nickname }} の詳細</h1>
  <p><strong>活動エリア:</strong> {{ $referral->areas }}</p>
  <p><strong>紹介:</strong> {{ $referral->introduction }}</p>
  <p><strong>仕事関係の紹介:</strong> {{ $referral->work_introduction }}</p>

  <h2>マッチした紹介者</h2>
  <ul>
    @foreach ($matchedReferrals as $matchedReferral)
    <li>
      <p><strong>名前:</strong> {{ $matchedReferral->nickname }}</p>
      <p><strong>活動エリア:</strong> {{ $matchedReferral->areas }}</p>
      <p><strong>紹介:</strong> {{ $matchedReferral->introduction }}</p>
      <p><strong>仕事関係の紹介:</strong> {{ $matchedReferral->work_introduction }}</p>
    </li>
    @endforeach
  </ul>
</div>
@endsection

これで、新しい「紹介したい人」機能が追加されます。

できましたね

ここで一息、
バンコクのサイアムパラゴンのスーパーの中で食べれるトリュフ風味のクリームパスタは絶品です、。

では、ここから少し複雑な仕様にしていきます。

今、マッチした人が表示されています。 このマッチごとに マッチした人とマッチされた人用に 二人のためにURLを発行し、 そのURLにアクセスすると ログインなしに相手の情報がみれて 直近2週間のカレンダーからお互いにリアルタイムで 13:00-15:00 15:00-17:00 17:00-19:00 19:00-21:00 21:00-23:00 の中で行ける日をお互いに調整し 行ける日が揃ったら その日の時間の背景を赤くするようなコードを書いていきます。

例えば ユーザAが A-1さんとA-2さんを紹介していて ユーザBがB-1さんとB-2さんを紹介していた場合 A-1さんとB-2さんがマッチしていたら AさんとBさんの二人の紹介者のマッチの画面では お互いにリンクが発行されていて AさんがそのURLをA-1さんに送ると A-1さんはB-2さんの情報とカレンダーが見れます。

BさんがそのURLをB-2さんに渡してB-2さんがアクセスすると B-2さんはA-1さんの情報とカレンダーが見れます

A-1さんとB-2さんはリアルタイムで変更できるカレンダーで 日程を調整します お互いにどこがクリックして空いているのかがわかります。

マッチしたらその日の時間の背景が赤くなり「マッチしています」という文章が表示されるようにします。

ここまでを一つの仕様としましょう。

まず、マッチングURLと日程調整情報を保存するためのモデルとマイグレーションを作成します。

php artisan make:model UserMatch -m

生成されたマイグレーションに、

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

class CreateUserMatchesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_matches', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('referral_a_id');
            $table->unsignedBigInteger('referral_b_id');
            $table->string('url')->unique();
            $table->json('available_times')->nullable(); // JSON形式で保存
            $table->timestamps();

            $table->foreign('referral_a_id')->references('id')->on('referrals')->onDelete('cascade');
            $table->foreign('referral_b_id')->references('id')->on('referrals')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('user_matches');
    }
}

を書きます。

php artisan migrate を実行しましょう。

そのあと、 app/Models/UserMatch.php には、

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;

class UserMatch extends Model
{
    use HasFactory;

    protected $fillable = ['referral_a_id', 'referral_b_id', 'url', 'available_times'];

    protected $casts = [
        'available_times' => 'array',
    ];

    public function referralA()
    {
        return $this->belongsTo(Referral::class, 'referral_a_id');
    }

    public function referralB()
    {
        return $this->belongsTo(Referral::class, 'referral_b_id');
    }
}

こちらを書きます。

php artisan make:controller UserMatchController でコントローラーを生成します。

web.phpに

use App\\Http\\Controllers\\UserMatchController;

を追加し、

Route::get('user_matches/{match}', [UserMatchController::class, 'show'])->name('user_matches.show');
Route::post('user_matches/{match}', [UserMatchController::class, 'update'])->name('user_matches.update');

を追加します。

次にviewを作ります。 php artisan make:view user_matches.show

@extends('layouts.user')

@section('content')
<div class="container">
    <h1>マッチング情報</h1>
    <p><strong>紹介者A:</strong> {{ $match->referralA->nickname }}</p>
    <p><strong>紹介者B:</strong> {{ $match->referralB->nickname }}</p>

    <h2>日程調整</h2>
    <div id="calendar"></div>
</div>

@php
$availableTimesJson = json_encode($match->available_times);
@endphp

<script>
    document.addEventListener('DOMContentLoaded', function() {
        const calendarEl = document.getElementById('calendar');
        let availableTimes = "{{ $availableTimesJson }}";

        // Check if availableTimes is empty, and set it to an empty object if it is
        if (!availableTimes) {
            availableTimes = "{}";
        }
        const availableTimesObj = JSON.parse(availableTimes);

        function updateAvailableTimes(date, timeSlot) {
            fetch("{{ route('user_matches.update', ['match' => $match->id]) }}", {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': "{{ csrf_token() }}"
                    },
                    body: JSON.stringify({
                        date: date,
                        time_slot: timeSlot
                    })
                })
                .then(response => response.json())
                .then(data => {
                    // Update the calendar based on the response
                    // Example: Update UI to reflect selected slots
                });
        }

        // カレンダーのレンダリングロジック
        const now = new Date();
        for (let i = 0; i < 14; i++) {
            const date = new Date(now);
            date.setDate(now.getDate() + i);

            const dateString = date.toISOString().split('T')[0];

            ['13:00-15:00', '15:00-17:00', '17:00-19:00', '19:00-21:00', '21:00-23:00'].forEach(timeSlot => {
                const timeSlotEl = document.createElement('div');
                timeSlotEl.textContent = `${dateString} ${timeSlot}`;
                timeSlotEl.className = 'time-slot';

                // 初回ロード時に全てのスロットを表示し、availableTimesがあれば選択状態を反映
                if (availableTimesObj && availableTimesObj[dateString] && availableTimesObj[dateString].includes(timeSlot)) {
                    timeSlotEl.classList.add('selected');
                }

                timeSlotEl.addEventListener('click', function() {
                    timeSlotEl.classList.toggle('selected');
                    updateAvailableTimes(dateString, timeSlot);
                });

                calendarEl.appendChild(timeSlotEl);
            });
        }
    });
</script>

<style>
    .time-slot {
        padding: 10px;
        border: 1px solid #ccc;
        margin: 5px;
        cursor: pointer;
    }

    .time-slot.selected {
        background-color: red;
        color: white;
    }
</style>
@endsection

ここで、 app/Http/Controllers/ReferralController.php のコントローラーの上部に use App\Models\UserMatch; を追加し、 showアクションを

    public function show(Referral $referral)
    {
        if ($referral->user_id != Auth::id()) {
            abort(403, 'Unauthorized action.');
        }

        $areas = explode(',', $referral->areas);
        $matchedReferrals = [];
        $matchUrls = []; // 追加

        foreach (Referral::where('user_id', '!=', Auth::id())->get() as $otherReferral) {
            $otherAreas = explode(',', $otherReferral->areas);
            $commonAreas = array_intersect($areas, $otherAreas);

            if (!empty($commonAreas)) {
                $matchedReferrals[] = $otherReferral;

                // マッチングURLを生成
                $url = URL::temporarySignedRoute(
                    'user_matches.show', now()->addDays(14), ['match' => uniqid()]
                );

                $match = UserMatch::create([
                    'referral_a_id' => $referral->id,
                    'referral_b_id' => $otherReferral->id,
                    'url' => $url,
                ]);

                $matchUrls[$otherReferral->id] = $url; // 追加
            }
        }

        return view('referrals.show', compact('referral', 'matchedReferrals', 'matchUrls')); // 変更
    }

のように変更し、 さらにこのコントローラーの上に

use App\Models\UserMatch; use Illuminate\Support\Facades\URL;

を追加してください。

UserMatchControllerのshowactionは

    public function show(Request $request, $match)
    {
        $match = UserMatch::where('url', $request->fullUrl())->firstOrFail();
        return view('user_matches.show', compact('match'));
    }

となります。

そのあとに、

referralのviewのshow.blade.phpを

@extends('layouts.user')

@section('content')
<div class="container">
  <h1>{{ $referral->nickname }} の詳細</h1>
  <p><strong>活動エリア:</strong> {{ $referral->areas }}</p>
  <p><strong>紹介:</strong> {{ $referral->introduction }}</p>
  <p><strong>仕事関係の紹介:</strong> {{ $referral->work_introduction }}</p>

  <h2>マッチした紹介者</h2>
  <ul>
    @foreach ($matchedReferrals as $matchedReferral)
    <li>
      <p><strong>名前:</strong> {{ $matchedReferral->nickname }}</p>
      <p><strong>活動エリア:</strong> {{ $matchedReferral->areas }}</p>
      <p><strong>紹介:</strong> {{ $matchedReferral->introduction }}</p>
      <p><strong>仕事関係の紹介:</strong> {{ $matchedReferral->work_introduction }}</p>
      <p><a href="{{ $matchUrls[$matchedReferral->id] }}" target="_blank" class="btn btn-primary">マッチングリンク</a></p>
    </li>
    @endforeach
  </ul>
</div>
@endsection

のように、 <p><a href=”{{ $matchUrls[$matchedReferral->id] }}” target=”_blank” class=”btn btn-primary”>マッチングリンク</a></p> を追加します。

ここまでできればまずはOKです。

これくらいにしておきましょう。

お祝いにブルーエレファント食べました。

グローバル視点では

このlaravel11の差分解説がわかりやすかったです。

ではまた。

弊社は開発以外でも技術を駆使して
https://field.asia/stelmah/
https://youtu.be/O_p3vd5AeKE
SEOでたいていのワードで1位にできる会社です。