Blogブログ

laravel

2024.08.01

はじめてのLaravel10開発入門[環境構築から実装まで解説]

こんにちは月岡です。

laravel10ですがlaravel11の方はこちら。


今我々は「ちょっくらバンコクでも旅行しながら仕事でもしててくれ
という話を間に受け、現在バンコクのアソークにいます

バンコクでいろいろな場所を観光しながら、
今回はlaravel10の環境構築と実装を紹介しようと思います。

laravel10の入門目次

まずはlaravel10でいろいろ解説してるブログをみてみました。

laravel10の環境構築で調べてみると
kinstaさんの「Laravel 10━アップデートと最新の機能を徹底解説」という記事がシンプルながらとてもわかりやすかったり、

40代からのプログラミングでお馴染みのブログで「Laravel10リリース!新機能・変更点・注意点10個ご紹介」という記事が変更点に特化していてわかりやすかったり、

みんな大好きqiitaの@7mpyさんの「Laravel 10🐿がリリースされたのだ🎉【Laravel 10 新機能】」はかなりわかりやすいし、

ゆるプロ日記さんは「Laravel10の特筆すべき内容5つ! 2023年 PHPフレームワークLaravel最新バージョン」の記事の中で他のサイトが見逃していそうな特筆ポイントを丁寧に解説しています。

いつもながらわかりやすくて感動しますがzenさんは「Laravel 10 Release/Upgrade memo」はとてもわかりやすく”ざっくり”と記載されている目次だけでも最後まで読もうという気持ちになりました。

ここまでさまざまなプロフェッショナルたちがlaravel10を解説しているので….

解説しなくていいかな。

って気持ちになりました。

ところで、このスタンプで帽子に「月」と書いてあるたましいは
わたし、月岡です。
すっかり良記事をみて体力はゼロになりました。

一応、わたしの存在意義としては、
実際にdocker-compose up -dと実行するだけで
なんかもう全部laravel10の環境構築完了!

みたいなものを提供することことにあるのかなと思いました。
そしてlaravel10からlaravelを勉強しようとする人に
わかりやすくそもそものlaravelの開発の解説をするようなブログにしようと思います。
laravel10から加わった機能を見たのですがまぁいつも必要なものはキューのところくらいでしょうか。あとはあまり使わなさそうなので普通にlaravel10で環境構築してブログでも作るぞって内容にしようと思いました。

では、さっそくlaravel10の環境構築や開発をしていきます。

まずdocker-compose.ymlを晒します。

version: '3.8'

volumes:
  mysql-volume:

services:
  app:
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
    ports:
      - 5173:5173
    depends_on:
      - db
    volumes:
      - ./src/:/var/www/html
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=${DB_NAME}
      - DB_USERNAME=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}

  web:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    ports:
      - ${WEB_PORT}:80
    depends_on:
      - app
    volumes:
      - ./src/:/var/www/html

  db:
    build:
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    ports:
      - ${DB_PORT}:3306
    environment:
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      PGTZ: 'Asia/Tokyo'
    volumes:
      - mysql-volume:/var/lib/mysql

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    restart: always
    ports:
      - ${PHPMYADMIN_PORT}:80
    environment:
      PMA_HOST: db
    depends_on:
      - db

そして同階層にdockerフォルダを作り、

docker/mysql/Dockerfile
docker/mysql/my.cnf
のファイルを作り、

それぞれ

FROM mysql:8.0

ENV TZ=UTC

COPY ./docker/mysql/my.cnf /etc/my.cnf

と、

[mysqld]
user=mysql
character_set_server = utf8mb4
collation_server = utf8mb4_0900_ai_ci

# timezone
default-time-zone = SYSTEM
# log_timestamps = SYSTEM

# Error Log
# log-error = mysql-error.log

# Slow Query Log
# slow_query_log = 1
# slow_query_log_file = mysql-slow.log
# long_query_time = 1.0
# log_queries_not_using_indexes = 0

# General Log
# general_log = 1
# general_log_file = mysql-general.log

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4

にします。

次に、
docker/nginx/Dockerfile

docker/nginx/default.conf
を作り、
それぞれ、

FROM nginx:1.23.2-alpine

ENV TZ=UTC

# nginx config file
COPY ./docker/nginx/*.conf /etc/nginx/conf.d/

WORKDIR /var/www/html

と、

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 app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

にします。

最後に
docker/php/Dockerfile
docker/php/php.ini
を作り、それぞれのファイルを

FROM php:8.1.16-fpm

# Viteのサーバーにローカルからアクセスするために開ける
EXPOSE 5173

# COPY php.ini
COPY ./docker/php/php.ini /usr/local/etc/php/php.ini

# Composer install
COPY --from=composer:2.5.4 /usr/bin/composer /usr/bin/composer

# install Node.js
COPY --from=node:18.14.2 /usr/local/bin /usr/local/bin
COPY --from=node:18.14.2 /usr/local/lib /usr/local/lib

RUN apt-get update && \
  apt-get -y install \
  git \
  zip \
  unzip \
  vim \
  libpq-dev \
  libonig-dev \
  libicu-dev \
  libzip-dev \
  libpng-dev \
  default-mysql-client \
  && docker-php-ext-install pdo_pgsql pdo_mysql bcmath intl mbstring gd && \
  pecl install xdebug && \
  docker-php-ext-enable xdebug

WORKDIR /var/www/html

と、

[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"

と書いて保存します。

これで、
laravel10の構築を目的に、
dockerの中でmysql nginx phpのDockerfileとその設定ファイルが記載されました。

このdocker-compose.ymlは変数を使っているのでその変数を.envに書きます。

この辺りのブログは皆さんを助けるかもしれません。

laravel10でもこのあたりは変わらずに、
.envファイルを作成し、中身を

WEB_PORT=80
DB_PORT=3306

DB_NAME=hamlets
DB_USER=root
DB_PASSWORD=pleaseSearchHamletsLineStamp!
DB_ROOT_PASSWORD = pleaseSearchHamletsLineStamp!
PHPMYADMIN_PORT=8080

のように、自分で勝手にきめたDB名とパスワードやポートを記載してください。

ではlaravel10用のコンテナを立ち上げてみましょう。

docker-compose up -d

でコンテナを作りましょう。

ところで解説をしながらですが、
今わたしは、タラートノイにいます。

このエリアは最近開発が進んでいてフォトジェニックな路地やおしゃれなカフェが多く、カフェで開発をしながらゆっくりできる最高なエリアです。

はい、続きを話します。

いま、laravel10の解説を書きながら自分で実行しているので、
実際に動くはずです。

次に、docker psで、
appのコンテナIDを見つけます。今回はxxxだっとします。
そしたら
docker exec -it xxx /bin/bash
でログインをし、laravel10をインストールしていきます。

docker exec -it 2da50c72fd14 /bin/bash

こんな感じでログインし、
そして/var/www/htmlの中で、
composerを使ってlaravel10をインストールしていきます。

下記を実行してlaravel10を入れましょう。

composer create-project --prefer-dist "laravel/laravel=10.*" .

これで、laravel10用のコンテナで作られた環境にlaravel10が入りました。

余談ですが、もしlaravel10のcomposer周りでエラーが起きたら、こちらをみてください。

ここまでやって127.0.0.1 でアクセスすると

と表示されます。

laravel10でも画像をアップロードして表示させるために
php artisan storage:link
をやらないといけないので、
実行してください。

つぎに、
laravelのルートにある.envの情報は先ほどのdockerの.envの情報に合わせないとなので、

DB_CONNECTION=mysql
DB_HOST=db //dockerのホスト名ですよ。
DB_PORT=3306
DB_DATABASE=hamlets
DB_USERNAME=root
DB_PASSWORD=pleaseSearchHamletsLineStamp!

このdatabaseの部分は同じにしておきましょう。
これで環境構築は終わりです。

さて、ちょっと移動しまして、
最近観光会社がこぞって有名にしようとしてるこのエリア
オンアン運河 ウォーキングストリート
にきました。

ここはちょっとしたカフェが日中はあるので、
web開発しながらうろうろできるのですが、
問題は夜です。

街をあげて相当やる気がなくなるので、
ここに時間をかけてくる意味はほぼない、というか幻滅するので
夜は来ない方がいい。
(というかここを観光名所といってしまってはいけないのではないかと思います)
というわけで個人的にはこのオンアンウォーキングストリートに14時以降にいくのはお勧めしません。
そもそも昼もそこまで盛り上がってないんですよね。
laravelの解説書くだけならいいのですが、
かなり時間をかけてきたのに全然みるところがない(リアルに15分で観光終了となり、)とても精神的に削られましたので、

話し合い、アソーク駅に戻ることになりました。

一緒についてきたみんなも移動してもらいました。
もうここは搾りかすみたいな町だからtuktuki乗ってアソーク行こうぜって感じで。移動です。

すっかり夜になりました。
夜はターミナル21やサイアムパラゴンなんかが明るくて、
ここでもlaravel解説のブログをかけるカフェや店がたくさんあります。

というわけで、移動してルーティングからまた解説していきます。

laravel10のルーティングについて

まずlaravelのルーティングを使いとてもシンプルなLPやstaticなページを作るときは、

Route::get('/about', function () {
    return view('about');
})->name('about');

Route::get('/staff', function () {
    return view('staff');
})->name('staff');

こんなかんじで、URLでアクセスがあったらすぐにviewを表示したりします。
それぞれ/aboutと/staffというURLを作成し、aboutとstaffという名前を付けます。これにより、システム内でこれらのページを参照する場合、名前を使用してリンクを作ったりとか、簡単にアクセスできます。

次に同じくlaravelのルーティングをする場合、create store update edit destory index showとかのCRUDに必要なルーティングを自動的に生成してくれるresouceという機能があります。

Route::resource(
    'users',
    'App\\Http\\Controllers\\UserController',
    ['only' => ['index', 'create', 'store']]
);

このコードは、usersというURLに対して、UserControllerクラスのindex、create、storeメソッドだけを許可してます。

こうしてどのURLにアクセスをしたときに
どのコントローラーのどのアクションを見るかをセットできます。

laravel10のルーティングの書き方はこちらのブログの方が100倍しっかりしてますのでみてください。

ではviewの書き方の解説に入る前に
サイアムパラゴンでご飯食べます。
ウニパスタとキャビアパスタ食べます。

とてもおいしいです。
日本のデパートよりも綺麗ですね。
ではviewの書き方にはいっていきます。

viewの書き方

laravel10でもここは変わらずに、
昔からおなじで、
resouces/views/staff.blade.php
等に下記のHTMLを書くとみれます。
ついでにpublic/statics/css/style.cssも書いておくと理解しやすいです。

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

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="{{ asset('statics/css/style.css') }}" rel="stylesheet">
  <title>Document</title>
</head>

<body>
  staff
  <a href="{{ route('about') }}">About</a>

</body>

</html>

assetは、Laravelアプリケーションでアセット(JavaScript、CSS、画像などの静的ファイル)を取得するためのヘルパー関数です。
asset関数は、引数として渡された相対パスを使用して、アセットの絶対パスを生成します。これにより、HTMLのリンクタグやイメージタグなどで、アセットの絶対パスを使用できるようになります。
アセットファイルは、publicディレクトリに置かれる必要があります。

route(‘about’)で、先ほどのルーティングでaboutと名前をコントローラーのアクションを見に行ってくれます。

余談ですが、
こちらのlaravel10の変更点に特化したブログでは、

    public function test(): View
    {
        return view('welcome');
    }

このようにView型を指定すると、
resources/views/welcome.blade.php
をみてくれると書いてありました。便利ですね。一旦、これは置いておいて、

ではlaravel10でadminを作っていきましょう。

せっかくlaravel10でブログを作っていくので丁寧に
誰でもわかるように丁寧にやっていきましょう。

docker psで、コンテナIDを確認して、

docker exec -it コンテナID /bin/bash

でコンテナに入り、

php artisan make:migration create_admins_table

を実行してadminテーブルを作るための設計ファイルのようなもの(マイグレーションファイルと呼ぶ)を作成します。

そうすると、

database/migrationsの中に、
本日の日付のマイグレーションファイルが生成されます。

このファイルを下記に変えましょう

<?php

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

class CreateAdminsTable extends Migration
{
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique();

            //Eメールアドレスの認証が行われた時間を格納
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');

            /**ユーザが再度きた時に覚えておいてログインするremember me機能のためのトークンで、サーバーはユニークなトークンを作り、そのトークンをユーザーのブラウザにクッキーとして保存。と、同時に、サーバーはそのトークンをデータベースにも保存。ユーザーがウェブサイトを訪れるとき、ブラウザはこのクッキーをサーバーに送信。サーバーはクッキーに含まれるトークンをデータベースと照合し、一致する場合はユーザーを自動的にログインするという機能ね。 **/
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

