12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

作って学ぶLaravel入門講座 by WEBOTV / WEBプログラム学習チャンネル チャンネル で学んだ

Last updated at Posted at 2022-02-02

前に作成した記事を誤って別の記事で上書きしてしまった。
前の記事は余計な事をせずに動画を忠実に模写したので、反響が本当に凄かった。
今回も、余計なことをせず、動画を模写するよう心掛けようと思った。
前回、lineについてのloginではメールアドレスの取得ができなかったので、今回はそれも併せて掲載してます。

下の拡張機能をいれていると、use時ファイルを検索してくれます。

第1回 インストール&初期設定編

laravelをインストール

#最新versionがインストール
laravel new Bookmark
laravel8のインストール#versionを指定してインストール
composer create-project "laravel/laravel=8.*" Bookmark

初期設定編

config\app.phpの変種
config\app.php
    /*
    |--------------------------------------------------------------------------
    | Application Timezone
    |--------------------------------------------------------------------------
    |ここで、アプリケーションのデフォルトのタイムゾーンを指定することができます。
    |は、PHP の日付関数と日付時刻関数で使用されます
    */
    'timezone' => 'Asia/Tokyo',
    // 'timezone' => 'UTC',
    /*
    |--------------------------------------------------------------------------
    | Application Locale Configuration
    |--------------------------------------------------------------------------
    | アプリケーションロケールとは、アプリケーションで使用されるデフォルトのロケールを決定する
    | 翻訳サービスプロバイダによって。この値は自由に設定することができます
    | アプリケーションでサポートされる任意のロケールに設定します。
    */
    'locale' => 'ja',
    // 'locale' => 'en',
    /*
    |--------------------------------------------------------------------------
    | Faker Locale
    |--------------------------------------------------------------------------
    | このロケールは、Faker PHP ライブラリがフェイクを生成する際に使用されます。
    | データベースのシードのためのデータです。例えば、これは
    | ローカライズされた電話番号、住所情報など。
    */

    'faker_locale' => 'ja_JP',
    // 'faker_locale' => 'en_US',
envファイルの編集
env
#アプリケーション名をBookmarkAppに変更
APP_NAME=BookmarkApp

#データベースをmySqlからsqliteに変更
DB_CONNECTION=sqlite
# DB_CONNECTION=mysql
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
sqliteを作成
win
type nul > database/database.sqlite
mac
touch database/database.sqlite

メモ:sqliteのファイル名とパスを変更する場合

config\database.php
    'connections' => [

        'sqlite' => [
            'driver' => 'sqlite',
            'url' => env('DATABASE_URL'),
//パスと名前を変更できる。
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
        ],
//省略
]
ローカルサーバーを起動
php artisan serve

切断は ctr+c

port番号を指定する場合
php artisan serve --port=8080

第2回 アプリケーションのデモ

login認証が必要なアプリ+laravel socialiteを用いてgithubアカウントでのログイン認証が可能
更に追加でgoogle,yahooJp,line(メール取得)を追記してます。
image.png

  1. bookmarkの登録/編集/削除
  2. tag付け機能があります。
  3. tagの登録/編集/削除 機能もあるアプリです。
  4. ページネーション機能

image.png

第3回 Router編

routes\web.phpで設定する

routes\web.php
//http://127.0.0.1:8000/test
Route::get('/test',function(){
    return 'Hello Laravel';
});

//変数を使用する
//http://127.0.0.1:8000/test/hello_laravel
Route::get('/test/{buff}',function($buff){
    return $buff;
});
//正規表現を使用する
Route::get('/test/{id}',function($id){
    return $id;
})->where('id','[0-9]+');

//コントローラーを指定する
//ブックマークコントローラーのindex関数,ルート名はbookmarks.index
Route::get('/bookmarks', [App\Http\Controllers\BookmarkController::class, 'index'])->name('bookmarks.index');

//エイリアスを使用した場合
use App\Http\Controllers\BookmarkController;
Route::get('/', [BookmarkController::class,'index'])->name('bookmarks.index');

第4回 データベース編

laravelではマイグレーションファイルやモデル、コントローラーファイルなどのファイル作成は
php artisan mkaeコマンドで作成していきます。
どのようなファイルが作成可能かは下記のコマンドで確認が可能です。

php artisan make

今回は、マイグレーションファイルだけではなく、モデルやコントローラーも
一括で作成しますので、下記のコマンドを実行します。

モデルとその他ファイルを一括で作成する
php artisan make:model Bookmark -a

その他:オプション

option 概要
-m モデルとマイグレーションファイルを作成する
-f モデルとファクトリーファイルを作成する
-r モデルとindex()やshow()等のリソースのついたコントローラーが作成される
-a -m-f-r全部
bookmarksテーブルを作成する
database\migrations\xxx_create_bookmarks_table.php
    public function up()
    {
        Schema::create('bookmarks', function (Blueprint $table) {
            $table->id();
  //+
            $table->string('title');
            $table->string('url');
            $table->string('description')->nullable();
            
            $table->timestamps();
        });
    }

テーブルを作成する

php artisan migrate

その他、laravel付属のusersテーブルとかも作成される。
image.png

ダミーデータを作成する

factoryファイルは
先ほど、php artisan make:model Bookmark -a-aで一緒に作成してくれている。

database\factories\BookmarkFactory.php

    public function definition()
    {
        return [
//UserFactoryファイルをコピペして編集する。
            'title' => $this->faker->realText($this->faker->numberBetween(10,25)),
            'url' => $this->faker->url(),
            'description' => $this->faker->realText($this->faker->numberBetween(50,200)),
        ];
    }
database\seeders\DatabaseSeeder.php
    public function run()
    {
        // \App\Models\User::factory(10)->create();
//上を書き換える
        \App\Models\Bookmark::factory(100)->create();
    }

ダミーデータを作成する

php artisan db:seed

第5回 データベースのデータを表示しよう

その前に、認証システムのファイルを作成する。

login画面やregister画面,home画面などのbladeファイルが作成されるため、
そのコードをコピペして使用します。
今回はbootstrapのuiで作成します。他にvueやreact等があります。

また、認証必須のアプリですので、後に使用します。

3.3以上はjqueryがデフォルトで削除されている。
composer require laravel/ui:^3.3
php artisan ui bootstrap --auth
npm install && npm run dev
npm run dev

login画面やregister画面、認証後のhome画面など認証に関する機能が全部作成される。
bookmarkの画面作成にはそのコードを利用して作成します。

データをテーブルから取得する

1.ルートを作成する

routes\web.php
Route::get('/bookmarks', [App\Http\Controllers\BookmarkController::class, 'index']);

2.コントローラーで取得する

  1. dd()を使用して変数の中身を確認できます。
app\Http\Controllers\BookmarkController.php
    public function index()
    {
        $bookmarks = Bookmark::all();
        dd($bookmarks);
    }

bookmark.index(xammpを使用してます。)
にアクセスすると下の画面のように
変数の中身が確認できるようになります。
image.png

viewsのbookmarks.index.blade.phpにデータを渡して表示する

bookmarks.index.blade.phpの作成

  • 1.viewsディレクトリの下にbookmarksディレクトリを作成
    • resources\views\bookmarks
  • 2.その下にindex.blade.phpファイルを作成する。
    • resources\views\bookmarks\index.blade.php

home画面をコピーして編集する

resources\views\bookmarks\index.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">ブックマーク一覧</div>
            <div class="card-body">
{{-- + --}}
                <table class="table table-light">
                    <thead class="thead-light">
                        <tr>
                            <th>id</th>
                            <th>タイトル</th>
                        </tr>
                    </thead>
                    <tbody>

                        @foreach ($bookmarks as $bookmark)
                            <tr>
                                <td>{{ $bookmark->id }}</td>
                                <td>{{ $bookmark->title }}</td>
                            </tr>
                        @endforeach

                    </tbody>
                </table>

            </div>
        </div>
    </div>
</div>
</div>
@endsection

作成したbladeにデータを渡して表示させる。views()を使用する。

app\Http\Controllers\BookmarkController.php
    public function index()
    {
        
        $bookmarks = Bookmark::all();
        //dd($bookmarks);
//+ディレクトリの区切りは'.'を使用し、`blade.php`は省略する。
        return view('bookmarks.index', compact('bookmarks'));
    }

bookmark.index
image.png

ページネーションを設定する

app\Http\Controllers\BookmarkController.php
    public function index()
    {
        // $bookmarks = Bookmark::all();

//+
        $bookmarks = Bookmark::paginate(10);
        return view('bookmarks.index', compact('bookmarks'));
    }

laravel8のデフォルトのページネーションはTailwindを使用しているため、
bootstrap4を使用するよう明示する必要があります。そのための設定です。

app\Providers\AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;


// +
use Illuminate\Pagination\Paginator;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
    }

    public function boot()
    {
// +
        Paginator::useBootstrap();
    }
}
resources\views\bookmarks\index.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">ブックマーク一覧</div>
            <div class="card-body">
                <table class="table table-light">
               {{-- 省略 --}}
                </table>

{{-- +テーブル要素の直下に追加 --}}
                    {{ $bookmarks->links() }}

            </div>
        </div>
    </div>
</div>
</div>
@endsection

bookmark.index
ページネーションが表示された。
image.png

第6回 詳細ページの作成

resources\views\bookmarks\show.blade.phpを作成する

resources\views\bookmarks\show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">ブックマーク詳細</div>
                <div class="card-body">
                    <table class="table table-bordered">
                        <tbody>
                            <tr>
                                <th class="text-nowrap">タイトル</th>
                                <td>{{ $bookmark->title }}</td>
                            </tr>
                            <tr>
                                <th>URL</th>
                                <td>{{ $bookmark->url }}</td>
                            </tr>
                            <tr>
                                <th>概要</th>
                                <td>{!! nl2br(e($bookmark->description)) !!}</td>
                            </tr>
                            <tr>
                                <th>作成日</th>
                                <td>{{ $bookmark->created_at->format('Y年m月d日 h:i:s') }}</td>
                            </tr>
                        </tbody>
                    </table>
                <a class="btn btn-primary" href="{{ route('bookmarks.index') }}">一覧にもどる</a>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
app\Http\Controllers\BookmarkController.php
//Bookmark $bookmark は一旦コメントアウト
    // public function show(Bookmark $bookmark)
    public function show($id)
    {
//findOrFail()見つからない場合
//404 HTTPレスポンスをクライアントへ自動的に返す
        $bookmark = Bookmark::findOrFail($id);
        return view('bookmarks.show',compact('bookmark'));
    }

ルートを作成する

routes\web.php
Route::get('/bookmarks/{id}', [App\Http\Controllers\BookmarkController::class, 'show'])->where('id','[0-9]+');

リンクを作成する

resources\views\bookmarks\index.blade.php
{{-- +タイトルにaタグをwrapする --}}
                <td>
{{-- + --}}
                    <a href="./bookmarks/{{ $bookmark->id }}">{{ $bookmark->title }}</a>
                </td>

bookmarks.indexとbookmarks.show画面
image.png

ルートに名前を付ける

コントローラーの関数を直接指定する場合--action()関数

resources\views\bookmarks\index.blade.php
<a href="{{ action([App\Http\Controllers\BookmarkController::class,'show'],$bookmark->id) }}">{{ $bookmark->title }}</a>

ルートにネームを付けた場合---route()関数

routes\web.php
use App\Http\Controllers\BookmarkController;
Route::get('/bookmarks/{id}', [BookmarkController::class, 'show'])->where('id','[0-9]+')->name('bookmarks.show');

route()を使用して、urlを作成することができる。

resources\views\bookmarks\index.blade.php
<a href="{{ route('bookmarks.show',$bookmark->id) }}">{{ $bookmark->title }}</a>

また、先ほど、$idをgetリクエストしてbookmarksテーブルを検索して取得しましたが、
本来は、そのまま、$bookmarkを流して表示させます。

app\Http\Controllers\BookmarkController.php
//Bookmarkをnewすることなく$bookmark変数が使用できます。
    public function show(Bookmark $bookmark)
    {
//getリクエストで送られてきた$bookmarkをそのまま渡す。
        return view('bookmarks.show', compact('bookmark'));
    }
resources\views\bookmarks\index.blade.php
{{-- <a href="{{ route('bookmarks.show',$bookmark->id) }}">{{ $bookmark->title }}</a> --}}
{{-- 変更 --}}
<a href="{{ route('bookmarks.show',$bookmark) }}">{{ $bookmark->title }}</a>
routes\web.php
//ルートは上から取得されるため、必ずコメントアウトしてください。
//Route::get('/bookmarks/{id}', [BookmarkController::class, 'show'])->where('id','[0-9]+')->name('bookmarks.show');
Route::get('/bookmarks/{bookmark}', [BookmarkController::class, 'show'])->name('bookmarks.show');

結果は先ほどと同じです。リンクから詳細画面が表示されればOKです。
image.png

第7回 レコード追加機能の作成

bookmarkのルートをresourceに変更する

laravelではcrudに必要な関数を予め用意してくれています。

  1. index()で一覧表示
  2. show()で詳細表示
  3. create()で登録画面の表示
  4. store()でテーブルにデータを登録
  5. edit()で編集画面
  6. update()でテーブルのデータを編集
  7. destroy()でテーブルのデータを消去

上記の、ルートを一括で作成するのが
Route::resource()です。

routes\web.php
//+
use App\Http\Controllers\BookmarkController;

// Route::get('/bookmarks', [App\Http\Controllers\BookmarkController::class, 'index']);
// Route::get('/bookmarks/{bookmark}', [App\Http\Controllers\BookmarkController::class, 'show'])->name('bookmarks.show');
//+
Route::resource('bookmarks', BookmarkController::class);

resourceで作成されるルート

Method URI Name Middleware
POST bookmarks bookmarks.store web
GET bookmarks bookmarks.index web
GET bookmarks/create bookmarks.create web
DELETE bookmarks/{bookmark} bookmarks.destroy web
PUT bookmarks/{bookmark} bookmarks.update web
GET bookmarks/{bookmark} bookmarks.show web
GET bookmarks/{bookmark}/edit bookmarks.edit web

新規登録フォームを作成する

create.blade.phpを作成する
//login画面をコピーして編集する

resources\views\bookmarks\create.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマークを登録する</div>

                    <div class="card-body">
                        <form method="POST" action="{{ route('bookmarks.store') }}" novalidate>
//GET送信以外はトークンが必要です。@csrfでトークンを生成して送信してくれます。
                            @csrf
                            <div class="row mb-3">
                                <label for="title" class="col-md-2 col-form-label text-md-end">タイトル</label>
                                <div class="col-md-10">
                                    <input id="title" type="text" class="form-control @error('title') is-invalid @enderror"
                                        name="title" value="{{ old('title', $bookmark->title ?? '') }}" required
                                        autocomplete="title" autofocus>

                                    @error('title')
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $message }}</strong>
                                        </span>
                                    @enderror
                                </div>
                            </div>

                            <div class="row mb-3">
                                <label for="url" class="col-md-2 col-form-label text-md-end">URL</label>

                                <div class="col-md-10">
                                    <input id="url" type="url" class="form-control @error('url') is-invalid @enderror"
                                        name="url" value="{{ old('url', $bookmark->url ?? '') }}" required
                                        autocomplete="current-url">

                                    @error('url')
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $message }}</strong>
                                        </span>
                                    @enderror
                                </div>
                            </div>

                            <div class="row mb-3">
                                <label for="description" class="col-md-2 col-form-label text-md-end">概要</label>

                                <div class="col-md-10">
                                    <textarea name="description" id="description" cols="30" rows="10"
                                        class="form-control @error('description') is-invalid @enderror" required
                                        autocomplete="description"
                                        autofocus>{{ old('description', $bookmark->description ?? '') }}</textarea>

                                    @error('description')
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $message }}</strong>
                                        </span>
                                    @enderror
                                </div>
                            </div>
                    </div>
                </div>

                <div class="row mb-0">
                    <div class="col-md-10 offset-md-2 d-flex justify-content-end">

                        <a class="btn btn-secondary me-3" href="{{ route('bookmarks.index') }}">
                            一覧に戻る
                        </a>
                        <button type="submit" class="btn btn-primary">
                            投稿する
                        </button>
                    </div>
                </div>
                </form>
            </div>
        </div>
    </div>
    </div>
    </div>
@endsection

create()を作成する

app\Http\Controllers\BookmarkController.php
    public function create()
    {
                return view('bookmarks.create');
    }

新規登録ボタンを作成する

resources\views\bookmarks\index.blade.php
{{-- .card-bodyの直下 --}}
            <div class="card-body">
                <a href="{{ route('bookmarks.create') }}" class="btn btn-primary mb-3">新規登録</a>

bookmarks.index画面とbookmarks.create画面
image.png

store()を作成してテーブルにデータを登録

最新のlaravelから自動でフォームリクエスト機能がかかるようになりました。
public function store(StoreBookmarkRequest $request)
そのため、フォームリクエスト機能は後に解説されていますので、いったんここではコメントアウトしてください。

app\Http\Controllers\BookmarkController.php
//+
    use Illuminate\Http\Request;

//一旦コメントアウト
    //public function store(StoreBookmarkRequest $request)
    public function store(Request $request)
    {
//+
//create()を使用するとホワイトリストに登録したデータだけ登録してくれます。滅茶苦茶安全です。
//戻り値もクリエイトしたデータを戻してくれます。$bookmark->idで登録したデータのidを取得できる。
        $bookmark = Bookmark::create($request->all());
//redirect()->route()でルートにアクセスできます。
//views()ではありません。views()だとそのまま、index.blade.phpにアクセスされます。
        return redirect()->route('bookmarks.index');
    }