Laravel には Passport, Sanctum といった API 認証ライブラリも提供されているのでLaravel10からlaravelを学ぶ人でも興味がある人は見ておくと良いかもしれません。

では、appのコンテナに入り

php artisan migrate

これでテーブルの生成に成功しました。
余談ですが、
「あ、migrate間違えた!」というときは
php artisan migrate:rollback
で、元に戻すことができます。

seedをいれてlaravel10でadminログインできるようにしてみましょう。

全然関係ないですが弊社の黒田という開発者なのかマーケターなのかわからんやつは、「俺自身がseedとなってくる」と呟き

そのあと連絡が取れなくなりました。

では、先ほどの続きで、
Artisanコマンドを使ってシーダファイルというものを作成します。
シーダは通常 database/seeders ディレクトリに作られるもので、テストデータなどをテーブルに入れたりするときに使われます。
ではさっそく、コンテナのappにはいり次のコマンドを実行してみましょう

php artisan make:seeder AdminSeeder

すると
database/seeders/AdminTableSeeder.php
というファイルが生成されるので、

そのファイルをこのようなコードにしてください

<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

class AdminTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('admins')->insert([
            [
                'email' => 'tsukioka@example.com',
                'password' => Hash::make('password'),
            ],
            [
                'email' => 'yamamoto@example.com',
                'password' => Hash::make('password'),
            ]
        ]);
    }
}

メールとパスワードは自分の好きなものにしましょう。

このシードを実行(データをテーブルにいれる)前に、
database/seeders/AdminTableSeeder.php
のrun()の中に、
このように追記して登録しないといけません

    public function run(): void
    {
        // \App\Models\User::factory(10)->create();

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);
        $this->call(AdminTableSeeder::class);
    }

そして
php artisan db:seed –class=AdminTableSeeder
を実行したらデータが入ります。

では次に、laravel10でadminのログインページを作っていきましょう

ログインするURLを決めないといけないので、
今決めます。
/admin/login にします。

URLのルーティングは
routes/web.php
に記載することが多いので、
こちらに書いてみます。

// 管理者ログイン画面の表示
Route::get('/admin/login', 'App\Http\Controllers\admin\AdminAuthController@showLoginForm')->name('admin.login');

// 管理者ログイン処理の実行
Route::post('/admin/login', 'App\Http\Controllers\admin\AdminAuthController@login')->name('admin.login.post');

// 管理者マイページの表示
Route::get('/admin/mypage', 'App\Http\Controllers\admin\AdminAuthController@showMypage')->name('admin.mypage')->middleware('auth.admin');

こちらの解説をすると,

Route::post()とかのこのpostって何?って話ですが、
URL毎にデータ送信や取得、更新の役割を定義していきます。
get postだけでなくて、

  • .get: GETメソッドに対応するルートのURLを生成。
  • .post: POSTメソッドに対応するルートのURLを生成。
  • .put: PUTメソッドに対応するルートのURLを生成
  • .patch: PATCHメソッドに対応するルートのURLを生成
  • .delete: DELETEメソッドに対応するルートのURLを生成
  • .options: OPTIONSメソッドに対応するルートのURLを生成。

などがあります。
Route::middleware(‘auth.admin’)は、
auth.adminという名前のミドルウェアを
このrouteに適用するよということを意味します。

ルーティング処理がされる前に
ミドルウェアの機能が適応されます。
この例だと、auth.adminというミドルウェアが
/admin/mypageのルーティングを見るより早く実行されますよ
ということを示します。


はい、ここまでわかったらちょっと休憩。

休憩がてらバンコクで今一番勢いがある鮨屋
「鮨みさき伸」にきました。

久兵衛→あきつき→とかみ→鮨みさき→鮨みさき伸
という鮨の系譜らしいです。
まぁわたしからしたら
php5→zend→zend2→cake→synfony→laravel
みたいなものですね。
cvn→svn→git
みたいなものともいいます。

顔からして明らかに伸さんだと思います(あとみんなタイ人だし)

おいしかったです。

はい、では、気を取り直して、

ログインのためのコントローラーを作成していきます。

本当はserviceパターンで作りたいですが、
まずはべたで書いていきます。

まずappコンテナで、

php artisan make:controller admin/AdminAuthController

を実行します。
すると
app/Http/Controllers/admin/AdminAuthController.php
ができるので、
その中身を

<?php

namespace App\Http\Controllers\admin;

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

class AdminAuthController extends Controller
{
    // ログインフォームの表示
    public function showLoginForm()
    {
        return view('admin.login');
    }

    // ログイン処理の実行
    public function login(Request $request)
    {
        // 入力データの検証
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        //requestを使ってemailとpasswordだけにする
        $credentials = $request->only('email', 'password');

        // 認証処理...といってもただemailで一人だけもってきてるだけ。
        $admin = Admin::where('email', $credentials['email'])->first();

        //もってきたadminのパスワードが入力したパスワードのハッシュ値と同じかどうかみてる
        if ($admin && Hash::check($credentials['password'], $admin->password)) {
            // ログイン成功時の処理 情報をセッションに入れてる
            Auth::guard('admin')->login($admin);
            return redirect()->route('admin.mypage');
        } else {
            // ログイン失敗時の処理
            return back()->withErrors(['login' => 'ログインに失敗しました。']);
        }
    }

    // マイページの表示
    public function showMypage()
    {
        return view('admin.mypage');
    }
}


にします。(guardに関しては後ほど解説します。)

これを動かすためにmodelを作ります。

まずコンテナの中で、
php artisan make:model Admin
でmodelのファイルを作り、
そのファイルを次のように直します。

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable
{
    // テーブル名の指定
    protected $table = 'admins';

    // 可変項目
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // パスワードのハッシュ化
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }
}

ここで、Auth::guard(‘admin’)->login($admin);
について理解する必要があります。
これは、Laravel の認証システムで特定の認証ガード(Guard)を指定して認証操作を行うための機能です。
引数にadminと入れると、adminという名前の認証ガードが動くことになります。今はそもそも作ってないのでエラーになってしまいます。なので、、adminガードを作りましょうか。

guardはlaravel10でも変わらずに書く場所が決まっていてconfig/auth.phpを開きます。
そのコードにadminのガードを追記します。
追記はguardの配列とproviderの配列に追記します。

これが、guardの配列の追記

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

そしてこれが、provider部分の追記
これでguardの設定はできました。

今回はlaravel10でserviceを使っていこうと思うので、serviceクラスを作っていきます。

めざすはlaravel10でservice repositoryのデザインパターンです。

まずはserviceを作ります。

<?php
namespace App\Services;

use App\Models\Admin;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class AdminAuthService
{
    public function authenticate($email, $password)
    {
        $admin = Admin::where('email', $email)->first();

        if ($admin && Hash::check($password, $admin->password)) {
            Auth::guard('admin')->login($admin);
            return true;
        }

        return false;
    }
}

なので、
先ほどのコントローラーは
下記のように書き換えることができるはずです。

<?php

namespace App\Http\Controllers\admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Admin;
use Illuminate\Support\Facades\Auth;
use App\Services\AdminAuthService;
use Illuminate\Support\Facades\Hash;

class AdminAuthController extends Controller
{
    protected $adminAuthService;

    public function __construct(AdminAuthService $adminAuthService)
    {
        $this->adminAuthService = $adminAuthService;
    }

    // ログインフォームの表示
    public function showLoginForm()
    {
        return view('admin.login');
    }

    // ログイン処理の実行
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        $email = $request->input('email');
        $password = $request->input('password');

        if ($this->adminAuthService->authenticate($email, $password)) {
            return redirect()->route('admin.mypage');
        } else {
            return back()->withErrors(['login' => 'ログインに失敗しました。']);
        }
    }

    // マイページの表示
    public function showMypage()
    {
        return view('admin.mypage');
    }
}

まだこれだと完成ではなく、
バリデーションのためのRequestクラスを作っていきます。
laravel10でもこの辺りは同じです。

app/Http/RequestsディレクトリにAdminRequest.phpファイルを作成し、以下のようにコードを記述します

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class AdminRequest extends FormRequest
{
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    // 認可のルールを指定する場合はここに記述
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'email' => 'required|email',
      'password' => 'required',
    ];
  }
  public function attributes()
  {
    return [
      'email' => 'メール',
      'password' => 'パスワード',
    ];
  }
  public function messages()
  {
    $attributes = $this->attributes(); // attributes() の値を取得

    return [
      'email.required' => $attributes['email'] . 'は必須ですよ',
      'password.required' => $attributes['password'] . 'は必須ですよ',
      'email.email' => $attributes['email'] . 'の形式で入力してください',
    ];
  }
}

このrequestクラスがバリデーションをしてくれるので、
AdminAuthControllerのコードを更新します。use App\Http\Requests\AdminRequest;を追加し、login()メソッドの引数をAdminRequest $requestに変更します。また、バリデーションの処理も修正して、AdminRequestクラスを使用するようにします。

<?php

namespace App\Http\Controllers\admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\AdminRequest;
use App\Services\AdminAuthService;

class AdminAuthController extends Controller
{
    protected $adminAuthService;

    public function __construct(AdminAuthService $adminAuthService)
    {
        $this->adminAuthService = $adminAuthService;
    }

    // ログインフォームの表示
    public function showLoginForm()
    {
        return view('admin.login');
    }

    // ログイン処理の実行
    public function login(AdminRequest $request)
    {
        // バリデーションの実行
        $validatedData = $request->validated();

        $email = $validatedData['email'];
        $password = $validatedData['password'];

        if ($this->adminAuthService->authenticate($email, $password)) {
            return redirect()->route('admin.mypage');
        } else {
            return back()->withErrors(['login' => 'ログインに失敗しました。']);
        }
    }

    // マイページの表示
    public function showMypage()
    {
        return view('admin.mypage');
    }
}

だいぶ綺麗なコードになったと思います。
login(AdminRequest $request){}
この書き方はlaravel10以前からlaravelでの一般的な記法で、
DI(Dependency Injection)と言ったりします。
この引数の中でバリデーションがもう終わってる感じですね。

DIは、クラスの依存関係を外部から注入することで、クラス間の疎結合を実現し、コードのテストや保守性の向上に役立ちます。
今回は関係ないんでsが1:nやn:nの諸々を解決した上でインスタンス化してくれる便利なやつです。

次にlaravel10でログインのためのviewを作っていきます。


レイアウトを決める
resources/views/layouts/admin.blade.php
とそれを継承した
resources/views/admin/login.blade.php
を作ります。

レイアウトのhtmlのコードは今回みなさんご存知のbootstrapをCDNで使おうと思いますので、
このようにしてください

<!-- resources/views/layouts/admin.blade.php -->
<!DOCTYPE html>
<html>

<head>
  <title>管理者用レイアウト</title>
  <!-- Bootstrap CDN の CSS リンク -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <!-- 追加の CSS リンクなど、共通のヘッドセクションの内容をここに記述 -->
</head>

<body>
  <!-- 共通のヘッダーセクションの内容をここに記述 -->

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

  <!-- 共通のフッターセクションの内容をここに記述 -->

  <!-- Bootstrap CDN の JavaScript リンク -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  <!-- 追加の JavaScript リンクなど、共通のフッターセクションの内容をここに記述 -->
</body>

</html>

そして、ログインページのHTMLはこんな感じで、


<!-- resources/views/admin/login.blade.php -->
@extends('layouts.admin')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">管理者ログイン</div>

                    <div class="card-body">
                        @if ($errors->any())
                            <div class="alert alert-danger">
                                <ul>
                                    @foreach ($errors->all() as $error)
                                        <li>{{ $error }}</li>
                                    @endforeach
                                </ul>
                            </div>
                        @endif

                        <form method="POST" action="{{ route('admin.login.post') }}">
                            @csrf

                            <div class="mb-3">
                                <label for="email" class="form-label">メールアドレス:</label>
                                <input type="email" name="email" id="email" class="form-control" required autofocus>
                            </div>

                            <div class="mb-3">
                                <label for="password" class="form-label">パスワード:</label>
                                <input type="password" name="password" id="password" class="form-control" required>
                            </div>

                            <button type="submit" class="btn btn-primary">ログイン</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

bootstrapはあまり解説はしないでおきますが、
フォーム全体をカード (card) コンポーネントで囲み、
各入力フィールドにはフォームコントロール (form-control) クラスを適用して作っています。

ここで、ログインのためにミドルウェアを作る解説をします。

余談ですが「Laravel10 ログイン時だけ表示したい画面をsanctumで実現」というブログもちょっとスピンアウトで実装したりしてみましたが、
もっとシンプルな初学者向けのやり方で実現していきます。

まず
コンテナに入り、
php artisan make:middleware AdminMiddleware
を実行すると
app/Http/Middleware/AdminMiddleware.php
が作られますので、
このコードにします。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class AdminMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->user() && $request->user()->isAdmin()) {
            return $next($request);
        }

        abort(403, 'Unauthorized');
    }
}

このミドルウェアはroutigが事項される前に呼ばれることになります。routeの記述を見るとmiddlewareが実行されていることがわかるので上のrouteのコードを再度確認してみると良いですね。

ちなみにrouteファイルでadmin.loginといってmiddlewareを認識させているのがわかると思うのですが、
それはさすがにkernelで登録してあげないと認識できないので、
追記してあげましょう

    protected $routeMiddleware = [
        // 他のミドルウェアの登録

        'auth.admin' => \App\Http\Middleware\AdminMiddleware::class,
    ];

この関数をapp/Console/kernel.phpに追記します。
これでrouteファイルで使えるようになりました。

laravel10でmypageを作ってみよう
(解説開始)

せっかくlaravel10でログインを作ったので、
mypageを見て安心したいので、
すべてのマイページのviewファイル
mypage.blade.phpのコードとそのすべてのroutingのコードを書いてみましょうか。

@extends('layouts.admin')

@section('content')
<div class="container">
  <h2>マイページ</h2>

  <!-- 共通メニューの読み込み -->
  @include('admin.common.menu')
  <!-- メインコンテンツ -->
  <div class="row mt-4">
    <div class="col-md-6">
      <div class="card">
        <div class="card-header">
          <h5>購入未対応者数</h5>
        </div>
        <div class="card-body">
          <p>ここに購入未対応者数の内容を表示</p>
        </div>
      </div>
    </div>
    <div class="col-md-6">
      <div class="card">
        <div class="card-header">
          <h5>問合せ一覧</h5>
        </div>
        <div class="card-body">
          <p>ここに問合せ一覧の内容を表示</p>
        </div>
      </div>
    </div>
  </div>
</div>
@endsection

admin/common/menu.blade.phpにメニュー部分を移動し、
それを読み込む形にしました。
admin.menu.bladeのコード解説はこちらになります。

 <!-- 共通メニュー -->
 <nav class="navbar navbar-expand-lg navbar-light bg-light">
   <div class="container-fluid">
     <ul class="navbar-nav">
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.admins.index') }}">Admin一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.products.index') }}">商品一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.categories.index') }}">商品カテゴリー一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.tags.index') }}">商品タグ一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.buyers.index') }}">購入者一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.articles.index') }}">記事一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.article-categories.index') }}">記事カテゴリー一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.article-tags.index') }}">記事タグ一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.restaurants.index') }}">飲食店一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.restaurant-categories.index') }}">飲食カテゴリー一覧</a>
       </li>
       <li class="nav-item">
         <a class="nav-link" href="{{ route('admin.restaurant-tags.index') }}">飲食タグ一覧</a>
       </li>
     </ul>
   </div>
 </nav>