redirect()についての注意事項
[ララジャパン-abort()を使ってリダイレクト](https://www.larajapan.com/2020/07/25/abort%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88/)
  • 要約
    • rediret()はreturnと一緒にしか使えない。
    • returnが使えないときはabort(redirect()->route(''));を使用する。

ホワイトリストを登録する

app\Models\Bookmark.php
//Userモデルからコピペして編集
    protected $fillable = [
        'title',
        'url',
        'description',
    ];

降順表示に変更する

app\Http\Controllers\BookmarkController.php
    public function index()
    {
        // $bookmarks = Bookmark::all();
        // $bookmarks = Bookmark::paginate(10);
        $bookmarks = Bookmark::orderBy('id','desc')->paginate(20);
        return view('bookmarks.index', compact('bookmarks'));
    }

bookmarks.create画面とbookmarks.index画面
登録したデータが表示されていれば、OKです。
image.png

第8回 レコード更新機能の作成

編集画面を作成する

formタグをcreate.blade.phpと共通化するため
コンポーネントを使用する

  1. viewsディレクトリの下にcomponentsディレクトリを作成
  2. componentsディレクトリの下にbookmarksディレクトリを作成
  3. bookmarksディレクトリの下にform.blade.phpを作成

image.png

bookmarks.create.blade.phpのformタグをカットしてに変更

resources\views\bookmarks\create.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマークを登録する</div>

                    <div class="card-body">

 {{-- +コンポーネントファイルは <x-ファイル名/> 値をコンポーネントに渡すこともできる--}}
                        <x-bookmarks.form route="store" />
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

formタグをresources\views\components\bookmarks\form.blade.phpに貼り付ける

resources\views\components\bookmarks\form.blade.php
{{-- $routeで値を受け取る でも普通はRoute::is() --}}
@if ($route == 'store')
<form method="POST" action='{{ route("bookmarks.$route") }}' novalidate>
@else
<form method="POST" action='{{ route("bookmarks.$route",$bookmark) }}' novalidate>
@endif
    @csrf
    <div class="row mb-3">
        <label for="title" class="col-md-2 col-form-label text-md-end">タイトル</label>
        <div class="col-md-10">
            <input id="title" type="text" class="form-control @error('title') is-invalid @enderror"
                name="title" value="{{ old('title', $bookmark->title ?? '') }}" required
                autocomplete="title" autofocus>

            @error('title')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-3">
        <label for="url" class="col-md-2 col-form-label text-md-end">URL</label>

        <div class="col-md-10">
            <input id="url" type="url" class="form-control @error('url') is-invalid @enderror"
                name="url" value="{{ old('url', $bookmark->url ?? '') }}" required
                autocomplete="current-url">

            @error('url')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

    <div class="row mb-3">
        <label for="description" class="col-md-2 col-form-label text-md-end">概要</label>

        <div class="col-md-10">
            <textarea name="description" id="description" cols="30" rows="10"
                class="form-control @error('description') is-invalid @enderror" required
                autocomplete="description"
                autofocus>{{ old('description', $bookmark->description ?? '') }}</textarea>

            @error('description')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>

<div class="row mb-0">
<div class="col-md-10 offset-md-2 d-flex justify-content-end">

<a class="btn btn-secondary me-3" href="{{ route('bookmarks.index') }}">
    一覧に戻る
</a>
<button type="submit" class="btn btn-primary">
    @if (Route::is('bookmarks.create'))
        投稿する
    @else
        編集する
    @endif
</button>
</div>
</div>
</form>

変わらずに表示されればOK
image.png

edit.blade.phpを作成する

resources\views\bookmarks\edit.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマークを編集する</div>

                    <div class="card-body">

 {{-- +コンポーネントに変数を渡すときは注意 : がいる--}}
                        <x-bookmarks.form route="update" :bookmark="$bookmark"/>
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
resources\views\components\bookmarks\form.blade.php
     @if ($route == 'store')
         <form method="POST" action='{{ route("bookmarks.$route") }}' novalidate>
     @else
         <form method="POST" action='{{ route("bookmarks.$route",$bookmark) }}' novalidate>
    @endif
         @csrf

{{-- +@csrfの下に追記 update()はPUT送信のため 追記してください  --}}
    @if(Route::is('bookmarks.edit')) @method('PUT') @endif

テーブルにアクション欄を作成して詳細と編集btnを作成する

resources\views\bookmarks\index.blade.php
                <table class="table table-light">
                    <thead class="thead-light">
                        <tr>
                            <th>id</th>
                            <th>タイトル</th>
{{-- + --}}
                            <th>アクション</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($bookmarks as $bookmark)
                            <tr>
                                <td>{{ $bookmark->id }}</td>
                                <td>{{ $bookmark->title }}</td>
{{-- + @foreachの<tr>の中 --}}
                                <td class="text-nowrap">
                                    <a class="btn btn-info"
                                        href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                                    <a class="btn btn-secondary"
                                        href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
                                </td>


                            </tr>
                        @endforeach
                    </tbody>
                </table>
app\Http\Controllers\BookmarkController.php
    public function edit(Bookmark $bookmark)
    {
       return view('bookmarks.edit',compact('bookmark'));
    }

bookmarks.indexとbookmarks.edit
image.png

update()を作成する

app\Http\Controllers\BookmarkController.php
//いったんコメントアウト
    // public function update(UpdateBookmarkRequest $request, Bookmark $bookmark)
    public function update(Request $request, Bookmark $bookmark)
    {
        $bookmark->update($request->all());
        return redirect()->route('bookmarks.show',$bookmark);
    }

編集内容が反映されていればOk.
image.png

第9回 レコードの削除とフラッシュメッセージ

destroy()を作成する

app\Http\Controllers\BookmarkController.php
    public function destroy(Bookmark $bookmark)
    {
        $bookmark->delete();
        return redirect()->route('bookmarks.index');
    }

アクションに削除ボタンを追加
resourceのdestroy()はform method="delete" とcsrfトークンと一緒に送信する必要がある

resources\views\bookmarks\index.blade.php
{{-- アクションボタンに追加 --}}
            <td class="text-nowrap">
                <a class="btn btn-info"
                    href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                <a class="btn btn-secondary"
                    href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
{{-- + --}}
                <a class="btn btn-danger" href="#"
                onclick="event.preventDefault();
                                document.getElementById(`destroy_form.{{ $bookmark->id }}`).submit();">
                    削除
                </a>
                <form id="destroy_form.{{ $bookmark->id }}" action="{{ route('bookmarks.destroy',$bookmark) }}" method="POST" class="d-none">
                    @csrf
                    @method('delete')
                </form>
                
            </td>

dataが削除されればOKです。
image.png

削除btnのコンポーネント化する
併せて、scriptを使用するためresources\views\layouts\app.blade.phpに@stack('js')を追記

resources\views\layouts\app.blade.php

        <main class="py-4">
            @yield('content')
        </main>
    </div>

{{-- </body>の上に追加 --}}
    @stack('js')
</body>
  1. 先ほど作成したcomponentsディレクトリの下に
  2. その下にdel_btn.blade.phpを作成する
resources\views\components\del_btn.blade.php
<a class="btn btn-danger" href="javascript:void(0)" onclick='del_bookmark(event);' id="{{ $id }}">
    削除
</a>

{{-- 1回だけレンダリングしてくれる --}}
@once
    @push('js')
        <form name="destroy_form" action="" method="POST" class="d-none">
            @method('delete')
            @csrf
        </form>

        <script>
{{-- 値は$routeIsで取得 $route_isなどのキャメルはNG --}}
            destroy_url = `{{ route("$routeIs.destroy", '') }}`;

            function del_bookmark(e) {
                e.preventDefault();
                del_form = document.destroy_form;
                del_form.setAttribute("action", `${destroy_url}/${e.target.id}`);
                document.destroy_form.submit()
            }
        </script>
    @endpush
@endonce
resources\views\bookmarks\index.blade.php
            <td class="text-nowrap">
                <a class="btn btn-info"
                    href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                <a class="btn btn-secondary"
                    href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
{{-- 変更 :bookmark_id など キャメルはNG --}}
               <x-del_btn routeIs="bookmarks" :id="$bookmark->id" />
            </td>

先ほどと変わらす、削除されればOKです。
image.png

flashメッセージを作成する

app\Http\Controllers\BookmarkController.php
    public function store(Request $request)
    {
        $bookmark=Bookmark::create($request->all());
//->with('status','登録しました')
        return redirect()->route('bookmarks.index')->with('status','登録しました');
    }

    public function update(Request $request, Bookmark $bookmark)
    {
        $bookmark->update($request->all());
//->with('status','更新しました')
        return redirect()->route('bookmarks.show',$bookmark)->with('status','更新しました');
    }
    public function destroy(Bookmark $bookmark)
    {
        $bookmark->delete();
//->with('status','削除しました')
        return redirect()->route('bookmarks.index')->with('status','削除しました');
    }
  1. viewsの下にコンポーネントディレクトリを作成
  2. その下にalert.blade.php
resources\views\components\alert.blade.php
@if (session()->has('status'))
<div class="alert alert-success" role="alert">
    {{ session('status') }}
</div>
@endif
resources\views\bookmarks\index.blade.php
        <div class="card-header">ブックマーク一覧</div>
        <div class="card-body">
{{-- +.card-bodの下 --}}
            <x-alert></x-alert>
resources\views\bookmarks\show.blade.php
            <div class="card">
                <div class="card-header">ブックマーク詳細</div>
                <div class="card-body">
{{-- +.card-bodの下 --}}
                    <x-alert></x-alert>

第10回 入力フォームのチェック機能(バリデーション)

フォームリクエストクラス
最新のlaravel8ではresource作成時、すでにリクエストクラス等も作成される。
また、FormRequestはコントローラーメソッドに勝手にDIされている。

artisanで作成する場合
php artisan make:request StoreBookmark

BookmarkControllerコントローラー

app\Http\Controllers\BookmarkController.php
//コメントアウトを解除して変更してください。
//store(StoreBookmarkRequest $request) 作成したクラスはDIして使用する。
    public function store(StoreBookmarkRequest $request)
    {
        $bookmark=Bookmark::create($request->all());
        return redirect()->route('bookmarks.index')->with('status','登録しました');
    }

//StoreBookmarkRequestクラスに変更してください。
    public function update(StoreBookmarkRequest $request, Bookmark $bookmark)
    {
        $bookmark->update($request->all());
        // return redirect()->route('bookmarks.show',$bookmark);
        return redirect()->route('bookmarks.show',$bookmark)->with('status','更新しました');
    }

再度、登録してみてください。そうすると図のようにエラー表記(403 THIS ACTION IS UNAUTHORIZED)になります。
これは、登録する権限がfalseになっているためです。
image.png

StoreBookmarkRequestリクエストクラスにルールを記述

app\Http\Requests\StoreBookmarkRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;

class StoreBookmarkRequest extends FormRequest
{
    //*
    // ユーザーがこの要求を行う権限があるかどうかを判断します。
    //*
    public function authorize()
    {
        //return false
//accessする権限があるかどうかをチェックできる。trueは必須
//例えば、編集権限があるかどうか、1日の投稿件数を超えていた場合、falseにするとか
//ここをtrueにすることで403 THIS ACTION IS UNAUTHORIZEDを解除できる
        return true;
    }
    //*
    // リクエストに適用されるバリデーションルールを取得します。
    //*
    public function rules()
    {
//バリデーションルールを
        return [
            'title' => 'required',
            'url'   => 'required|url',
            'description' => 'max:500',
        ];
    }
}

入力がルールに抵触すると英語のエラーメッセージが返る
image.png
そうでないときは先ほどはエラーメッセージがでたが、ちきんと登録される。
image.png

その他解説
viewsでのバリデーションのエラーの判定は@error('name名') @enderroでできる。

        <div class="col-md-10">
// @error('title') is-invalid @enderror titleがエラーなら is-invalid を表示
            <input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ old('title',$bookmark->title??'') }}" required autocomplete="title" autofocus>

            @error('title')
                <span class="invalid-feedback" role="alert">