これから作っていくrouteのファイルは..
adminのprefixを使って書きます。

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::prefix('admin')->group(function () {
    // 管理者ログイン画面の表示
    Route::get('login', 'App\Http\Controllers\admin\AdminAuthController@showLoginForm')->name('admin.login');

    // 管理者ログイン処理の実行
    Route::post('login', 'App\Http\Controllers\admin\AdminAuthController@login')->name('admin.login.post');

    // 管理者マイページの表示
    Route::get('mypage', 'App\Http\Controllers\admin\AdminAuthController@showMypage')->name('admin.mypage')->middleware('auth.admin');

    // Admin一覧
    Route::get('admins', 'App\Http\Controllers\admin\AdminController@index')->name('admin.admins.index');

    // 商品一覧
    Route::get('products', 'App\Http\Controllers\admin\ProductController@index')->name('admin.products.index');

    // 商品カテゴリー一覧
    Route::get('categories', 'App\Http\Controllers\admin\CategoryController@index')->name('admin.categories.index');

    // 商品タグ一覧
    Route::get('tags', 'App\Http\Controllers\admin\TagController@index')->name('admin.tags.index');

    // 購入者一覧
    Route::get('buyers', 'App\Http\Controllers\admin\BuyerController@index')->name('admin.buyers.index');

    // 記事一覧
    Route::get('articles', 'App\Http\Controllers\admin\ArticleController@index')->name('admin.articles.index');

    // 記事カテゴリー一覧
    Route::get('article-categories', 'App\Http\Controllers\admin\ArticleCategoryController@index')->name('admin.article-categories.index');

    // 記事タグ一覧
    Route::get('article-tags', 'App\Http\Controllers\admin\ArticleTagController@index')->name('admin.article-tags.index');

    // 飲食店一覧
    Route::get('restaurants', 'App\Http\Controllers\admin\RestaurantController@index')->name('admin.restaurants.index');

    // 飲食カテゴリー一覧
    Route::get('restaurant-categories', 'App\Http\Controllers\admin\RestaurantCategoryController@index')->name('admin.restaurant-categories.index');

    // 飲食タグ一覧
    Route::get('restaurant-tags', 'App\Http\Controllers\admin\RestaurantTagController@index')->name('admin.restaurant-tags.index');
});

とすると、、
見れるはず…

できてるっぽいです。
できたのでちょっとでかけて休憩しました。

では、最後の解説へ…ひと頑張り、

laravel10でのadmin制作としては最後にブログの作成、一覧、詳細の機能を解説しますのでやっていきましょう。

ブログそのものの前に、まずはブログのカテゴリーに関連するルーティング、マイグレーション、モデル、リポジトリ、サービス、コントローラー、リクエスト、ビューのコードを以下に示していきます。

ルーティングから解説



Route::prefix('admin')->name('admin.')->group(function () {
    // ブログカテゴリー一覧
    Route::get('blog-categories', 'App\Http\Controllers\Admin\BlogCategoryController@index')->name('blog-categories.index');

    // ブログカテゴリー作成フォーム表示
    Route::get('blog-categories/create', 'App\Http\Controllers\Admin\BlogCategoryController@create')->name('blog-categories.create');

    // ブログカテゴリー保存
    Route::post('blog-categories', 'App\Http\Controllers\Admin\BlogCategoryController@store')->name('blog-categories.store');

    // ブログカテゴリー詳細
    Route::get('blog-categories/{category}', 'App\Http\Controllers\Admin\BlogCategoryController@show')->name('blog-categories.show');

    // ブログカテゴリー編集フォーム表示
    Route::get('blog-categories/{category}/edit', 'App\Http\Controllers\Admin\BlogCategoryController@edit')->name('blog-categories.edit');

    // ブログカテゴリー更新
    Route::put('blog-categories/{category}', 'App\Http\Controllers\Admin\BlogCategoryController@update')->name('blog-categories.update');

    // ブログカテゴリー削除
    Route::delete('blog-categories/{category}', 'App\Http\Controllers\Admin\BlogCategoryController@destroy')->name('blog-categories.destroy');
});

ですね。前は記事をarticleという表記にしようとしたけど一旦blogで書いてみます。

次はカテゴリーのマイグレーション解説を書いていきます。

まずはマイグレーションファイル (create_blog_categories_table.php):を作るためにコンテナに入り、
php artisan make:migration create_blog_categories_table –create=blog_categories
でmigrationファイルを作り、そこコードを下記にします。

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

class CreateBlogCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('blog_categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

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

そして、コンテナでphp artisan migrateを実行するとテーブルが造られます。
テーブルが造られたので次はモデルですかね、
書いていきましょうか。

次はカテゴリーのmodelを書きましょう。

modelのなかにBlogCategory.phpを作って中身を下記にしました。

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class BlogCategory extends Model
{
    protected $fillable = ['name'];
    
    public function blogs()
    {
        return $this->belongsToMany(Blog::class, 'blog_category_relationships', 'blog_category_id', 'blog_id');
    }
}

belogsToManyはカテゴリーとblogsが n:nの関係にあり、
その中間テーブル名はblog_category_relationshipsであり、
そこでは、中間テーブルの名前と中間テーブル内の外部キーのカラム名を指定しています。
というわけでblog_category_relationshipsも作らないといけなくなったのですが今は割愛します。あとで作成します。

次はリポジトリのコードです

app/Repogitories/BlogCategoryRepository.phpを作ってそこに
このコードを書きます。

<?php
namespace App\Repositories;

use App\Models\BlogCategory;

class BlogCategoryRepository
{
    public function getAll()
    {
        return BlogCategory::all();
    }
    
    public function getById($id)
    {
        return BlogCategory::findOrFail($id);
    }
    
    // 他の必要なメソッドも追加可能
}

で、これを呼び出すServiceを作ります。

ブログカテゴリーのservice

<?php

namespace App\Services;

use App\Repositories\BlogCategoryRepository;

class BlogCategoryService
{
    protected $categoryRepository;
    
    public function __construct(BlogCategoryRepository $categoryRepository)
    {
        $this->categoryRepository = $categoryRepository;
    }
    
    public function getAllCategories()
    {
        return $this->categoryRepository->getAll();
    }
    
    public function getCategoryById($id)
    {
        return $this->categoryRepository->getById($id);
    }
    
    // 他の必要なサービスメソッドも追加可能
}

repogitoryをインスタンス化してそこからrepogitoryに書いた関数を
呼び出してることがわかりますね。
ここまでシンプルなコードだとservice repogitoryで分ける必要あるの?って感じですが、分けましょう。

そしてコントローラーを書いていきます

ブログカテゴリーのコントローラー
一覧画面を作ります。

<?php
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use App\Services\BlogCategoryService;
use Illuminate\Http\Request;

class BlogCategoryController extends Controller
{
  protected $categoryService;

  public function __construct(BlogCategoryService $categoryService)
  {
    $this->categoryService = $categoryService;
  }

  public function index()
  {
    $categories = $this->categoryService->getAllCategories();

    return view('admin.blog-categories.index', compact('categories'));
  }

  // 他のアクションメソッドも追加していきます
}

バリデーションのためにrequestファイル作ります。

BlogCategoryRequest.phpを作りました。

<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class BlogCategoryRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
        ];
    }
}

ではカテゴリーの一覧のviewを作っていきましょう

カテゴリーの一覧ページのviewを作る

コントローラーの中で、
resources/admin/blog-categories/index.blade.php
を見るように書いたので、
ここにindexというファイルで記載していきます。

@extends('layouts.admin')

@section('content')
<div class="container">
  <h2>ブログカテゴリー一覧</h2>

  <div class="mb-3">
    <a href="{{ route('admin.blog-categories.create') }}" class="btn btn-primary">新規作成</a>
  </div>

  <table class="table">
    <thead>
      <tr>
        <th>ID</th>
        <th>カテゴリー名</th>
        <th>編集</th>
      </tr>
    </thead>
    <tbody>
      @foreach($categories as $category)
      <tr>
        <td>{{ $category->id }}</td>
        <td>{{ $category->name }}</td>
        <td>
          <a href="{{ route('admin.blog-categories.edit', $category->id) }}" class="btn btn-primary">編集</a>
        </td>
      </tr>
      @endforeach
    </tbody>
  </table>
</div>
@endsection

これで、ブログカテゴリーの一覧は見れるようになりました

ここで、コントローラーでブログカテゴリーをCRUDできるコードが欲しいので書きます。
先ほどのコントローラーをこのように変えたらOKです。

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

use App\Http\Controllers\Controller;
use App\Http\Requests\BlogCategoryRequest;
use App\Services\BlogCategoryService;
use Illuminate\Http\Request;

class BlogCategoryController extends Controller
{
    protected $categoryService;

    public function __construct(BlogCategoryService $categoryService)
    {
        $this->categoryService = $categoryService;
    }

    public function index()
    {
        $categories = $this->categoryService->getAllCategories();

        return view('admin.blog-categories.index', compact('categories'));
    }

    public function create()
    {
        return view('admin.blog-categories.create');
    }

    public function store(BlogCategoryRequest $request)
    {
        $this->categoryService->createCategory($request->validated());

        return redirect()->route('admin.blog-categories.index')->with('success', 'カテゴリーを作成しました。');
    }

    public function show($id)
    {
        $category = $this->categoryService->getCategoryById($id);

        return view('admin.blog-categories.show', compact('category'));
    }

    public function edit($id)
    {
        $category = $this->categoryService->getCategoryById($id);

        return view('admin.blog-categories.edit', compact('category'));
    }

    public function update(BlogCategoryRequest $request, $id)
    {
        $this->categoryService->updateCategory($id, $request->validated());

        return redirect()->route('admin.blog-categories.index')->with('success', 'カテゴリーを更新しました。');
    }

    public function destroy($id)
    {
        $this->categoryService->deleteCategory($id);

        return redirect()->route('admin.blog-categories.index')->with('success', 'カテゴリーを削除しました。');
    }
}