//$messageで最初のエラーメッセージを取得できる。@error('title')で囲まれているためtitleのエラーメッセージが取得される
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>

//複数のメッセージを取得する場合
@if ($errors->has('title'))
  @foreach($errors->get('title') as $message)
  <strong> {{ $message }} </strong>
  @endforeach
@endif

入力した値を取得する
old()で取得 第一引数に項目名、第二引数にデフォルトの値(edit.blade.phpの時とか)

        <div class="col-md-10">
            <input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title"

//第一引数に項目名、第二引数にデフォルトの値($bookmark->title ?? '')
                value="{{ old('title', $bookmark->title ?? '') }}" required autocomplete="title" autofocus>

その他解説 終了

エラーメッセージを日本語に変更
StoreBookmarkRequestクラスにmessages()関数を追加

app\Http\Requests\StoreBookmarkRequest.php
//方法1
    public function messages()
{
    return [
        'title.required' => 'タイトルは必須やねん',
        'url.required' => 'urlがぬけているねん',
        'description.max:500' => '概要は500文字以内に書いて欲しいねん',
    ];
}
//方法2
    public   function   messages()
    {
        return   [
            "required"    => ":attributeは必須項目です。",
            "email"       => ":attributeはメールアドレスの形式で入力してください。",
            "max:500"     => ":attributeは500文字以内で入力してください。",
        ];
    }
    public function attributes()
    {
        return[
            'title'=> 'タイトル',
            'description'   => '概要',
        ];
    }

レジスター画面やログイン画面のメッセージも日本語に変更する場合
普通はこっちで一括して変更する。かち合った場合は上記が優先される。

php -r "copy('https://readouble.com/laravel/5.6/ja/install-ja-lang-files.php', 'install-ja-lang.php');"
php -f install-ja-lang.php 
php -r "unlink('install-ja-lang.php');"

ja.jsonファイルを作成

type nul > resources/lang/ja.json

作成された空のファイルに下のコードをコピペ

resources\lang\ja.json
{
    "Register": "ユーザー登録",
    "Name": "氏名",
    "E-Mail Address": "メールアドレス",
    "Password": "パスワード",
    "Confirm Password": "パスワード (確認用)",
    "Login": "ログイン",
    "Logout": "ログアウト",
    "Remember Me": "ログインしたままにする",
    "Forgot Your Password?": "パスワードを忘れた方はこちら",
    "Reset Password": "パスワード再設定",
    "Send Password Reset Link": "パスワード再設定用のリンクを送る",
    "Verify Your Email Address": "メールアドレス認証を行ってください",
    "A fresh verification link has been sent to your email address.": "ユーザー登録の確認メールを送信しました。",
    "Before proceeding, please check your email for a verification link.": "メールに記載されているリンクをクリックして、登録手続きを完了してください。",
    "If you did not receive the email,": "メールが届かない場合、",
    "click here to request another.": "こちらをクリックして再送信してください。",
    "Please confirm your password before continuing.": "続行するにはパスワードを入力してください。"
}
resources\lang\ja\passwords.php
-------------------省略----------------
    'user' => "メールアドレスに一致するユーザーは存在していません。",
//+
    'throttled' => 'しばらく待ってから再度お試しください。',
resources\lang\ja\validation.php
    'attributes' => [
//+
        "name" => "名前",
        "password" => "パスワード",
        "password_confirmation" => "パスワード(確認用)",
        "email" => "メールアドレス",

//+
        'title'=>'タイトル',
        'description' => '概要',
    ],

];

エラーメッセージが日本語化されていればOKです。
image.png

第11回 ユーザー登録・認証機能の作成

もうすでに認証関係は導入済み。

#第4回データベース編でusersテーブルも一緒に作成
#usersテーブルはdefaultで用意されているため。削除しなければ、一緒に作成される。
php artisan migrate
第5回の時に--authで認証機能を導入
#--authでコントローラーもveiwsファイルも全部作成
php artisan ui bootstrap --auth

login後のリダイレクト先を変更する

app\Providers\RouteServiceProvider.php
//homeから'/'に変更
    // public const HOME = '/home';
    public const HOME = '/';

bookmarks.indexのルートを/に変更する

routes\web.php
//
Route::get('/', [BookmarkController::class, 'index']);
Route::resource('bookmarks', BookmarkController::class);

レジスター登録を確認する
登録して、登録後、bookmarks.index画面に遷移されればOKです。
併せて、バリデーションが日本語になっているかも確認してください。
image.png

bookmarkに認証機能を追加

認証されたユーザーだけアクセスできるように変更

routes\web.php
//->middleware('auth')
//    Route::get('/', [BookmarkController::class, 'index'])->name('bookmarks.index')->middleware('auth');

Route::group(['middleware' => 'auth'], function () {
    Route::get('/', [BookmarkController::class, 'index'])->name('bookmarks.index');
    Route::resource('bookmarks', BookmarkController::class);
});

一旦ログアウトして、再度bookmarks.index画面にアクセスしてみてください。ロゴのリンクが
bookmarks.indexのリンクに変更されています。
login画面にリダイレクトされればOKです。
image.png

第12回 タグCRUD(作成・読み込み・更新・削除)機能の作成

tagsテーブルを作成する

モデル等も一括して作成する

php artisan make:model Tag -a

tagsテーブルを編集する

database\migrations\xxx_create_tags_table.php
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            //+
            $table->string('title');
            
            $table->timestamps();
        });
    }
php artisan migrate

ホワイトリストを登録する

app\Models\Tag.php
    protected $fillable = [
        'title',
    ];

ダミーデータを作成する

database\factories\TagFactory.php
    public function definition()
    {
        static $id=1;
        return [
            'title' => 'タグ'.$id++,
        ];
    }
database\seeders\DatabaseSeeder.php
        // \App\Models\User::factory(10)->create();
//先ほどのBookmarkはコメントアウトする。
        // \App\Models\Bookmark::factory(100)->create();

        \App\Models\Tag::factory(10)->create();
php artisan db:seed

バリデーションを作成する

app\Http\Requests\StoreTagRequest.php

class StoreTagRequest extends FormRequest
{
    public function authorize()
    {
//+
        return true; //false
    }
    public function rules()
    {
        return [
//+
            'title' => 'required|max:300|unique:tags,title,,',
        ];
    }
}

TagControllerを編集する

->BookmarkControllerの関数群をコピペして変更する
->BookmarkをTagにbookmarkをtagに変更すればOK

    public function index()
    {
        $tags = Tag::paginate(10);
        return view('tags.index',compact('tags'));
    }

    public function create()
    {
        return view('tags.create');
    }

    public function store(StoreTagRequest $request)
    {
        Tag::create($request->all());
        return redirect()->route('tags.index')->with('status','登録しました');
    }

    public function show(Tag $tag)
    {
        return view('tags.show',compact('tag'));
    }

    public function edit(Tag $tag)
    {
        return view('tags.edit',compact('tag'));
    }

    public function update(StoreTagRequest $request, Tag $tag)
    {
        $tag->update($request->all());
        return redirect()->route('tags.index')->with('status','更新しました');
    }

    public function destroy(Tag $tag)
    {
        $tag->delete();
        return redirect()->route('tags.index')->with('status','削除しました');
    }

viewsの作成

  1. resources\views\bookmarksディレクトリをそのままコピして
  2. resources\views\tagsディレクトリに変更する。
  3. 以下Bookmark仕様からtag使用へ変更する
resources\views\tags\index.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">タグ一覧</div>
                    <div class="card-body">
                        <a href="{{ route('tags.create') }}" class="btn btn-primary mb-3">新規登録</a>
                        <x-alert></x-alert>

                        <table class="table table-light">
                            <thead class="thead-light">
                                <tr>
                                    <th>id</th>
                                    <th>タイトル</th>
                                    <th>アクション</th>
                                </tr>
                            </thead>
                            <tbody>

                                @foreach ($tags as $tag)
                                    <tr>
                                        <td>{{ $tag->id }}</td>
                                        <td><a
                                                href="{{ route('tags.show', $tag) }}">{{ $tag->title }}</a>
                                        </td>

                                        <td class="text-nowrap">
                                            <a class="btn btn-info"
                                                href="{{ route('tags.show', $tag) }}">詳細</a>
                                            <a class="btn btn-secondary"
                                                href="{{ route('tags.edit', $tag) }}">編集</a>
                                            <x-del_btn routeIs="tags" :id="$tag->id" />
                                        </td>
                                    </tr>
                                @endforeach
                            </tbody>
                        </table>
                        {{ $tags->links() }}
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
resources\views\tags\show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">タグ詳細</div>
                <div class="card-body">
                    <table class="table table-bordered">
                        <tbody>
                            <tr>
                                <th class="text-nowrap">タイトル</th>
                                <td>{{ $tag->title }}</td>
                            </tr>
                            <tr>
                                <th>作成日</th>
                                <td>{{ $tag->created_at->format('Y年m月d日 h:i:s') }}</td>
                            </tr>
                        </tbody>
                    </table>
                    <a class="btn btn-primary" href="{{ route('tags.index') }}">一覧に戻る</a>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
resources\views\tags\create.blade.php
@extends('layouts.app')

@section('content')
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマークを登録する</div>

                    <div class="card-body">

 {{-- +コンポーネントファイルは <x-ファイル名/> 値をコンポーネントに渡すこともできる--}}
                        <x-tags.form route="store" />

                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
resources\views\tags\edit.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマークを編集する</div>

                    <div class="card-body">

 {{-- +コンポーネントに変数を渡す--}}
                        <x-tags.form route="update" :tag="$tag"/>
                        
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

resources\views\components\bookmarks\form.blade.phpをフォルダごとコピーして
resources\views\components\tags\form.blade.php名前を変更して作成する。

resources\views\components\tags\form.blade.php
{{-- $routeで値を受け取る でも普通はRoute::is() --}}
@if ($route == 'store')
<form method="POST" action='{{ route("tags.$route") }}' novalidate>
@else
<form method="POST" action='{{ route("tags.$route",$tag) }}' novalidate>
@endif
    @csrf
{{-- +  --}}
    @if(Route::is('tags.edit')) @method('PUT') @endif
    
    <div class="row mb-3">
        <label for="title" class="col-md-2 col-form-label text-md-end">タイトル</label>
        <div class="col-md-10">
            <input id="title" type="text" class="form-control @error('title') is-invalid @enderror"
                name="title" value="{{ old('title', $tag->title ?? '') }}" required
                autocomplete="title" autofocus>

            @error('title')
                <span class="invalid-feedback" role="alert">
                    <strong>{{ $message }}</strong>
                </span>
            @enderror
        </div>
    </div>
</div>
</div>

<div class="row mb-0">
<div class="col-md-10 offset-md-2 d-flex justify-content-end">

<a class="btn btn-secondary me-3" href="{{ route('tags.index') }}">
    一覧に戻る
</a>
<button type="submit" class="btn btn-primary">
    @if (Route::is('tags.create'))
        投稿する
    @else
        編集する
    @endif
</button>
</div>
</div>
</form>

ナビにbookmarkとtagのリンクを作成する

nemeで判断
class="nav-link {{ request()->route()->named('bookmarks*')? 'active': '' }}"
bootstrapのナビを使用しているため .active をつけると、active表示してくれる。
urlで判断
他にRequest::is('bookmark/*')?'active':''

resources\views\layouts\app.blade.php
    <!-- Left Side Of Navbar -->
            <ul class="navbar-nav me-auto">
{{-- +レフトサイドに追記する --}}
                <li><a class="nav-link {{ request()->route()->named('bookmarks*')? 'active': '' }}"
                        href="{{ route('bookmarks.index') }}">ブックマーク</a></li>
                <li><a class="nav-link {{ request()->route()->named('tags*')? 'active': '' }}"
                        href="{{ route('tags.index') }}">タグ</a></li>

            </ul>

ルートを設定する

routes\web.php
Route::group(['middleware' => 'auth'], function () {
    Route::get('/', [BookmarkController::class, 'index'])->name('bookmarks.index')->middleware('auth');
    Route::resource('bookmarks', BookmarkController::class, ['except' => ['index']]);

//+
    Route::resource('tags', TagController::class);
});

tagのCRUDが確認できればOKです。
image.png

第13回 データベースのリレーション設定(ブックマークとタグの関連付け)

ブックマークとタグをリレーションさせる

  • ブックマークとタグは多対多の関係にあるため中間テーブルを作成する

  • 中間テーブルは直接CRUDしないのでモデルは必要ない

  • そのため、中間テーブルを操作するためのメソッドが用意されている。

  • laravel 中間テーブル 命名規則

    1. 2つのテーブルをアルファベット順に並べる
    2. 2つのテーブル名 (単数形) を_ (アンダーバー) で繋げる
php artisan make:migration create_bookmark_tag_table
    public function up()
    {
        Schema::create('bookmark_tag', function (Blueprint $table) {
            $table->id();
            //中間テーブルの時は定型文
            $table->foreignId('bookmark_id')
// ->references('id')->on('bookmarks')の意味,
            ->constrained()
//deleteされたら一緒にdelete
            ->cascadeOnDelete()
//updateされたら一緒にupdate・・idがupdateされたら一緒にupdateする。
            ->cascadeOnUpdate();

            $table->foreignId('tag_id')
            ->constrained()
            ->cascadeOnDelete()
            ->cascadeOnUpdate();
            
            $table->timestamps();
        });
    }
php artisan migrate

bookmarkとtagをリレーションさせる

app\Models\Bookmark.php
    public function tags()
    {
      return $this->belongsToMany(Tag::class);
    }
app\Models\Tag.php
    public function bookmarks()
    {
        return $this->belongsToMany(Bookmark::class);
    }

これだけで、リレーションは完了です。後は、取得して表記するだけです。
これから、下記の図のようなbookmarks.create画面にtagを登録させるためのformを追加します。
image.png

Bookmarkコントローラーを編集する

//+
use App\Models\Tag;

    public function create()
    {
//+
                //id(key)とタイトル(value)の1次元連想配列で取得する
                $tags = Tag::pluck('title','id')->toArray();

                return view('bookmarks.create',compact('tags'));
    }

    public function edit(Bookmark $bookmark)
    {
//+
        //タグテーブルのid(key)とタイトル(value)を1次元連想配列で取得する
        $tags = Tag::pluck('title','id')->toArray();
        return view('bookmarks.edit',compact('bookmark','tags'));
    }
resources\views\components\bookmarks\form.blade.php
<!-- buttonの上に追記 -->