はい。そしてcreate.blade.phpで新規作成のviewもかきましょー

@extends('layouts.admin')

@section('content')
<div class="container">
  <h2>新規ブログカテゴリー作成</h2>

  <form method="POST" action="{{ route('admin.blog-categories.store') }}">
    @csrf

    <div class="form-group">
      <label for="name">カテゴリー名</label>
      <input type="text" name="name" id="name" class="form-control" required>
    </div>

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

先ほどのserviceとrepositoryコードには、create update deleteのサービスがなかったので、一応追記しました。(今回は一覧と作成しかやらないですが。)

<?php
namespace App\Services;

use App\Repositories\BlogCategoryRepository;

class BlogCategoryService
{
    protected $categoryRepository;

    public function __construct(BlogCategoryRepository $categoryRepository)
    {
        $this->categoryRepository = $categoryRepository;
    }

    public function getAllCategories()
    {
        return $this->categoryRepository->getAll();
    }

    public function getCategoryById($id)
    {
        return $this->categoryRepository->getById($id);
    }

    public function createCategory(array $data)
    {
        return $this->categoryRepository->create($data);
    }

    public function updateCategory($id, array $data)
    {
        return $this->categoryRepository->update($id, $data);
    }

    public function deleteCategory($id)
    {
        return $this->categoryRepository->delete($id);
    }
}

リポジトリはこうなりますよね

<?php

namespace App\Repositories;

use App\Models\BlogCategory;

class BlogCategoryRepository
{
  public function getAll()
  {
    return BlogCategory::all();
  }

  public function getById($id)
  {
    return BlogCategory::findOrFail($id);
  }

  public function create(array $data)
  {
    return BlogCategory::create($data);
  }

  public function update($id, array $data)
  {
    $category = $this->getById($id);
    $category->update($data);
    return $category;
  }

  public function delete($id)
  {
    $category = $this->getById($id);
    $category->delete();
  }
}

はい、これで、ブログカテゴリーのCURDができるようになりました。

余談ですがサービスを無視してリポジトリパターンだけでやってるlaravel10のブログはこちらです

はい、では、ブログカテゴリーのCURDの画面スクショで同じになってるか確認してください。

では全く同じ構造なのでブログのためのタグを作成しましょう。

これはコードはcategoryのところをtagにしていくだけなので、
コードは書きませんので自分で上のを見てやってみましょう。
マイグレーションは
php artisan make:migration create_blog_tags_table –create=blog_tags
で、あとはcontrollerもサービスもレポジトリもviewもリクエストも
ほとんどカテゴリーのコピーでいけますよね。

一応modelだけ書いておきます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class BlogTag extends Model
{
  protected $fillable = ['name'];

  public function blogs()
  {
    return $this->belongsToMany(Blog::class, 'blog_blog_tag', 'blog_tag_id', 'blog_id');
  }
}

はい。これで、記事のカテゴリーと記事のタグができたので、
次に記事そのものの一覧と作成画面を作ってみましょう。
さっきも書きましたが、
ルーティングでarticleよりもblogの方がいいかなと思いまして、
articleという文字をblogで一括置換してます。

laravel10での記事作成画面のポイント

ポイントは
1. 記事作成画面で、カテゴリーとタグを選択する
2. キャッチ画像を表示する
3. エディタはCKeditorを使う
あたりですかね。

これでやってみます。

まずルーティングは

// 記事作成画面
Route::get('blog/create', 'App\Http\Controllers\admin\BlogController@create')->name('admin.blog.create');
Route::post('blog', 'App\Http\Controllers\admin\BlogController@store')->name('admin.blog.store');

// 記事更新画面
Route::get('blog/{id}/edit', 'App\Http\Controllers\admin\BlogController@edit')->name('admin.blog.edit');
Route::put('blog/{id}', 'App\Http\Controllers\admin\BlogController@update')->name('admin.blog.update');

// 記事削除
Route::delete('blog/{id}', 'App\Http\Controllers\admin\BlogController@destroy')->name('admin.blog.destroy');

次にmodelを作っていきましょう。

blogのmodelを作っていきます。
まずはマイグレーションとして、

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    protected $fillable = [
        'title',
        'blog_category_id',
        'content',
        'catch_image'
    ];

    public function blogCategory()
    {
        return $this->belongsTo(BlogCategory::class);
    }

    public function tags()
    {
        return $this->belongsToMany(BlogTag::class);
    }
}

もう分かってきてると思いますが、
fillableはフォームから通すカラム名を書いていくことになります。
そして1:nの関係を記載するのがmodelです。

マイグレーションは

<?php

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

class CreateBlogTable extends Migration
{
    public function up()
    {
        Schema::create('blogs', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('catch_image')->nullable();
            $table->timestamps();
        });
    }

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

にしました。php artisan migrateを実行しました。

docker-compose.ymlに8080で
情弱ツールであるphpmyadminも入れておきました。


次に、laravel10の基礎コード解説としては終わりが見えてきましたが、blogテーブルとしてblog_category_idを追加します。

そのマイグレーションコードはこれ

<?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('blogs', function (Blueprint $table) {
            $table->unsignedBigInteger('blog_category_id')->after('title');
        });
    }

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

blogとblog_tagのリレーションテーブル(blog_blog_tagテーブル)を作ります。

マイグレーションはこれ

<?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('blog_blog_tag', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('blog_id');
            $table->unsignedBigInteger('blog_tag_id');
            $table->timestamps();
        });
    }

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

ではバリデーションのためにBlogのrequestを書いていきましょう。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class BlogRequest extends FormRequest
{
  public function authorize()
  {
    return true; // 認可のチェックは別途行う場合は、必要に応じて変更してください
  }

  public function rules()
  {
    return [
      'title' => 'required|string|max:255',
      'content' => 'required|string',
      'catch_image' => 'image|mimes:jpeg,png,jpg,gif|max:2048', // 画像のバリデーションルール
      // その他のフォームフィールドのバリデーションルール
    ];
  }
}

次に記事のコントローラーを記載します。

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\BlogRequest;
use App\Services\BlogService;

class BlogController extends Controller
{
    protected $blogService;

    public function __construct(BlogService $blogService)
    {
        $this->blogService = $blogService;
    }

    public function create(BlogService $blogService)
    {
        // カテゴリーとタグの一覧を取得する
        $categories = $blogService->getAllCategories();
        $tags = $blogService->getAllTags();

        // ブログ作成画面を表示するビューを返す
        return view('admin.blog.create', compact('categories', 'tags'));
    }

/** ダメな例 
    public function store(BlogRequest $request)
    {
        // ブログを作成して保存するロジックをサービスに委譲する
        $data = $request->validated();

        if ($request->hasFile('catch_image')) {
            $imagePath = $request->file('catch_image')->store('public/images');
            $data['catch_image'] = $imagePath;
        }

        $blog = $this->blogService->createBlog($data);
        dd($blog);
        exit;
        // 成功時のリダイレクトやメッセージ表示などを行う
        return redirect()->route('admin.blog.index')
            ->with('success', 'ブログが作成されました');
    }
**/
    public function store(BlogRequest $request)
    {
        
        $data = $request->validated();

        if ($request->hasFile('catch_image')) {
            $date = date('YmdHis', time());
            $fileName = $date . uniqid() . '.' . $request->file('catch_image')->getClientOriginalExtension();
            $request->file('catch_image')->storeAs('public/images', $fileName);
            $data['catch_image'] = $fileName;
        }

        $blog = $this->blogService->createBlog($data);
        dd($blog);
        exit;
        // 成功時のリダイレクトやメッセージ表示などを行う
        return redirect()->route('admin.blog.index')
            ->with('success', 'ブログが作成されました');
    }

    public function index()
    {

        $blog = $this->blogService->getBlogs();
        dd($blog);
        exit;
        // 成功時のリダイレクトやメッセージ表示などを行う
        return redirect()->route('admin.blog.index')
            ->with('success', 'ブログが作成されました');
    }


    public function edit($id)
    {
        // 指定されたIDのブログを取得するロジックをサービスに委譲する
        $blog = $this->blogService->getBlogById($id);

        // ブログ編集画面を表示するビューを返す
        return view('admin.blog.edit', compact('blog'));
    }

    public function update(BlogRequest $request, $id)
    {
        // 指定されたIDのブログを更新するロジックをサービスに委譲する
        $this->blogService->updateBlog($id, $request->validated());

        // 成功時のリダイレクトやメッセージ表示などを行う
        return redirect()->route('admin.blog.edit', ['id' => $id])
            ->with('success', 'ブログが更新されました');
    }
}

では、ブログサービスとレポジトリを書いていきます。

<?php

namespace App\Services;

use App\Repositories\BlogRepository;
use App\Repositories\BlogCategoryRepository;
use App\Repositories\BlogTagRepository;

class BlogService
{

    protected $blogRepository;
    protected $blogCategoryRepository;
    protected $blogTagRepository;

    public function __construct(
        BlogRepository $blogRepository,
        BlogCategoryRepository $blogCategoryRepository,
        BlogTagRepository $blogTagRepository
    ) {
        $this->blogRepository = $blogRepository;
        $this->blogCategoryRepository = $blogCategoryRepository;
        $this->blogTagRepository = $blogTagRepository;
    }

    public function createBlog(array $data)
    {
        // ブログを作成して保存するロジックを実装する
        $blog = $this->blogRepository->create($data);

        // タグを関連付ける
        $tags = $data['tags'] ?? [];
        $this->syncTags($blog, $tags);

        return $blog;
    }