<!-- ここから -->
<div class="row mb-3">
    <label for="tags" class="col-md-2 col-form-label text-md-end">tags</label>
    <div class="col-md-10 d-flex flex-wrap align-content-around p-2">
        @foreach ($tags as $key => $tag)
            <div class="form-check mr-3">
                <input class="form-check-input" type="checkbox" value="{{ $key }}" id="tag{{ $key }}"
                    name="tags[]" 
{{-- //$bookmark->tagsに$keyが含まれているかどうか判定してる --}} 
{{-- //$bookmark->tagsが3.5.7で$keyが3ならtrue判定してくる結果、3.5.7にcheckedがつく --}} 
       @if (isset($bookmark->tags) && $bookmark->tags->contains($key))
                checked
        @endif
        >
        <label class="form-check-label" for="tag{{ $key }}">
            {{ $tag }}
        </label>
    </div>
    @endforeach

    @error('tags')
        <span class="invalid-feedback" role="alert">
            <strong>{{ $message }}</strong>
        </span>
    @enderror
</div>
</div>
<!--ここまで -->


<div class="row mb-0">
    <div class="col-md-10 offset-md-2 d-flex justify-content-end">

        <a class="btn btn-secondary me-3" href="{{ route('bookmarks.index') }}">
            一覧に戻る
        </a>
        <button type="submit" class="btn btn-primary">
            @if (Route::is('bookmarks.create'))
                投稿する
            @else
                編集する
            @endif
        </button>
    </div>
</div>
</form>
resources\views\bookmarks\create.blade.php
 {{-- +コンポーネントに$tagsを渡す--}}
                        <x-bookmarks.form route="store" :tags="$tags"/>
resources\views\bookmarks\edit.blade.php
 {{--+コンポーネントに$tagsを渡す--}}
                        <x-bookmarks.form route="update" :bookmark="$bookmark" :tags="$tags"/>

bookmarks.createとbookmarks.editに表示されればOKです。
image.png

中間テーブルを操作して、bookmarkとtagをリレーションさせる。

メソッド insert update delete
attach × ×
detach × ×
updateExistingPivot × ×
sync
syncWithoutDetaching ×
toggle ×
app\Http\Controllers\BookmarkController.php
    public function store(StoreBookmarkRequest $request)
    {
        $bookmark=Bookmark::create($request->all());
//sync()は登録時更新時、中間テーブルにcreate update deleteしてくる
//1,2,3を登録して2,5,7に更新すると1,3をdeleteして5,7を新しく登録してくれる。
        $bookmark->tags()->sync($request->tags);
        return redirect()->route('bookmarks.index')->with('status','登録しました');
    }
    public function update(UpdateBookmarkRequest $request, Bookmark $bookmark)
    {
        $bookmark->update($request->all());

        $bookmark->tags()->sync($request->tags);

        return redirect()->route('bookmarks.show',$bookmark)->with('status','更新しました');
    }

    public function destroy(Bookmark $bookmark)
    {
        $bookmark->delete();
//sql時onDleteCascade設定しているため本来的に不要だが、
//そうでない場合detach()で中間テーブルから削除できる。
        $bookmark->tags()->detach();
        return redirect()->route('bookmarks.index')->with('status','削除しました');
    }

database.sqliteのデータを確認する。拡張機能を導入する

bookmark_tagテーブルにデータが登録されていればOKです。
image.png

第14回 リレーション(タグ)の表示とデバッグツールについて

デバックツールを導入する

composer require barryvdh/laravel-debugbar --dev
php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"

下にデバッグバーが表示される。
image.png

本番環境でデバッグバーが見えてしまうとDBの中身も見えたりしてまずいので.envを設定します。APP_DEBUGの設定をfalseにすればデバッグバーが出ない設定になります。

env
APP_DEBUG=false

bookmark.index画面にタグも一緒に表示

resources\views\bookmarks\index.blade.php
    <table class="table table-light">
        <thead class="thead-light">
            <tr>
                <th>id</th>
                <th>タイトル</th>
{{-- + --}}
                <th>タグ</th>
                <th>アクション</th>
            </tr>
        </thead>
        <tbody>
            @foreach ($bookmarks as $bookmark)
                <tr>
                    <td>{{ $bookmark->id }}</td>
                    <td>{{ $bookmark->title }}</td>
{{-- +新しく<td>を追加 --}}
                    <td>
                        @foreach ($bookmark->tags as $tag)
                            <a
                                href="{{ route('tags.show', $tag) }}">{{ $tag->title }}</a>
{{-- ループのlastでないならカンマさせる --}}
                                @unless ($loop->last)
                                ,
                                @endunless
                        @endforeach
                    </td>

                    <td class="text-nowrap">
                        <a class="btn btn-info"
                            href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                        <a class="btn btn-secondary"
                            href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
                        <x-del_btn action="bookmarks" :bookmarkId="$bookmark->id" />
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>

先ほど、bookmarkに登録したタグが一緒に表示された。
image.png

発行されたクエリを確認すると23個も発行されており、tagsテーブルに何度もアクセスされている。
これは、繰り返しクエリが発行される。N+1問題が生じている。
image.png

N+1問題を解決するため遅延ロードさせる。

app\Http\Controllers\BookmarkController.php
    public function index()
    {
//with('リレーション名') これだけでOKです。
        $bookmarks = Bookmark::with('tags')->orderBy('id','desc')->paginate(20);
        return view('bookmarks.index', compact('bookmarks'));
    }

クエリの発行回数が4回に激減して、tagsに対するクエリもまとめられている。
image.png

メンバ変数で設定する場合

app\Models\Bookmark.php
//+    
    protected $with = [
//リレーション名
      'tags'
    ];

    public function tags()
    {
      return $this->belongsToMany(Tag::class);
    }
app\Http\Controllers\BookmarkController.php
    public function index()
    {
        //with('リレーション名')
        //$bookmarks = Bookmark::with('tags')->orderBy('id','desc')->paginate(20);
//自動で遅延ロードしてくれる。
        $bookmarks = Bookmark:orderBy('id','desc')->paginate(20);
        return view('bookmarks.index', compact('bookmarks'));
    }

tags.show画面でbookmarkの一覧を表示させる

app\Http\Controllers\TagController.php
    public function show(Tag $tag)
    {
        $bookmarks = $tag->bookmarks()->paginate(3);
        return view('tags.show',compact('tag','bookmarks'));
    }
resources\views\tags\show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">タグ詳細</div>
            <div class="card-body">
                <table class="table table-bordered">
                    <tbody>
                        <tr>
                            <th class="text-nowrap">タイトル</th>
                            <td>{{ $tag->title }}</td>
                        </tr>
                        <tr>
                            <th>作成日</th>
                            <td>{{ $tag->created_at->format('Y年m月d日 h:i:s') }}</td>
                        </tr>
                    </tbody>
                </table>

{{-- + bookmarks一覧 --}}
                <table class="table table-light">
                    <thead class="thead-light">
                        <tr>
                            <th>id</th>
                            <th>タイトル</th>
                            <th>タグ</th>
                            <th>アクション</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($bookmarks as $bookmark)
                            <tr>
                                <td>{{ $bookmark->id }}</td>
                                <td>{{ $bookmark->title }}</td>
                                <td>
                                    @foreach ($bookmark->tags as $tag)
                                        <a class="me-1"
                                            href="{{ route('tags.show', $tag) }}">{{ $tag->title }}</a>
                                    @endforeach
                                </td>

                                <td>
                                    <a class="btn btn-info"
                                        href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                                    <a class="btn btn-secondary"
                                        href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
                                    {{-- + --}}
                                    <x-del_btn action="bookmarks" :bookmarkId="$bookmark->id" />
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
                <div class="d-flex flex-column align-items-center">
                    {{ $bookmarks->links() }}
                </div>



            </div>
        </div>
    </div>
</div>
</div>
@endsection

何個か登録して確認できればOKです。
image.png

bookmarksテーブルをコンポーネント化する

  1. componentsディレクトリの下にbookmarksディレクトリを作成する
  2. その下にtable.blade.phpを作成する
resources\views\components\bookmarks\table.blade.php
{{-- bookmarks.index.blade.phpのテーブルをそのままカットして貼り付け --}}

<table class="table table-light">
    <thead class="thead-light">
        <tr>
            <th>id</th>
            <th>タイトル</th>
            <th>タグ</th>
            <th>アクション</th>
        </tr>
    </thead>
    <tbody>

        @foreach ($bookmarks as $bookmark)
            <tr>
                <td>{{ $bookmark->id }}</td>
                <td><a href="{{ route('bookmarks.show', $bookmark) }}">{{ $bookmark->title }}</a>
                </td>
                {{-- + --}}
                <td>
                    @foreach ($bookmark->tags as $tag)
                        <a href="{{ route('tags.show', $tag) }}">{{ $tag->title }}</a>
                        {{-- ループのlastでないならカンマさせる --}}
                        @unless($loop->last)
                            ,
                        @endunless
                    @endforeach
                </td>

                <td class="text-nowrap">
                    <a class="btn btn-info" href="{{ route('bookmarks.show', $bookmark) }}">詳細</a>
                    <a class="btn btn-secondary" href="{{ route('bookmarks.edit', $bookmark) }}">編集</a>
                    {{-- + --}}
                    {{-- 変更 :bookmark_id など キャメルはNG --}}
                    <x-del_btn routeIs="bookmarks" :id="$bookmark->id" />
                </td>

            </tr>
        @endforeach
    </tbody>
</table>

resources\views\bookmarks\index.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">ブックマーク一覧</div>
                    <div class="card-body">
                        <x-alert></x-alert>
                        <a href="{{ route('bookmarks.create') }}" class="btn btn-primary mb-3">新規登録</a>
   {{-- tebleをcutして変更 --}}
               <x-bookmarks.table :bookmarks="$bookmarks"/>

                        <div class="d-flex flex-column align-items-center">
                            {{ $bookmarks->links() }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

@endsection
resources\views\tags\show.blade.php
@section('content')
<div class="container">
<div class="row justify-content-center">
    <div class="col-md-8">
        <div class="card">
            <div class="card-header">タグ詳細</div>
            <div class="card-body">
                <table class="table table-bordered">
                    <tbody>
                        <tr>
                            <th class="text-nowrap">タイトル</th>
                            <td>{{ $tag->title }}</td>
                        </tr>
                        <tr>
                            <th>作成日</th>
                            <td>{{ $tag->created_at->format('Y年m月d日 h:i:s') }}</td>
                        </tr>
                    </tbody>
                </table>

{{-- + tableをcutして変更 :bookmarks="$bookmarks"--}}
                <x-bookmarks.table :bookmarks="$bookmarks" />
                    

                <div class="d-flex flex-column align-items-center">
                    {{ $bookmarks->links() }}
                </div>
            </div>
        </div>
    </div>
</div>
</div>
@endsection

そのまま表記されていればOKです。
image.png

リレーション先でN+1問題が生じています。tagsクエリが繰り返し発行されている。
image.png

原因
コントローラー
        $bookmark = $tags->bookmark->paginate(3);

conmponents.bookmarks.table.blade.php
        <td>

//此処でfrom tagクエリが繰り返し発行される。
        @foreach ($bookmark->tags as $tag)
            <a href="{{ route('tags.show', $tag) }}">{{ $tag->title }}</a>
            {{-- ループのlastでないならカンマさせる --}}
            @unless($loop->last)
                ,
            @endunless
        @endforeach
        </td>

遅延ロードさせる。

app\Http\Controllers\TagController.php
    public function show(Tag $tag)
    {

//->with()を追加するだけ
        $bookmarks = $tag->bookmarks()->with('tags')->paginate(3);
        return view('tags.show',compact('tag','bookmarks'));
    }

結果 N+1問題が解決される。
発行されるクエリの数が激減しているのを確認してください。
image.png

app\Http\Controllers\TagController.php
    public function show(Tag $tag)
    {

//->paginate(10)に変更してください
        $bookmarks = $tag->bookmarks()->with('tags')->paginate(10);
        return view('tags.show',compact('tag','bookmarks'));
    }

発行されるクエリの数が維持されているのと、tagsがまとめてselectされていればOKです。
image.png

第15回 Laravel Socialiteを使ってSNSアカウントでログインできるようにしよう

laravel socialiteをインストール

composer require laravel/socialite

gitHubアカウントを使用してログインする。

環境構築
image.png
image.png
image.png
上から
BookmarkApp
http://localhost
bookmark
http://localhost/Bookmark/public/auth/github/callback(xammp)
http://localhost:8000/auth/github/callback(php artisan serve時)
image.png
image.png
image.png

config\services.php
return [
    'mailgun' => [
        'domain' => env('MAILGUN_DOMAIN'),
        'secret' => env('MAILGUN_SECRET'),
        'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
    ],

    'postmark' => [
        'token' => env('POSTMARK_TOKEN'),
    ],

    'ses' => [
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    ],
//+
    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('GITHUB_CALLBACK_URL'),
    ],
]
env
GITHUB_CLIENT_ID=fafadfadfja;dfkaffaw
GITHUB_CLIENT_SECRET=fadfap:fawkeflasdl;ffa
GITHUB_CALLBACK_URL=http://localhost/Bookmark/public/auth/github/callback

ログインコントローラーを設定する

まずはユーザー情報を取得してみる。

app\Http\Controllers\Auth\LoginController.php
//+
use Laravel\Socialite\Facades\Socialite;

//この引数$proveiderはルートの{proveider}のこと
//
public function redirectToProvider($provider){

    //登録してあるsocialiteのログインサイトに自動でアクセスしてくれる。
    return Socialite::driver($provider)->redirect();

}

public function handleProviderCallback($provider)
{

//socialiteがこのルートに直接user情報を送信してくるので、
//そのuser情報はこの関数で取得できる。
       $user = Socialite::driver($provider)->user();
       dd($user);
}

ルートを用意する

routes\web.php
//+
use App\Http\Controllers\Auth\LoginController;

Route::get('/auth/{provider}/redirect', [LoginController::class, 'redirectToProvider'])->name('social_login');

//ridirectに登録したURIを作成する。ここにアカウントのuser情報が返ってくる。{provider}は
//変数にして汎用性を高めてある。
Route::get('/auth/{provider}/callback', [LoginController::class, 'handleProviderCallback']);

githubログインbtnを用意する

resources\views\auth\login.blade.php
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Login') }}
                                </button>

                                @if (Route::has('password.request'))
                                    <a class="btn btn-link" href="{{ route('password.request') }}">
                                        {{ __('Forgot Your Password?') }}
                                    </a>
                                @endif
  {{-- //+パスワードリセットボタンの@endifの下に追記 --}}
                    <hr>
                    <p>SNSアカウントでログイン</p>
  {{-- //+ --}}
                    <a class="btn btn-secondary" href="{{ route('social_login','github') }}">
                        Github
                    </a>
                           </div>

githubのログイン画面をクリック
image.png
githubのログインサイトにアクセスして緑のbtnをクリックしてuser情報が取得できていればOKです。
image.png

user情報を用いてユーザー登録とログインを完了させる

usersテーブルを修正する
passwordやemailをnull_ableに設定する必要があります。
返ってきた情報はprovider_idとprovider_nameとsocialite_emailに追加します。

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique()->nullable();

            $table->string('provider_name')->nullable();
            $table->string('provider_id')->nullable();
            $table->string('provider_email')->nullable();


            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            
            $table->rememberToken();
            $table->timestamps();
        });
    }

ホワイトリストを登録する

app\Models\User.php
    protected $fillable = [
        'name',
        'email',
        'password',
 //+       
        'provider_id',
        'provider_name',
        'provider_email',
    ];
php artisan migrate:fresh

新しくダミーデータを作成する

database\seeders\DatabaseSeeder.php
        \App\Models\Tag::factory(10)->create();

        \App\Models\Bookmark::factory(100)->create()->each(function (\App\Models\Bookmark $bookmark) {
            $array = range(1, 10);
            shuffle($array);
            $tag_ids = array_slice($array, 0, rand(0, 4));
            $bookmark->tags()->sync($tag_ids);
        });
php artisan db:seed

app\Http\Controllers\Auth\LoginController.php
//+
use App\Models\User;
//+
use Illuminate\Support\Facades\Auth;

    public function handleProviderCallback($provider)
    {
            try {
                //socialiteがこのルートに直接user情報を送信してくるので、
                //そのuser情報はこの関数で取得できる。
                // $user = Socialite::driver($provider)->user();
                // dd($user);
        //+
                $socialiteUser = Socialite::driver($provider)->user();
        
            } catch (\Throwable $th) {
                dd($th);
                return  redirect()->route('login');
            }
        //+今回久保さんのコードを採用しましたが、採用したコードはセキュリティー的にどうかと思った。
        //前回gitHubで登録したユーザーが今回gogoleで登録した時、同じメールを使用していた場合、
        //当然ながら前回のgithubでのユーザーとして認証される。
        //つまり、googleのアカウントが乗っ取られた場合、そのほかのgithubで認証したサイトの情報も乗っ取られてしまう可能性がある。
        //oath認証って滅茶苦茶怖いですね。
            // $user = User::where([
            //     'provider_name'=> $provider,
            //     'provider_id'=> $socialiteUser->getId(),
            // ])->first();
        
        //+ 返されたuser情報でusersテーブルに照会する
            $user = User::where(function ($query) use ($socialiteUser, $provider) {
                $query->where('provider_name', $provider)
                    ->where('provider_id', $socialiteUser->id);
            })
                ->orWhere('provider_email', $socialiteUser->email)
                ->first();

            //+ user情報がなかったらcreateする。
            if(!$user){
               $user = User::create([
                    'name'=> $socialiteUser->getNickname()??$socialiteUser->getName(),
                    'provider_id'=> $socialiteUser->getId(),
                    'provider_name'=> $provider,
                    'provider_email'=> $socialiteUser->getEmail(),
                ]);
            }
            //ログインする。
            Auth::login($user, true);
            return redirect()->route('bookmarks.index');
        }

loginができているのを確認してください。
image.png
usersテーブルにデータが作成されていればOKです。
image.png

以上完了です。

googleでログインしてみる.

リファレンスサイト(ここに全部書いてくれています)

環境構築

手順

  1. https://console.cloud.google.com/?hl=ja
  2. GOOGLE_CLIENT_ID=取得
  3. GOOGLE_CLIENT_SECRET=取得
  4. GOOGLE_CALLBACK_URL=http://localhost/Bookmark/public/auth/google/callback
  5. その他にも、OATH認証の登録が最初に1回必要だったと思いますが、2度目からは要求されませんでした。

1のサイトで,2,3を取得し4を登録すれば環境構築は完了
手順
image.png
image.png
4の承認済みリダイレクトURLはコールバックURLの意味です。web.phpのルートで作成したルートを登録します。
名前:BookmarkApp
承認済みJavaScript生成元に:http://localhost
承認済みリダイレクトURL:http://localhost/Bookmark/public/auth/google/callback
image.png
クライアントIDとシークレットIDをここでメモっておいて下さい。
image.png
以上で、環境構築は終了です。
手順1

env
GOOGLE_CLIENT_ID=106fadfadfad47181-dpfadklfjaogo4hm2krvb.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=fasdfafX-fadfafPb_3mfadfdc-B-OYI5Is
GOOGLE_CALLBACK_URL=http://localhost/Bookmark/public/auth/google/callback

手順2

config\services.php
    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('GITHUB_CALLBACK_URL'),
    ],

// 追記する
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_CALLBACK_URL'),
    ],

手順3
ルートとコントローラーは既に作成済みなので、OK

手順4
リンクの作成

resources\views\auth\login.blade.php
                    <p>SNSアカウントでログイン</p>
                    <a class="btn btn-secondary" href="{{ route('social_login','github') }}">
                        Github
                    </a>
  {{-- //+ --}}
                    <a class="btn btn-danger" href="{{ route('social_login','google') }}">
                        google
                    </a>
                            </div>
                        </div>
                    </form>

googleアカウントでログインしてみる。
image.png
usersテーブルにデータが入力されていたらOKです。
image.png

githubのメールがgoogleアカウントと同じ場合、googoleアカウントでloginしても、
githubで登録したuser情報でログインできるようにしています。
新しく登録したい場合はuserの取得方法を変更してください。

app\Http\Controllers\Auth\LoginController.php
//+
            $user = User::where([
                'provider_name'=> $provider,
                'provider_id'=> $socialiteUser->getId(),
            ])->first();
        
        //+ 返されたuser情報でusersテーブルに照会する
            // $user = User::where(function ($query) use ($socialiteUser, $provider) {
            //     $query->where('provider_name', $provider)
            //         ->where('provider_id', $socialiteUser->id);
            // })
            //     ->orWhere('provider_email', $socialiteUser->email)
            //     ->first();

yahooアカウントで登録してみる

リファレンスサイト

↓のサイトのコードを丸々使用しました。

環境構築の手順は先ほどのgithub,googleと同じです。
コールバックURLの登録に癖があります。

image.png

image.png
image.png
image.png
ここでクライアントIDとシークレットIDを取得
image.png
上の画像の3のアプリケーションの詳細リンクをクリックしてコールバックURLを登録
http://localhost/Bookmark/public/auth/yahoojp/callback
image.png

環境構築は終了です。

env
YahooJp_ID=fdkalfjaodfija;dlfafadfamVfadsklfjafadf-
YahooJp_SECRET=fadklsfjadlfja;ldfkjafdafiweofjaf
YahooJp_CALLBACKURL=http://localhost/Bookmark/public/auth/yahoojp/callback
config\services.php

    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
 //githubのredirectに登録したURL
        'redirect' => env('GITHUB_CALLBACK_URL'),
    ],

    
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_CALLBACK_URL'),
    ],
// 追記する
    'yahoojp' => [
        'client_id'     => env('YahooJp_ID'),
        'client_secret' => env('YahooJp_SECRET'),
        'redirect'      => env('YahooJp_CALLBACKURL'),
    ],

リンクを作成する

                    <p>SNSアカウントでログイン</p>
                    <a class="btn btn-secondary" href="{{ route('social_login','github') }}">
                        Github
                    </a>
                    <a class="btn btn-danger" href="{{ route('social_login','google') }}">
                        google
                    </a>
  {{-- //+ --}}
                    <a class="btn btn-danger" href="{{ route('social_login','yahoojp') }}">
                        yahoo
                    </a>
                            </div>
                        </div>
                    </form>

ルートとコントローラーは既に作成済みです。
本来なら、これでOKなのですが、Laravel SocialiteはyahooJpには対応していませんので、
Laravel Socialiteを拡張する必要があります。

手順1

  1. appディレクトリの下にapp\Socialiteディレクトリを作成
  2. その下に、2つのファイルと1つのディレクトリを作成してください
    1. app\Socialite\MySocialManager.php
    2. app\Socialite\MySocialServiceProvider.php
    3. Twoディレクトリを作成その下に
      1.app\Socialite\YahooJpProvider.php

image.png

app\Socialite\MySocialServiceProvider.php
<?php

namespace App\Socialite;

use Laravel\Socialite\SocialiteServiceProvider;
use Laravel\Socialite\Contracts\Factory;


class MySocialServiceProvider extends SocialiteServiceProvider
{
    public function register()
    {
        //シングルトンとしてMySocialManagerを生成
        $this->app->singleton(Factory::class, function ($app) {
            return new MySocialManager($app);
        });
    }
}
app\Socialite\MySocialManager.php
<?php

    namespace App\Socialite;

    use Laravel\Socialite\SocialiteManager;

    class MySocialManager extends SocialiteManager{

//+
        protected function createYahooJpDriver()
        {
            //services.phpの設定情報を読む
            $config = $this->config['services.yahoojp'];
            //設定情報と共にプロバイダを生成
            return $this->buildProvider(
                'App\Socialite\Two\YahooJpProvider',$config
            );
        }
app\Socialite\Two\YahooJpProvider.php
<?php

namespace App\Socialite\Two;

use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

class YahooJpProvider extends AbstractProvider implements ProviderInterface
{

    //scopeの区切り文字設定
    protected $scopeSeparator = ' ';

    //スコープ設定
    protected $scopes = [
        'openid',
        'profile',
        'email',
    ];

    //認証用URL($stateはオプション)
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://auth.login.yahoo.co.jp/yconnect/v2/authorization', $state);
    }

    //token取得用URL
    protected function getTokenUrl()
    {
        return 'https://auth.login.yahoo.co.jp/yconnect/v2/token';
    }

    //Token取得の際のオプション
    //Basic認証と必要なPOSTパラメータを送付
    public function getAccessToken($code)
    {
        $basic_auth_key = base64_encode($this->clientId.":".$this->clientSecret);

        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
            //認証
            'headers' => [
                'Authorization' => 'Basic '.$basic_auth_key,
            ],
            // 'form_params' => $this->getTokenFields($code),
            // 直接記述
            'form_params' => [
                'grant_type' => 'authorization_code',
                'code' => $code,
                'redirect_uri' => $this->redirectUrl
            ],
        ]);

        return $this->parseAccessToken($response->getBody());
    }

    //ユーザー情報取得
    protected function getUserByToken($token)
    {
        //url + schema=openidが必要だった
        $response = $this->getHttpClient()->get('https://userinfo.yahooapis.jp/yconnect/v2/attribute?schema=openid', [
            'headers' => [
                // 'Authorization' => 'Bearer '.$token['access_token'],
                'Authorization' => 'Bearer '.$token,
            ],
        ]);
        return json_decode($response->getBody(), true);
    }

    //Userにパラメーターをマップ(必要に応じてその他のパラメータ追加)
    protected function mapUserToObject(array $user)
    {
        return (new User())->setRaw($user)->map([
            'id' => $user['sub'],
            'name' => $user['name']??$user['nickname'],
            'email' => $user['email'],
            
        ]);
    }

    //token parse用の関数
    protected function parseAccessToken($body)
    {
        return json_decode($body, true);
    }
}

作成したproviderの登録

config\app.php
    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
//+
        App\Socialite\MySocialServiceProvider::class,
    ],
composer update

composer updateしてもコードのエラー表記が消えない場合で、気になる場合は、
vscodeを再起動してください。

lineアカウントでログイン

リファレンスサイト

滅茶苦茶勉強になったサイト
正直laravel Socialiteを使用するとOath認証の仕組みを理解することは困難だと思います。
laravel Socialiteを使用せずにsocialite loginの解説をしてくれています。

↓こっちをメインに↑を参考にすると凄くいい。↓は↑を参考にしている。因みに私は、上下共に頻繁に見てます。

今回は、laravel Socialiteを使用して実装します。

環境構築

https://developers.line.biz/ja/
lineログインをクリック
image.png
今すぐはじめようをクリック
image.png
各種入力
image.png
チャンネルIDとチャンネルシークレットがクライアントIDとシークレットIDになります。
上の方の非公開タグを公開タグに忘れずに設定してください。
image.png

コールバックURLの登録
http://localhost/Bookmark/public/auth/line/callback
image.png
メールアドレス取得の設定です。この設定をしていないとメールアドレスは取得できません。
スクリーンショットはログイン画面をスクショして保存しました
image.png
image.png
申請済みになれば、完了です。
image.png
環境構築は終了です。
大まかな手順はyahooJpと同じですが、emailの取得方法が独特です。
下記のサイトでくわしく書いてくれいます。画像は下記のサイトのスクショです。

image.png
上記サイトでは2の方法をで記載してくれていますので、1の方法で記載します。
1の方法がスタンダードだと思います。

env
LINE_CLIENT_ID=fdafjadlsffadf
LINE_CLIENT_SECRET=dfasdfadkl;fjaoifjaafda
LINE_REDIRECT=http://localhost/Bookmark/public/auth/line/callback
config\services.php

    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('GITHUB_CALLBACK_URL'),
    ],


    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_CALLBACK_URL'),
    ],

    'yahoojp' => [
        'client_id'     => env('YahooJp_ID'),
        'client_secret' => env('YahooJp_SECRET'),
        'redirect'      => env('YahooJp_CALLBACKURL'),
    ],

// 追記する
    'line'   => [
        'client_id'     => env('LINE_CLIENT_ID'),
        'client_secret' => env('LINE_CLIENT_SECRET'),
        'redirect'      => env('LINE_REDIRECT'),
    ],

ルートとコントローラーは既に作成済み。

resources\views\auth\login.blade.php
                    <p>SNSアカウントでログイン</p>
                    <a class="btn btn-secondary" href="{{ route('social_login','github') }}">
                        Github
                    </a>
                    <a class="btn btn-danger" href="{{ route('social_login','google') }}">
                        google
                    </a>
                    <a class="btn btn-danger" href="{{ route('social_login','yahoojp') }}">
                        yahoo
                    </a>
  {{-- //+ --}}
                    <a class="btn btn-success" href="{{ route('social_login','line') }}">
                        line
                    </a>
                            </div>
                        </div>
                    </form>
app\Socialite\MySocialManager.php
<?php

    namespace App\Socialite;

    use Laravel\Socialite\SocialiteManager;

    class MySocialManager extends SocialiteManager{

        protected function createYahooJpDriver()
        {
            //services.phpの設定情報を読む
            $config = $this->config['services.yahoojp'];
            //設定情報と共にプロバイダを生成
            return $this->buildProvider(
                'App\Socialite\Two\YahooJpProvider',$config
            );
        }
        
//+
        protected function createLineDriver()
        {
            $config = $this->config['services.line'];

            return $this->buildProvider('App\Socialite\Two\LineProvider', $config);
        }
    }
app\Socialite\Two\LineProvider.php
<?php
namespace App\Socialite\Two;

use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Arr;

class LineProvider extends AbstractProvider implements ProviderInterface
{
    // protected $parameters = [
    //     'nonce' => null
    // ];
    protected $scopeSeparator = ' ';

    protected $scopes = [
        'openid',
        'profile',
        'email',
    ];
 
    protected function getAuthUrl($state)
    {
        // 'nonce' パラメータ値の生成//正直これの使い方が不明だった。勉強になる。
        // $this->parameters['nonce'] = (string) uniqid('PREFIX', true);
 
        // 'nonce' パラメータ値をセッション変数に保存
        // session()->put(['nonce' => $this->parameters['nonce']]);
 
        return $this->buildAuthUrlFromBase('https://access.line.me/oauth2/v2.1/authorize', $state);
    }
    protected function getTokenUrl()
    {
        return 'https://api.line.me/oauth2/v2.1/token';
    }

    public function getAccessTokenResponse($code)
    {
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
            'headers' => [
                'Content-Type' => 'application/x-www-form-urlencoded',
            ],
            'form_params' => [
                'grant_type' => 'authorization_code',
                'code' => $code,
                'redirect_uri' => $this->redirectUrl,
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret,
            ],
        ]);
        return json_decode($response->getBody(),true);
    }

    protected function getUserByToken($token)
    {
        $response = $this->getHttpClient()->get('https://api.line.me/v2/profile', [
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer '.$token,
            ],
        ]);
 
        return json_decode($response->getBody(), true);
    }

    public function user()
    {
        if ($this->user) {
            return $this->user;
        }

        if ($this->hasInvalidState()) {
            throw new InvalidStateException;
        }

        $response = $this->getAccessTokenResponse($this->getCode());

        $id_token = Arr::get($response, 'id_token');

        $json = $this->curl_line($id_token);

        $this->user = $this->mapUserToObject($this->getUserByToken(
            $token = Arr::get($response, 'access_token')
        ));

// 戻り値: User にセット (map() でメールアドレスをセット)
        return $this->user->setToken($token)
                    ->setRefreshToken(Arr::get($response, 'refresh_token'))
                    ->setExpiresIn(Arr::get($response, 'expires_in'))
                    ->map(['email' => $json->email]);
    }

    protected function mapUserToObject(array $user)
    {
        return (new User())->setRaw($user)->map([
            'id' => $user['userId'],
            'name' => $user['displayName'],
            // 'avatar_original' => $user['pictureUrl'],
        ]);
    }

    public function curl_line($id_token){
        $headers = [ 'Content-Type: application/x-www-form-urlencoded' ];
        $post_data = array(
       //シークレットキー
       'id_token' => $id_token,
       //POST されたトークンの値
       'client_id' => $this->clientId,
        );
        $url = 'https://api.line.me/oauth2/v2.1/verify';

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post_data));
        $res = curl_exec($curl);
        curl_close($curl);
    
        $json = json_decode($res);
        // dd($json);
        return $json;
    }
}

以上、終了です。

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?