    public function updateBlog(int $id, array $data)
    {
        // 指定されたIDのブログを取得する
        $blog = $this->blogRepository->findById($id);

        // ブログを更新するロジックを実装する
        $blog->update($data);

        // タグを関連付ける
        $tags = $data['tags'] ?? [];
        $this->syncTags($blog, $tags);

        return $blog;
    }

    public function getBlogById(int $id)
    {
        return $this->blogRepository->findById($id);
    }

    public function getBlogs()
    {
        return $this->blogRepository->getBlogs();
    }


    protected function syncTags($blog, $tags)
    {
        // タグの関連を同期するロジックを実装する
        // dd($blog->tags());
        $blog->tags()->sync($tags);
    }

    public function getAllCategories()
    {
        return $this->blogCategoryRepository->getAll();
    }

    public function getAllTags()
    {
        return $this->blogTagRepository->getAll();
    }
}

同じ要領で、レポジトリも書いていきます。

<?php

namespace App\Repositories;

use App\Models\Blog;

class BlogRepository
{
  public function create(array $data)
  {
    // ブログの作成と保存を行うロジックを実装する
    return Blog::create($data);
  }

  public function findById(int $id)
  {
    // 指定されたIDのブログを取得するロジックを実装する
    return Blog::findOrFail($id);
  }

  public function getBlogs()
  {
    //N+1を産むダメな例
    // return Blog::get();
    
    //N+1問題を解消するためにこっちがベターです。
    return Blog::with('tags')->get();
  }
}

そしてcssをつけたviewで
admin.blog.create.blade.phpを作っていきます。
まずview側のbladeは

@extends('layouts.admin')

@section('content')
    <h1>ブログ作成</h1>

    <form action="{{ route('admin.blog.store') }}" method="POST" enctype="multipart/form-data" class="blog-form">
        @csrf

        <div class="form-group">
            <label for="title">タイトル</label>
            <input type="text" name="title" id="title" class="form-control">
        </div>

        <div class="form-group">
            <label for="content">内容</label>
            <textarea name="content" id="content" rows="5" class="form-control"></textarea>
        </div>

        <div class="form-group">
            <label for="catch_image">キャッチ画像</label>
            <input type="file" name="catch_image" id="catch_image" class="form-control-file">
        </div>

        <div class="form-group">
            <label for="categories">カテゴリー</label>
            <select name="categories[]" id="categories" multiple class="form-control">
                @foreach ($categories as $category)
                    <option value="{{ $category->id }}">{{ $category->name }}</option>
                @endforeach
            </select>
        </div>

        <div class="form-group">
            <label for="tags">タグ</label>
            <select name="tags[]" id="tags" multiple class="form-control">
                @foreach ($tags as $tag)
                    <option value="{{ $tag->id }}">{{ $tag->name }}</option>
                @endforeach
            </select>
        </div>

        <button type="submit" class="btn btn-primary">作成</button>
    </form>

    <script src="https://cdn.ckeditor.com/ckeditor5/33.0.0/classic/ckeditor.js"></script>
    <script>
        ClassicEditor
            .create(document.querySelector('#content'))
            .catch(error => {
                console.error(error);
            });
    </script>
@endsection

CKエディタを使っているところがポイントです。
便利ですよね。

フォーム要素にBootstrapのクラスを適用していますので、

form-group のclassはフォームグループ全体のスタイルを設定します。
form-control クラスはテキスト入力やテキストエリアのスタイルをきれいにしてます。
form-control-fileは、ファイル入力のスタイルをそれっぽくしています。
btn クラス: bootstrapのいつものスタイルを設定。
btn-primary クラス: プライマリ色のボタンスタイルを設定してます。

postすると

きてますね。
これを

public function index()
{
    $blogs = $this->blogService->getBlogs();
    return view('admin.blog.index', compact('blogs'));
}

にしてviewにわたします。

ということでブログ一覧のHTMLを作っていきましょう。

@extends('layouts.admin')

@section('content')

<h1>ブログ一覧</h1>

<table class="table table-bordered">
    <thead>
        <tr>
            <th>タイトル</th>
            <th>内容</th>
            <th>カテゴリー</th>
            <th>タグ</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        @foreach ($blogs as $blog)
        <tr>
            <td>{{ $blog->title }}</td>
            <td>{{ Str::limit($blog->content, 100) }}</td>
            <td>{{ $blog->blogCategory->name }}</td>
            <td>
                @if (!$blog->tags->isEmpty())
                @foreach ($blog->tags as $tag)
                <span>{{ $tag->name }}</span>
                @endforeach
                @endif
            </td>
            <td>
                <a href="{{ route('admin.blog.edit', $blog->id) }}" class="btn btn-success btn-sm">編集</a>
                <form method="POST" action="{{ route('admin.blog.destroy', $blog->id) }}"
                    style="display: inline-block;">
                    @csrf
                    @method('DELETE')
                    <button type="submit" class="btn btn-danger btn-sm">削除</button>
                </form>
            </td>
        </tr>
        @endforeach
    </tbody>
</table>

<a href="{{ route('admin.blog.create') }}" class="btn btn-primary">新規作成</a>

@endsection

@if (!$blog->tags->isEmpty()) laravelの$blogのオブジェクトはmodelから持ってきたものですが、laravelでDBから持ってきたオブジェクトはコレクションと呼ぶと思ってください。そしてlaravelのコレクションでemptyチェックをする場合はこのようにisEmptyを使います。

はい。

adminはこれでひと段落したかな。。

さて、余談ですがこの象見てください。
ピンクの像。バンコクから少し離れたところにおやすみになられている象なのですが

この象の下にいるねずみに願い事を耳打ちすると
3倍のスピードで叶えてくれる
という迷信があります。
まったく頼りにならない顔をしてるネズミなのに…そんな力が..(あるわけないだろうに)

では最後にユーザ側で表示をしてみましょう。

ユーザ側はご存知laravel10でもデフォルトでは…
うぉ…

Route::get('/', function () {
    return view('welcome');
});

viewいきなり返してましたねw
LPじゃないのでさすがにこれはやめましょう。

うーん..そうですね…
コントローラーにUser/TopController.phpでも作ってそこの
indexアクションをトップにしようと思います。

routingは
上のviewを返してるのはコメントアウトして
Route::get(‘/’, ‘App\Http\Controllers\user\TopController@index’)->name(‘user.top.index’);
を記載しました。

そしてコントローラーは
こうしました。

コピペせずに自分で書いてみましょう。
単純にブログサービスをコンストラクタでDIしてインスタンス化し、
blogserviceの中のgetBlogsをもってきてviewに渡してるだけのコードです。

またアクセスするとviewがないよというエラーになると思うので
viewを書いていきます。

このようにpublicフォルダに自分で作ったコーディングのcssやjsやimageなどをそのままぶち込んでみてください。

そしてresouces/user/top/index.blade.phpで
コーディングしたファイルをペーストします。
本当はlayout/user/userLayout.blade.phpとかを継承したいところですが、
ちょっとこのブログ長すぎなんじゃないかと思い始めたのでそのまま行きます。

今、この状態で私のブログはこのように見えています。

ときどきでてくるたましいのようなキャラクター、
帽子かぶってますよね。その帽子に「月」と入っていますが、
私、月岡の月でございます。

では、
ブログを組み込んでいきます。

まずルーティングでブログ一覧のコントローラーを読み込ませたいので、
Route::get(‘/’, ‘App\Http\Controllers\user\BlogController@index’)->name(‘user.blog.index’);
そして、
top/index.blade.phpの中でブログ一覧への遷移をしたいときは、

            <li class="header_menu_item">
              <a href="{{ route('user.blog.index') }}" class="header_menu_link">
                記事一覧
              </a>
            </li>

こんな感じの関数でリンクを生成します。

ブログのコントローラーも作る前にトップにもブログの一覧があるので
その部分を動的にしてみます。

この部分ですね。

ちなみにもしrepositoryでwithをつかってN+1問題を解決していなかったらviewはこのようになるんですよね

                <div class="column_box_wrap">
                  @foreach ($blogs as $blog)
                  <div class="column_box">
                    <div class="new">
                      <img src="static/images/common/icon_new.svg" alt="NEW">
                    </div>
                   
                  <a href="column_detail.html" class="column_box_img box_img" style="background-image:url({{ asset('storage/images/'.$blog->catch_image) }});">
                    </a>
                    <div class="column_box_category_date">
                      <a href="column_list.html" class="column_box_category">
                        <p>{{ $blog->blogCategory->name }}</p>
                      </a>
                      <p class="column_box_date">
                        {{ $blog->created_at->format('Y.m.d') }}
                      </p>
                    </div>
                    <a href="column_detail.html" class="column_box_title_link">
                      <h3 class="column_box_title">
                        {{ $blog->title }}
                      </h3>
                    </a>
                    <ul class="column_box_tag_list">
                      @foreach ($blog->tags as $tag)
                      <li class="column_box_tag_item">
                        <a href="column_list.html" class="column_box_tag_link">
                          {{ $tag->name }}
                        </a>
                      </li>
                      @endforeach
                    </ul>
                  </div>
                  @endforeach

これはあまりよくないので、
N+1を対応した下記のコードが正しいものとなります。

                            <div class="column_box_wrap">
                                @foreach ($blogs as $blog)
                                <div class="column_box">
                                    <div class="new">
                                        <img src="static/images/common/icon_new.svg" alt="NEW">
                                    </div>
                                    <a href="column_detail.html" class="column_box_img box_img"
                                        style="background-image:url({{ asset('storage/images/'.$blog->catch_image) }});">
                                    </a>
                                    <div class="column_box_category_date">
                                        <a href="column_list.html" class="column_box_category">
                                            <p>{{ $blog->blogCategory->name }}</p>
                                        </a>
                                        <p class="column_box_date">
                                            {{ $blog->created_at->format('Y.m.d') }}
                                        </p>
                                    </div>
                                    <a href="column_detail.html" class="column_box_title_link">
                                        <h3 class="column_box_title">
                                            {{ $blog->title }}
                                        </h3>
                                    </a>
                                    <ul class="column_box_tag_list">
                                        @foreach ($blog->tags as $tag)
                                        <li class="column_box_tag_item">
                                            <a href="column_list.html" class="column_box_tag_link">
                                                {{ $tag->name }}
                                            </a>
                                        </li>
                                        @endforeach
                                    </ul>
                                </div>
                                @endforeach


                            </div>

このように動的化しました。
このlaravelのN+1問題に関してはオフショアと日本の違いにも代表的な話としてでてくるので注目です。

そしてこれからblog一覧をやっていくにあたり、
共通パーツはやはりlayoutにしたくなってきたので、
つい今し方やらないっていったんですが、
やっぱやります。

layout/user.blade.phpに
HTMLの構造を書き、

<!DOCTYPE HTML>
<html lang="ja" prefix="og: http://ogp.me/ns#">

<head>
  <meta charset="utf-8">
  <title>はむれっつ</title>
  <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
  <meta name="viewport" content="width=device-width">
  <!---
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0">
  --->
  <meta name="description" content="">
  <meta name="keyword
  s" content="">
  <meta name="author" content="">
  <meta property="og:title" content="はむれっつ">
  <meta property="og:type" content="">
  <meta property="og:description" content="">
  <meta property="og:url" content="">
  <meta property="og:site_name" content="はむれっつ">
  <meta property="og:image" content="static/ogp.png">
  <link href="static/css/reset.css" rel="stylesheet" type="text/css" media="all" />
  <link href="static/css/style.css" rel="stylesheet" type="text/css" media="all" />
  <link href="static/animate/animate.min.css" rel="stylesheet" type="text/css" media="all" />
  <link rel="stylesheet" type="text/css" href="static/js/slick/slick.css" />
  <link rel="stylesheet" type="text/css" href="static/js/slick/slick-theme.css" />
  <link rel="apple-touch-icon" href="static/webclip.png">
  <script type="text/javascript" src="static/js/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="static/js/slick/slick.min.js"></script>
  <script src="static/animate/wow.min.js"></script>
  <script>
    new WOW().init();
  </script>
  <!---mplus 1p--->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@300;400;500;700;800;900&display=swap" rel="stylesheet">
  <!---/mplus 1p--->
  <!---Oswald--->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@700&display=swap" rel="stylesheet">
  <!---/Oswald--->
  @yield('head')
</head>

<body>
  @yield('header')

  @yield('main')

  <footer>
    <div class="footer_ham_wrap">
      <div class="footer_ham animate__animated animate__bounce animate__infinite wow" data-wow-duration="2s">
        <img src="static/images/common/img_footer_top.svg" alt="はむれっつ">
      </div>
    </div>
    <div class="footer_main">
      <div class="common_wrap">
        <div class="common_inner">
          <div class="footer_main_content">
            <div class="footer_main_ham_block">
              <a <?php ?> class="footer_logo">
                <img src="static/images/common/logo.svg" alt="はむれっつ">
              </a>
              <p class="footer_ham_text">
                はむれっつとは合同会社FIELDが飼っている架空のハムスターです。<br>
                見た目はモッツァレラ、動きは地鶏、触ってみると桜餅。<br>
                皆さんの頭の中のお花畑を根こそぎ枯らすハムスターです。
              </p>
            </div>
            <div class="footer_column_category_block">
              <ul class="footer_main_menu_list">
                <li class="footer_main_menu_item">
                  <a href="column.html" class="footer_main_menu_link">
                    記事
                  </a>
                </li>
              </ul>
              <ul class="footer_column_category_list">
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
                <li class="footer_column_category_item">
                  <a href="column_list.html" class="footer_column_category_link">
                    よくある質問
                  </a>
                </li>
              </ul>
            </div>
            <div class="footer_main_menu_block">
              <ul class="footer_main_menu_list">
                <li class="footer_main_menu_item">
                  <a href="comic.html" class="footer_main_menu_link">
                    漫画
                  </a>
                </li>
                <li class="footer_main_menu_item">
                  <a href="shop/index.html" class="footer_main_menu_link">
                    オンラインストア
                  </a>
                </li>
                <li class="footer_main_menu_item">
                  <a href="https://store.line.me/stickershop/author/68502/ja" class="footer_main_menu_link">
                    LINEスタンプ
                  </a>
                </li>
                <li class="footer_main_menu_item">
                  <a href="http://bar-hamlets.com/" class="footer_main_menu_link">
                    Bar hamlets
                  </a>
                </li>
                <li class="footer_main_menu_item">
                  <a href="contact.html" class="footer_main_menu_link">
                    お問い合わせ
                  </a>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
      <div class="footer_top_link_wrap">
        <a href="#top" class="footer_top_link">
          <img src="static/images/common/img_top_link.svg" alt="TOPに戻る">
        </a>
      </div>
    </div>
    <div class="footer_menu">
      <div class="common_wrap">
        <div class="common_inner">
          <ul class="footer_menu_list">
            <li class="footer_menu_item">
              <a href="https://field.asia/" class="footer_menu_link">
                運営会社
              </a>
            </li>
            <li class="footer_menu_item">
              <a href="privacy.html" class="footer_menu_link">
                プライバシーポリシー
              </a>
            </li>
            <li class="footer_menu_item">
              <a href="terms.html" class="footer_menu_link">
                利用規約
              </a>
            </li>
          </ul>
        </div>
      </div>
    </div>
    <div class="footer_copy">
      <div class="common_wrap">
        <div class="common_inner">
          <p class="copy">
            Copyright &copy;はむれっつ All Rights Reserved.
          </p>
        </div>
      </div>
    </div>
  </footer>
  <script type="text/javascript" src="static/js/common.js">
  </script>
</body>

</html>


結局topページのviewは

@extends('layouts.user')
  @section('head')
  @endsection

  @section('header')
  <header>
     ...コンテンツ
  </header>
  @endsection
  @section('main')
  <main id="top">
   .....コンテンツ
  </main>
  @endsection

となりました。

これで、ブログのTOPの表示部分は終わりました。

laravel10でユーザ側のブログ一覧表示

routingが、

Route::get('/blogs', 'App\Http\Controllers\user\BlogController@index')->name('user.blog.index');

ですね。

コントローラーは、

<?php

namespace App\Http\Controllers\user;

use App\Http\Controllers\Controller;
use App\Services\BlogService;
use Illuminate\Http\Request;

class BlogController extends Controller
{
  protected $blogService;

  public function __construct(BlogService $blogService)
  {
    $this->blogService = $blogService;
  }

  public function index()
  {
    $blogs = $this->blogService->getBlogs();

    return view('user.blog.index', compact('blogs'));
  }
}

ですね。

そしてviewは、indexのときに使った部分がそのまま作れるので、
簡単に埋め込みができると思います。

これで、ブログ一覧ができました。

ここまでlaravel10で通常のブログを作るときにまだできないものがあります。ページネーション

たとえばページネーション
とても簡単なのでつけていきましょう
まず
Controllers/user/BlogController.php

$blogs = $this->blogService->getBlogs();
を消しましょう。
その代わりに、
$blogs = $this->blogService->getPaginatedBlogs();
に変更します。

そして、
app/Repositories/BlogRepository.php
に、

  public function getPaginatedBlogs()
  {
    return Blog::with('tags')->orderBy('created_at', 'desc')->paginate(8);
  }

を追加します。
N+1にならないようにwithでtagをもってきて、作成日で並び替えました。

そして、
resources/views/user/blog/index.blade.php
のviewで明らかに静的なページネーション部分があるので
それを削除します。
具体的にはここです

そしてこれを追加。
ページネーション部分のviewを切り出したので
resources/views/user/pagination/blog.blade.php

を作っていきます。

/**  ページが複数存在する場合の分岐として書いています。**/
@if ($paginator->hasPages())
    <div class="page_wrap">
        <ul class="page_list">
            @if ($paginator->onFirstPage())
/**
現在のページが最初のページの場合、前へのリンクは表示しません。
**/
            @else
                <li class="page_item_prev">
                    <a href="{{ $paginator->previousPageUrl() }}">
                        <img src="static/images/common/icon_prev.svg" alt="<">
                    </a>
                </li>
            @endif
/**
(ページネーションの各ページ)をループします。
**/
            @foreach($elements as $element)
/**
elementが文字列であれば、その文字列をページ番号として表示します。
これは一般的には省略形のページリンク(「...」)です。
**/
                @if(is_string($element))
                    <li class="page_item active">
                        <a>
                            {{ $element }}
                        </a>
                    </li>
                @endif

                @if(is_array($element))
/**
elementが配列であれば、各ページに対応するリンクを作成します。ここで$pageはページ番号で、$urlはそのページのURLです。
**/
                    @foreach ($element as $page => $url)
                        @if ($page == $paginator->currentPage())
                            <li class="page_item active">
                                <a>
                                    {{ $page }}
                                </a>
                            </li>
                        @else
                            <li class="page_item">
                                <a href="{{ $url }}">
                                    {{ $page }}
                                </a>
                            </li>
                        @endif
                    @endforeach
                @endif
            @endforeach

            @if ($paginator->hasMorePages())
/**
さらにページがある場合、次へのリンクを表示します。
**/
                <li class="page_item_next">
                    <a href="{{ $paginator->nextPageUrl() }}">
                        <img src="static/images/common/icon_next.svg" alt=">">
                    </a>
                </li>
            @endif
        </ul>
    </div>
@endif

はい。できましたね。

ここまでの技術で全く同じものをつかってさくっと作って公開してみたサイトがあります。ホテル業界の転職のエージェントサイト。これはlaravel10で作ってあります。しっかりやるとこういう大掛かりにみえるものもできるってわかってもらえればと思います。

では最後にlaravel10でユーザのブログ詳細を作る解説にいきましょう

routingは
Route::get(‘blogs/{id}’, ‘App\Http\Controllers\user\BlogController@show’)->name(‘user.blog.show’);
これですね。
そしてBlogコントローラーに

  public function show($id)
  {
    $blog = $this->blogService->getBlogById($id);

    return view('user.blog.show', compact('blog'));
  }

を追加します。
そのviewも書いていきます。
まずは何も書かずに値が取れているかviewの中でddして確認してみます。

show.blade.phpの中で、
<?php dd($blog); ?>
をすると…

よし。きてますね。

では組み込みをしていきます。
もうheaderやサイドバーは共通化しているので、
メインコンテンツだけ動的化すれば完成です。

そういえば、viewでcssやjsを読み込む時に、
コーダーから渡されるHTMLは、


  <meta property="og:image" content="static/ogp.png">
  <link href="{{ asset('static/css/reset.css') }}" rel="stylesheet" type="text/css" media="all" />
  <link href="{{ asset('static/css/style.css') }}" rel="stylesheet" type="text/css" media="all" />
  <link href="static/animate/animate.min.css" rel="stylesheet" type="text/css" media="all" />
  <link rel="stylesheet" type="text/css" href="static/js/slick/slick.css" />
  <link rel="stylesheet" type="text/css" href="static/js/slick/slick-theme.css" />
  <link rel="apple-touch-icon" href="static/webclip.png">
  <script type="text/javascript" src="static/js/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="static/js/slick/slick.min.js"></script>
  <script src="static/animate/wow.min.js"></script>

だったりするのですが、
laravel10でもここは変わらずに

<meta property="og:image" content="{{ asset('static/ogp.png') }}">
<link href="{{ asset('static/css/reset.css') }}" rel="stylesheet" type="text/css" media="all" />
<link href="{{ asset('static/css/style.css') }}" rel="stylesheet" type="text/css" media="all" />
<link href="{{ asset('static/animate/animate.min.css') }}" rel="stylesheet" type="text/css" media="all" />
<link rel="stylesheet" type="text/css" href="{{ asset('static/js/slick/slick.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('static/js/slick/slick-theme.css') }}" />
<link rel="apple-touch-icon" href="{{ asset('static/webclip.png') }}">
<script type="text/javascript" src="{{ asset('static/js/jquery-3.3.1.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('static/js/slick/slick.min.js') }}"></script>
<script src="{{ asset('static/animate/wow.min.js') }}"></script>

このように書いたりします。

ちょっとshow.blade.phpのコードは長いので書きませんが、
こんな感じになるはずです

まだ色々動的化されてませんが、
表示することはできました。

そしたら、エディタの内容やキャッチやタイトルなどを
動的化を解説していきます。

             <div class="column_detail_category_date">
                <a href="column_list.html" class="column_detail_category">
                  {{$blog->blogCategory->name }}
                </a>
                <p class="column_detail_date">
                  <img src="/static/images/common/icon_date.svg" alt="更新日">
                  {{$blog->updated_at}}
                </p>
              </div>
              <h1 class="column_detail_title">
                {{ $blog->title }}
              </h1>
              <div class="column_detail_main_img box_img" style="background-image:url({{ asset('storage/images/'.$blog->catch_image) }}); height: 430.667px;">
              </div>
              <div id="column_editor">
                {!! $blog->content !!}
              </div>
              <!---/#column_editor--->

エディタのHTMLコードを表示する場合は{{! !}}この中に書きます。
modelでカテゴリーとのリレーションを書いてるので、
{{$blog->blogCategory->name }}でもってこれます

もってこれてますね。

では、

中級〜上級者のための解説コードも書いておきます。

「関連カテゴリーの記事取得」を通して実践用の本当のコードを書いて解説していきます

まずはmodelにこのようなコードを追記します。

Blogモデルの上で、
use Illuminate\Database\Eloquent\Builder;
をuseしています。
そして

    public function scopeBuildSearchQuery(Builder $query, array $params)
    {
        $query->orderBy('updated_at', 'DESC');

        $limit = data_get($params, 'limit', '');
        $query->limit($limit);

        $blogCategoryId = data_get($params, 'blog_category_id');
        if (!empty($blogCategoryId)) {
            $query->where('blog_category_id', $blogCategoryId);
        }

        $exclusionBlogId = data_get($params, 'exclusionBlogId');
        if (!empty($exclusionBlogId)) {
            $query->where('id', '!=', $exclusionBlogId);
        }

        return $query;
    }

これにより、検索用の関数ができました。
関数の前にscopeという名前をつけると
laravelの仕様で第一引数には検索クエリがくるよということになります。
使われる時はscopeという文字はつけません(あとからreposigotryのコード見て)
そのときの型をBuilderとして定義しています。
これもlaravelが用意してる型です。
scopeに関しては「こちらのブログのほうが解説がうまいです。

見ての通りですが、
デフォでは無限に記事をもってくるようになっていて、
DESCで並び替えています。

渡されるパラメータの中のキーに検索が付与されているなら
ifの中にはいり検索クエリを追加していくというコードです。

これがrepositoryでどう使われるかというと、

めんどうだからblog repositoryのコード全部さらすと

<?php

namespace App\Repositories;

use App\Models\Blog;

class BlogRepository
{
    public function create(array $data)
    {
        // ブログの作成と保存を行うロジックを実装する
        return Blog::create($data);
    }

    public function findById(int $id)
    {
        // 指定されたIDのブログを取得するロジックを実装する
        return Blog::findOrFail($id);
    }

    public function getBlogs()
    {
        return Blog::with('tags')->get();
    }

    public function getPaginatedBlogs()
    {
        return Blog::with('tags')->orderBy('created_at', 'desc')->paginate(8);
    }

    public function getSearchBlogs($params = [])
    {
        $latestBlogs = Blog::buildSearchQuery($params)
            ->get();
        return $latestBlogs;
    }
}

このようになります。
buildSearchQuery使ってますねparamを渡してるだけです。

ではこれを呼んでるserviceはどうなるのかというと、

    public function getConnectionBlogs($blog, $limit)
    {
        $connectionBlogs = [];
        $params['blog_category_id'] = $blog->blog_category_id;
        $params['limit'] = $limit;
        $params['exclusionBlogId'] = $blog->id;
        $connectionBlogs = $this->blogRepository->getSearchBlogs($params);

        if ($connectionBlogs->count() <= $limit) {
            $remainingparams['limit'] = ($limit - $connectionBlogs->count());
            $remainingBlogs = $this->blogRepository->getSearchBlogs($remainingparams);
            $connectionBlogs = $connectionBlogs->merge($remainingBlogs);
        }

        return $connectionBlogs;
    }

このようにreposigtoryを呼んでいます。
確かに$paramに検索用の情報を入れてますね。

ではcontrollerでどのように呼ぶのかを解説します。

    public function show($id)
    {
        $blog = $this->blogService->getBlogById($id);
        $limit = self::SHOW_CONNECTION_BLOG_COUNT;
        $connectionBlogs = $this->blogService->getConnectionBlogs($blog, $limit);

        return view('user.blog.show', compact('blog', 'connectionBlogs'));
    }

超わかりやすくないですか。
上で

const SHOW_CONNECTION_BLOG_COUNT = 4;

を書いています。

4件自分自身のIDのブログは持ってこないようにして、
もし4件なかったら新着を持ってきてねというコードになっています。

そして最後にview部分を見せて解説します

            <section class="relation_column">
              <div class="common_title_block">
                <h2 class="common_title">
                  関連記事
                </h2>
                <p class="common_title_en">
                  RELATION COLUMN
                </p>
              </div>
              <div class="column_box_wrap">
                @foreach ($connectionBlogs as $connectionBlog)
                <div class="column_box">
                  <div class="new">
                    <img src="/static/images/common/icon_new.svg" alt="NEW">
                  </div>
                  <a href="column_detail.html" class="column_box_img box_img" style="background-image: url(/static/images/sample/sample01.jpg); height: 202px;">
                  </a>
                  <div class="column_box_category_date">
                    <a href="column_list.html" class="column_box_category">
                      <p>{{ $connectionBlog->blogCategory->name }}</p>
                    </a>
                    <p class="column_box_date">
                      {{ date('Y-m-d', strtotime($connectionBlog->created_at)) }}
                    </p>
                  </div>
                  <a href="column_detail.html" class="column_box_title_link">
                    <h3 class="column_box_title">
                      {{ $connectionBlog->title }}
                    </h3>
                  </a>
                  <ul class="column_box_tag_list">
                    @foreach ($connectionBlog->tags as $tag)
                    <li class="column_box_tag_item">
                      <a href="column_list.html" class="column_box_tag_link">
                        {{ $tag->name }}
                      </a>
                    </li>
                    @endforeach
                  </ul>
                </div>
                @endforeach
              </div>
              <div class="common_more_btn_wrap">
                <a href="column_list.html" class="common_more_btn">
                  関連記事をもっと見る
                </a>
              </div>
            </section>

このようになります。
その結果

とれました。

以上でlaravel10の環境構築から基本的なブログ作成の解説ができたことになります。