リファレンスサイト
🔽リクさんの運営ブログ
https://rikulog.com/
WEBOTV / WEBプログラム学習チャンネル
どっちにしようか迷った。
どちらとも、素晴らしい内容だ。
laravelを使っていれば、理解する必要もなく出来てしまう。
動画を見直していたら、全く理解していなかったことがよくわかった。
laravelのbladeコンポーネントが物凄く良かった。
btn とか リンクとか 後々編集するのは大変、やる気すらおきん。
なぜ、btnやリンクみたいなのをコンポーネント化するのが不思議だったが、
やっと意味がわかった、始めっから、コンポーネント化して使用するべきだった。
start project
機能紹介
ログイン機能
掲示板機能
記事の投稿
コメントの入力
検索機能
カテゴリー検索
テキスト検索
ハッシュタグ機能
タグの貼り付けと検索
作成するテーブル
users
posts
comments
categories
後で、
tags
post_tag
laravel new laravel_create_board_by_riku
データーベースの設定
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel_create_board_by_riku2
# DB_USERNAME=root
# DB_PASSWORD=
type nul > database/database.sqlite
touch database/database.sqlite
//false->true に変更
'debug' => (bool) env('APP_DEBUG', true),
composer require barryvdh/laravel-debugbar --dev
php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"
composer require laravel/ui
php artisan ui bootstrap --auth
npm install && npm run dev
npm run dev
###日本語化のための設定 @mitashun
'timezone' => 'Asia/Tokyo',
'locale' => 'ja',
'faker_locale' => 'ja_JP',
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');"
type nul > 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.": "続行するにはパスワードを入力してください。"
}
-------------------省略----------------
'user' => "メールアドレスに一致するユーザーは存在していません。",
//+
'throttled' => 'しばらく待ってから再度お試しください。',
'attributes' => [
//+
"name" => "名前",
"password" => "パスワード",
"password_confirmation" => "パスワード(確認用)",
"email" => "メールアドレス",
],
パスワード再発行メールの日本語化
APP_URL=https://localhost/laravel_create_board_by_riku/public
パスワードリセットメールの作成
php artisan make:notification ResetPasswordNotification
パスワードリセットメール
日本語に作り変える。Notificationクラスで宛先、主題やメールのテキストを
作成できる。
class ResetPasswordNotification extends Notification
{
use Queueable;
//+
public $token;
public function __construct($token)
{
//+
$this->token = $token;
}
//+
public function toMail($notifiable)
{
//dd($notifiable)
return (new MailMessage)
//sendPasswordResetNotification()はdefaultで'vendor.notifications.email'
//->markdown('vendor.notifications.email')指定する必要はないが、
//指定して、別のファイルに変更することもできる。区切り文字は'/'ではなく'.','blade.php'はイラン
->markdown('vendor.notifications.email')
//actionbtnの色を緑色に変更できる
->success() //->error()
->from('admin@example.com', config('app.name'))
->subject($notifiable->name.'様 パスワード再発行いたしました。')
//actionbtnの前のラインで表示
->line('パスワード再発行リクエストがありましたので、メッセージ送信しました。')
//config('app.url')でenvのAPP_URLを取得,
//route('password.reset', $this->token, false)))
//第3引数のfalseで相対パスを作成。
->action('パスワード再設定', url(config('app.url').route('password.reset', $this->token, false)))
//actionbtnの後のラインで表示
->line('もし心当たりがない場合は、本メッセージは破棄してください。');
}
}
メール本文、日本語に作り変える。2
メールのレイアウト等、より詳細に文面を作成する。
Notificationはクラスを作成して、bladeも作成する2段階設定になっている。
php artisan vendor:publish --tag=laravel-notifications
@component('mail::message')
{{-- Greeting --}}
@if (! empty($greeting))
# {{ $greeting }}
@else
@if ($level == 'error')
# Whoops!
@else
# こんにちは。
@endif
@endif
{{-- Intro Lines --}}
@foreach ($introLines as $line)
{{ $line }}
@endforeach
{{-- actionBtnの前のライン --}}
@isset($actionText)
<?php
switch ($level) {
case 'success':
{{-- yellowとかしてもアカンかった。--}}
$color = 'green';
break;
case 'error':
$color = 'red';
break;
default:
$color = 'blue';
}
?>
@component('mail::button', ['url' => $actionUrl, 'color' => $color])
{{ $actionText }}
@endcomponent
@endisset
{{-- actionBtnのあとのライン --}}
@foreach ($outroLines as $line)
{{ $line }}
@endforeach
{{-- Salutation --}}
@if (! empty($salutation))
{{ $salutation }}
@else
{{ config('app.name') }} より
@endif
{{-- Subcopy --}}
@isset($actionText)
@component('mail::subcopy')
もし、「{{ $actionText }}ボタン」がうまく機能しない場合、以下のURLをコピー&ペーストして直接ブラウザからアクセスしてください。
[{{ $actionUrl }}]({{ $actionUrl }})
@endcomponent
@endisset
@endcomponent
@component('mail::message')
等の中を直接編集したい場合。
php artisan vendor:publish --tag=laravel-mail
//+
use App\Notifications\ResetPasswordNotification;
-----------省略---------------
class User extends Authenticatable
{
-----------省略---------------
//+ パスワードリセットメールの変更
//sendPasswordResetNotification()はdefaultで'vendor.notifications.email'を指定
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPasswordNotification($token));
}
}
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=xxxxxxx@gmail.com
MAIL_PASSWORD=gcegjkehfdsftksf
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=xxxxxxx@gmail.com
MAIL_FROM_NAME="メッセージが届いています"
php artisan migrate
1.ユーザー登録をする。
2.validation等の日本語を確認する
//novalidateを貼ってvalidationの日本語化を確認する。
<form method="POST" action="{{ route('login') }}" novalidate>
3.パスワードリセットを送って、メールの日本語と
アクションボタンのURLが適切に機能するか確認する。
上記1,2,3の機能を確認する。
###モデル・テーブルを作成する。
1.posts 2.comments 3.categorie
php artisan make:model Post -a
php artisan make:model Comment -a
Laravelのマイグレーション用に覚えておくべきデータ型6個
データ型 | 説明 | 使用時 |
---|---|---|
integer | INTEGERカラム | 整数 |
string | VARCHARカラム | 名前など短めの文字列 |
text | TEXTカラム | コメントなどの文字列 |
longText | LONGTEXTカラム | かなり長い文字列 |
unsignedBigInteger | 符号なしBIGINTカラム | 他のテーブルのID |
boolean | aligned | true/false |
public function up()
{
//外部キー制約を無効
Schema::disableForeignKeyConstraints();
Schema::create('posts', function (Blueprint $table) {
$table->id();
//not nullableでリレーション先のrecordが更新されたら更新、削除されたら削除
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->foreignId('category_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->string('title');
$table->text('content');
$table->timestamps();
//nullableでリレーション先のrecordが更新されたら更新、削除されたらnull値に変更
//$table->foreignId('to_user_id')
// ->nullable()
// ->constrained("users")
// ->cascadeOnUpdate()
// ->nullOnDelete();
});
//外部キー制約を有効
Schema::enableForeignKeyConstraints();
}
public function up()
{
//外部キー制約を無効
Schema::disableForeignKeyConstraints();
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->foreignId('post_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->text('content');
$table->timestamps();
});
//外部キー制約を有効
Schema::enableForeignKeyConstraints();
}
//+ホワイトリスト方式createやupdateとfillの時に登録してある
//カラムだけが、保存できる。insert()は対象外
protected $fillable = [
'user_id',
'title',
'content',
'category_id',
];
//+
protected $fillable = [
'user_id',
'post_id',
'content',
];
php artisan make:model Category -a
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
//+
protected $fillable = [
'name',
];
php artisan migrate
###テーブルに、ダミーデータを追加する
users,posts,categoryテーブルにダミーデータを入力
ここは、いくらでも無駄にコレるが、
コレさえ、あれば、もう何でもかける。
//+
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
public function run()
{
//db:seedする度にテーブルを消去してくれるtruncate()
//truncate()する時、外部キーが邪魔するので、一旦無効にしている。
Schema::disableForeignKeyConstraints();
DB::table('categories')->truncate();
DB::table('users')->truncate();
DB::table('posts')->truncate();
DB::table('comments')->truncate();
//外部キーを有効に戻す
Schema::enableForeignKeyConstraints();
//正直、ダミーデータを入力するくらいなら、これで十分。
DB::table('categories')->insert(
[
['name'=>'book'],
['name'=>'travel'],
['name'=>'cafe'],
]
);
//コメントを解除するだけ。
$users = \App\Models\User::factory(10)->create();
//雛形にして、使いまわしている。
$users->each(function (\App\Models\User $user) use($users) {
\App\Models\Post::factory(3)->create(
[
'user_id' => $user->id,
]
)->each(function ($post) use($users){
for($i=1;$i<=4;$i++){
\App\Models\Comment::factory()->create(
[
'post_id' => $post->id,
//User::where()はエラーになる。
'user_id' => $users->where('id','<>',$post->user_id)->random()->id,
]
);
}
});
});
}
//+
use App\Models\Category;
-----
public function definition()
{
$categories = Category::all();
return [
'category_id' => $categories->random()->id,
'title' =>$this->faker->realText(rand(10,20)),
'content' =>$this->faker->realText(),
];
}
public function definition()
{
return [
'content' => $this->faker->realText(rand(20,30)),
];
}
php artisan db:seed
###ログイン後のトップページを変更する
// public const HOME = '/home';
public const HOME = '/';
PostのCRUDを実装する
ルートを作成する
//コメントアウト
// Route::get('/', function () {
// return view('welcome');
// });
Route::middleware(['auth'])->group(function () {
Route::get('/', [App\Http\Controllers\PostController::class, 'index'])->name('posts.index');
Route::resource('/posts', App\Http\Controllers\PostController::class,['except'=>'index']);
});
// onlyを使う方法
//Route::resource('hoge', App\Http\Controllers\PostController::class, ['only' => ['index', 'create', 'edit', 'store', 'destroy']]);
// exceptを使う方法
//Route::resource('hoge', App\Http\Controllers\PostController::class, ['except' => ['show', 'update']]);
Verb | Uri | Action | Route Name |
---|---|---|---|
GET | /posts | index | posts.index |
GET | /posts/create | create | posts.create |
POST | /posts | store | posts.store |
GET | /posts/{post} | show | posts.show |
GET | /posts/{post}/edit | edit | posts.edit |
PUT/PATCH | /posts/{post} | update | posts.update |
DELETE | /posts/{post} | destroy | posts.destroy |
Postの一覧画面を作成する
layouts.appの編集
{{-- +asset('css/app.css')の下 --}}
@stack('css')
-----
<!-- Scripts deferを消去して</body>の上に移動-->
<script src="{{ asset('js/app.js') }}"></script>
<!-- + -->
@stack('js')
posts.indexの作成
コントローラーを編集する
posts.index(一覧画面)を作成する
edit:
app\Http\Controllers\PostController.php
public function index()
{
$posts = Post::all();
//dd($posts);
return view('posts.index',compact('posts'));
}
postsとusersでリレーションする。
//laravelの命名規則通りに外部キーはuser_id <-> 主キーがid のため、明示する必要がない。
//どちらかが、命名規則外だと、それぞれ、明示する必要がある。
//+ users->postsは1対多
public function posts()//複数
{
return $this->hasMany(Post::class);
// return $this->hasMany(Post::class, 'user_id');//外部キーを明示
// return $this->hasMany(Post::class, 'user_id', 'id');//主キーも明示
}
postsとusersとcategoriesでリレーションする。
//+posts->usersは多対1
public function user() //単数
{
return $this->belongsTo(User::class);
// return $this->belongsTo(User::class, 'user_id');
// return $this->belongsTo(User::class, 'user_id', 'id');
}
//+ posts->categoryは多対1
public function category() //単数
{
//外部キーはpostsにあって参照する側もposts 普通、参照する側に外部キーを持たすらしい
return $this->belongsTo(Category::class);
//外部キーと主(対)キーを明示する場合
//return $this->belongsTo(Category::class,'category_id','id');
}
//+ category->postsは1対多
public function posts() //複数形
{
return $this->hasMany(Post::class);
//外部キーと主(対)キーを明示する場合
//return $this->hasMany(Post::class,'category_id','id');
}
create:
resources\views\posts\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">
<!-- $postが繰返えされている -->
@foreach ($posts as $post)
<div class="
card mt-1">
<div class="card-body">
<h5 class="card-title">{{ $post->title }}</h5>
<!-- さらに、カテゴリーテーブルとリレーションしている 関係は 多対1 -->
<p><a href="" class="card-text">{{ $post->category->name }}</a></p>
<!--{!! nl2br(e($post->content)) !!} 改行+サニタイズ -->
<p class="card-text mt-1">{!! nl2br(e($post->content)) !!}</p>
<!--リンク先は route('ルートネーム',$model) で取得できる -->
<a href="{{ route('posts.show', $post) }}" class="btn btn-primary">コメ</a>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection
◎カテゴリーテーブルとリレーションしている。{{ $post->category->name }}
関係は 多対1 N+1問題が生じている。
N+1問題を解決するために、先にデータを全部取得しておく。
public function index(Request $request)
{
$posts = Post::with('category')->get();
// dd($posts);
return view('posts.index',compact('posts'));
}
それぞれのポストItemsにカテゴリーのリレーションが貼られる。
クエリの発行数が31回->2回に激減した。
リレーション関係が多対1で多のモデルが1を繰り返し取得するため、N+1問題が生じている。そのため、EagerLoadingで一括して多に1の情報を貼り付けて取得している。
投稿者の名前を追記する。posts->usersテーブル(多対1)
<h5 class="card-title">{{ $post->title }}</h5>
<!-- カテゴリーネームの上に追記 ポスト->ユーザーの関係は多対1 -->
<p><a href="" class="card-text">{{ $post->user->name }}</a></p>
<!-- 記入済み category->name -->
<p><a href="" class="card-text">{{ $post->category->name }}</a></p>
また、多のモデルが繰り返されクエリーが発行されている
N+1問題が生じているので、同じように、EagerLoadingが必要
N+1問題が生じるか生じないかは単純な規則がある。
1対1、1対多、多対1、多対多
赤のモデルが繰り返されるとN+1問題が発生する。
ここでは、{{$post->user->name }}で発生する。
//変更リレーションuserにもEagerLoadingする。
public function index()
{
//複数のテーブルをwithやloadでリレーションする時は配列にしてやるだけ。
$posts = Post::with(['category','user'])->get();
// dd($posts);
return view('posts.index', compact('posts'));
}
見違えるようにスッキリした。
###ページネーションを実装する。
bootstrap4のージネーションを実装する場合、
app\Providers\AppServiceProvider.phpで明示する必要がある。
//+
use Illuminate\Pagination\Paginator;
----
public function register()
{
//+
Paginator::useBootstrap();
}
//変更
public function index()
{
// $posts = Post::with(['category','user'])->get();
//get->paginate(数)に変更
$posts = Post::with(['category','user'])->paginate(3);
// dd($posts);
return view('posts.index',compact('posts'));
}
@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 overflow-auto">
@foreach ($posts as $post)
<div class="
card mt-1">
<div class="card-body">
<h5 class="card-title">{{ $post->title }}</h5>
<a href="" class="card-text">{{ $post->category->name }}</a>
<p class="card-text mt-1">{!! nl2br(e($post->content)) !!}</p>
<a href="{{ route('posts.show', $post) }}" class="btn btn-primary">詳細 コメントを投稿</a>
</div>
</div>
@endforeach
</div>
<!-- + -->
{{ $posts -> links() }}
</div>
</div>
</div>
</div>
@endsection
モデルへのアクセス数が33回から5回へと激減している。
ページネーションをカスタマイズする。
今回は中央寄せをページネーションの方で書き換える。
php artisan vendor:publish --tag=laravel-pagination
resources\views\vendor\pagination にページネーションのファイルが作成される。
不要なやつは削除する。
@if ($paginator->hasPages())
{{-- nav要素にmx-autoを追記 --}}
<nav class="mx-auto mt-2">
シンプルページネーションに変更してみる。
//+$postをviewにリターンしてやるだけ。
public function show(Post $post)
{
//dd($post)
return view('posts.show',compact('post'));
}
リレーションは無いが$post
の時はN+1問題は生じない
のでEagerLoadingする必要がない。
これが、$posts
だったら、繰り返しが生じた場合、N+1問題が生じる。
create:
resources\views\posts\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">{{ $post->title }}</div>
<div class="card-body">
<p>カテゴリー:<a href="">{{ $post->category->name }}</a></p>
<p>{!! nl2br(e($post->content)) !!}</p>
</div>
<div class="card-footer">
<p>投稿時間:{{ $post->created_at }}</p>
<a href="{{ route('posts.index') }}">戻る</a>
</div>
</div>
</div>
</div>
</div>
@endsection

投稿画面を作成する
posts.createの作成
これはshow画面をformに作り変えてやればいいだけ。
index画面にposts.createのリンクを貼ってやる。
<div class="col-md-8">
<div class="card">
{{-- - カードヘッダーを修正 --}}
{{-- <div class="card-header">投稿一覧</div> --}}
{{-- + --}}
<div class="card-header d-flex justify-content-between">
<span>投稿一覧</span>
<a href="{{ route('posts.create') }}">投稿</a>
</div>
//+
use App\Models\Category;
//+
public function create()
{
$categories = Category::all();
return view('posts.create', compact('categories'));
}
create:
resources\views\posts\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('posts.store') }}" novalidate>
@csrf
<div class="form-group row no-gutters">
<label for="title" class="col-md-4 col-form-label text-md-left">タイトル</label>
<div class="col-md-8">
<input id="title" type="text" class="form-control @error('title') is-invalid @enderror" name="title" value="{{ old('title') }}" required autocomplete="title" autofocus>
@error('title')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row no-gutters">
<label for="category" class="col-md-4 col-form-label text-md-left">カテゴリー</label>
<div class="col-md-8">
<select name="category_id" id="" class="form-control @error('category_id') is-invalid @enderror" >
<option value="">未選択</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @if (old('category_id') == $category->id)
selected
@endif>{{ $category->name }}</option>
@endforeach
</select>
@error('category_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row no-gutters">
<label for="content" class="col-md-4 col-form-label text-md-left">コンテンツ</label>
<div class="col-md-8">
<textarea name="content" id="" cols="30" rows="10" class="form-control @error('content') is-invalid @enderror" >{{ old('content') }}
</textarea>
@error('content')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row mb-0 no-gutters">
<div class="col-md-8 offset-md-4 text-right">
<button type="submit" class="btn btn-primary">
投稿
</button>
</div>
</div>
</form>
</div>
<div class="card-footer text-right">
<a href="{{ route('posts.index') }}">戻る</a>
</div>
</div>
</div>
</div>
</div>
@endsection
###バリデーションクラスを作成する
php artisan make:request PostRequest
app\Http\RequestsにPostRequest.phpが作成される。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
//+
use App\Models\Category;
class PostRequest extends FormRequest
{
public function authorize()
{
//falseからtureに変更 認証系の機能をココで作成できる。
//falseだと403エラーになる。
//$this->nameで$_REQUEST['name']を取得できる。
return true;
}
public function rules()
{
$categories = Category::get('id')->implode('id', ',');
return [
'title' => ['required', 'min:10', 'max:25'],
'content' => 'required',
'category_id' => "required|in:$categories",
];
}
//日本語化済み。
public function messages()
{
return [
// "required" => ":attributeは必須項目です。",
// "email" => ":attributeはメールアドレスの形式で入力してください。",
// "numeric" => ":attributeは数値で入力してください。",
// "opinion.max" => ":attributeは500文字以内で入力してください。"
];
}
public function attributes()
{
return [
'title' => 'タイトル',
'content' => '内容',
'category_id' => 'カテゴリー',
];
}
}
//+
use App\Http\Requests\PostRequest;
//Request から PostRequest に変更するだけ。
public function store(PostRequest $request)
{
//
}
formにnovalidateをつけて、バリデーションを確認する。

投稿をデーターベースに保存する。
posts.storeの作成
use Illuminate\Support\Facades\Auth;
public function store(PostRequest $request)
{
$request->merge(['user_id'=>Auth::id()]);
//$fillableしているからもう、これで十分だと思う。
Post::create($request->all());
//flashメッセージをつけて、index画面にリダイレクト
return redirect()->route('posts.index')->with('status', ['success' => '投稿完了']);
}
posts.indexを修正するflashメッセージを表示させる。
コンポーネントを使用してflashメッセージ表示してみる。
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<!-- コンポーネントファイルの読み込み -->
<x-flashMessage/>
<div class="card">
resources\views\components\
の直下に *.blade.php
を作成するだけ。
読み込みは <x-*/>
で読み込める。
@if (session()->has('status'))
@foreach (session('status') as $key => $session)
<div class="alert alert-{{$key}} alert-dismissible fade show" role="alert">
<strong>{{ $session }}</strong>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
@endforeach
@endif
###画像投稿機能を追加する
posts.create画面を修正
<!-- 追記 enctype="multipart/form-data" -->
<form method="POST" action="{{ route('posts.store') }}" novalidate enctype="multipart/form-data">
<!-- + <button>の直上 -->
<div class="form-group row no-gutters">
<label for="content" class="col-md-4 col-form-label text-md-left">画像</label>
<div class="col-md-8">
<x-fileRender></x-fileRender>
</div>
</div>
<!-- + ここまで -->
https://v4.bootstrap-guide.com/javascript/forms/file-browser
コンポーネント化することで、使いまわしたり、切って可読性を向上できる。
create:
resources\views\components\fileRender.blade.ph
{{-- https://v4.bootstrap-guide.com/javascript/forms/file-browser --}}
@push('css')
<style>
.custom-file {
max-width: 20rem;
overflow: hidden;
}
.custom-file-label {
white-space: nowrap;
}
</style>
@endpush
<div class="form-group">
<label for="file">ファイル(4つまで選択可)</label>
<div id="file" class="input-group">
<div class="custom-file">
<input type="file" id="cutomfile" class="custom-file-input" name="cutomfile[]" multiple accept="image/*" />
<label class="custom-file-label" for="customfile" data-browse="参照">ファイル選択...</label>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary reset">取消</button>
</div>
</div>
@if ($errors->has('cutomfile.*'))
@foreach ($errors->get('cutomfile.*') as $message)
<span class="text-danger d-block m-0"> {{ $message[0] }} </span>
@endforeach
@endif
</div>
@push('js')
<script>
function check(file) {
if (file.length > 4) {
$('.custom-file-input').val('');
}
}
$('.custom-file-input').on('change', handleFileSelect);
function handleFileSelect(evt) {
$('#preview').remove(); // 繰り返し実行時の処理
$(this).parents('.input-group').after('<div id="preview"></div>');
var files = evt.target.files;
check(files);
for (var i = 0, f; f = files[i]; i++) {
if (i > 3) {
return false
}
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
if (theFile.type.match('image.*')) {
// 画像では画像のプレビューとファイル名の表示
var $html = [
'<div class="d-inline-block mr-1 mt-1"><img class="img-thumbnail" src="', e
.target.result, '" title="', escape(theFile.name),
'" style="height:100px;" /><div class="small text-muted text-center">',
escape(theFile.name), '</div></div>'
].join('');
} else {
//画像以外はファイル名のみの表示
// var $html = ['<div class="d-inline-block mr-1"><span class="small">', escape(theFile
// .name), '</span></div>'].join('');
}
$('#preview').append($html);
};
})(f);
reader.readAsDataURL(f);
}
$(this).next('.custom-file-label').html(+files.length + '個のファイルを選択しました');
}
//ファイルの取消
$('.reset').click(function() {
$(this).parent().prev().children('.custom-file-label').html('ファイル選択...');
$('#preview').remove();
$('.custom-file-input').val('');
})
</script>
@endpush
public function rules()
{
$categories = Category::get('id')->implode('id', ',');
return [
'title' => ['required', 'min:10', 'max:25'],
'content' => 'required',
'category_id' => "required|in:$categories",
//+
'cutomfile.*' => [
'file',
'image',
'mimes:jpeg,png,jpg',
'max:1024',//1MB
],
];
}
public function attributes()
{
return [
'title' => 'タイトル',
'content' => '内容',
'category_id' => 'カテゴリー',
//+
'cutomfile.*' => 'ファイル'
];
}
画像を保存する。
//+
use Illuminate\Support\Str;
public function store(PostRequest $request)
{
//+cutomfileがあったら、
if ($request->has('cutomfile')) {
//ユニークなフォルダパスを作成
$img_path = uniqid("tmp_img_") . "/";
foreach ($request->file('cutomfile') as $key => $img_file) {
//ファイルが存在しているかに付け加え、isValidメソッドで問題なくアップロードできたのかを確認できたなら
if ($img_file->isValid()) {
//画像なら//既にバリデーション済みだからイランが、ファイルと混合している時とか。
if (Str::is('image*', $img_file->getMimeType())) {
//pathを作成(path/+ ファイル名(空白は '_' に置き換えている))
$img_files_path = $img_path . Str::of($img_file->getClientOriginalName())->replace(' ', '_');
// storage\appからの相対パス 画像を保存している。//取得はsrc="{{ asset('storage/'.$tmp_file)
$img_file->storeAs("public", $img_files_path);
$img_files_path = '';
}
}
}
}
$request->merge(
['user_id' => Auth::id()]
);
//修正
Post::create($request->except('cutomfile'));
return redirect()->route('posts.index')->with('status', ['success' => '投稿完了']);
}
画像のパスを保存するカラムを追加する。
php artisan make:migration add_column_to_posts
class AddColumnToPosts extends Migration
{
public function up()
{
Schema::table('posts', function (Blueprint $table) {
$table->string('img_path')->nullable();
});
}
public function down()
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('img_path');
});
}
}
protected $fillable = [
'user_id',
'title',
'content',
'category_id',
//+
'img_path',
];
php artisan migrate
コントローラーを修正する
public function store(PostRequest $request)
{
if ($request->has('cutomfile')) {
$img_path = uniqid("tmp_img_") . "/";
//$requestにimg_pathを追加
$request->merge(
['img_path' => $img_path],
);
foreach ($request->file('cutomfile') as $key => $img_file) {
//ファイルが存在しているかに付け加え、isValidメソッドで問題なくアップロードできたのかを確認できたなら
if ($img_file->isValid()) {
//画像なら
if (Str::is('image*', $img_file->getMimeType())) {
//pathを作成(path/+ ファイル名(空白は '_' に置き換えている))
$img_files_path = $img_path . Str::of($img_file->getClientOriginalName())->replace(' ', '_');
// storage\appからの相対パス 画像を保存している。//取得はsrc="{{ asset('storage/'.$tmp_file)
$img_file->storeAs("public", $img_files_path);
$img_files_path = '';
}
}
}
}
$request->merge(
['user_id' => Auth::id()],
);
//修正2
Post::create($request->except('cutomfile'));
return redirect()->route('posts.index')->with('status', ['success' => '投稿完了']);
}
###画像を表示する。
リンクを作成する。
php artisan storage:link
@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">{{ $post->title }}</div>
<div class="card-body">
<p>カテゴリー:<a href="">{{ $post->category->name }}</a></p>
<p>{{ $post->content }}</p>
{{-- + :post="$post" でコンポーネントに値を渡している :img_path= これはエラーになる --}}
<x-img_views :imgPath="$post->img_path"></x-img_views>
</div>
<div class="card-footer">
<p>投稿時間:{{ $post->created_at }}</p>
<a href="{{ route('posts.index') }}">戻る</a>
</div>
</div>
</div>
</div>
</div>
@endsection
@php
if ($imgPath) {
$path = '/public/' . $imgPath;
$files = Storage::files($path);
//dd($files);
}else{
return;
}
@endphp
@isset($files)
@foreach ($files as $img_file)
<div class="d-inline-block mr-1 mt-1"><img class="img-thumbnail" src="{{ asset(Storage::url($img_file)) }}"
alt="{{ class_basename($img_file) }}" style="height:100px;" />
<div class="small text-muted text-center">{{ class_basename($img_file) }}</div>
</div>
@endforeach
@endisset
画像を投稿して、詳細画面で確認する。

投稿を編集する
posts.editの作成
create画面に$postを付け加えるだけ。
リンクをshow.blade.phpにつける。
{{--card-foote--}}
<div class="card-footer">
<p>投稿時間:{{ $post->created_at }}</p>
<a href="{{ route('posts.index') }}">戻る</a>
{{-- + --}}
<a class="btn btn-danger" href="{{ route('posts.edit',$post) }}">編集</a>
</div>
//+
public function edit(Post $post)
{
$categories = Category::all();
return view('posts.edit', compact('categories','post'));
}
個々の画像を追加したり、変更したり、削除すること考えたら、imgsテーブルを作って
img_pathを保存したほうが、良かった。反省だ。
@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">
<!-- enctype="multipart/form-data" -->
<form method="POST" action="{{ route('posts.update',$post) }}" novalidate enctype="multipart/form-data">
@method('put')
@csrf
<div class="form-group row no-gutters">
<label for="title" class="col-md-4 col-form-label text-md-left">タイトル</label>
<div class="col-md-8">
<!-- /* {{ old('title', $post->title) }}テーブルの値はold()の第2引数に入れてやればいいだけ*/ -->
<input id="title" type="text" class="form-control @error('title') is-invalid @enderror"
name="title" value="{{ old('title', $post->title) }}" required autocomplete="title"
autofocus>
@error('title')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row no-gutters">
<label for="category" class="col-md-4 col-form-label text-md-left">カテゴリー</label>
<div class="col-md-8">
<select name="category_id" id=""
class="form-control @error('category_id') is-invalid @enderror">
<option value="">未選択</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @if (old('category_id', $post->category_id) == $category->id)
selected
@endif>{{ $category->name }}</option>
@endforeach
</select>
@error('category_id')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row no-gutters">
<label for="content" class="col-md-4 col-form-label text-md-left">コンテンツ</label>
<div class="col-md-8">
<textarea name="content" id="" cols="30" rows="10"
class="form-control @error('content') is-invalid @enderror">{{ old('content', $post->content) }}
</textarea>
@error('content')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row no-gutters">
<label for="content" class="col-md-4 col-form-label text-md-left">画像削除</label>
<div class="col-md-8">
<x-img_views type="edit" :imgPath="$post->img_path"></x-img_views>
</div>
</div>
<div class="form-group row no-gutters">
<label for="content" class="col-md-4 col-form-label text-md-left">画像追加</label>
<div class="col-md-8">
<x-fileRender></x-fileRender>
</div>
</div>
<div class="form-group row mb-0 no-gutters">
<div class="col-md-8 offset-md-4 text-right">
<button type="submit" class="btn btn-primary">
投稿
</button>
</div>
</div>
</form>
</div>
<div class="card-footer text-right">
<a href="{{ route('posts.index') }}">戻る</a>
</div>
</div>
</div>
</div>
</div>
@endsection
components\img_views.blade.phpを編集する
@php
if ($imgPath) {
$path = '/public/' . $imgPath;
$files = Storage::files($path);
//dd($files);
}else{
return;
}
@endphp
{{-- 編集画面 --}}
@if (isset($type) == 'edit')
@isset($files)
{{-- checkboxを追加 --}}
@foreach ($files as $img_file)
<div class="d-inline-block mr-1 mt-1 border text-center"><label class=""><span class=" d-block mx-auto my-1
small"><input type="checkbox" class="align-middle" name="delete_customfile[]"
value="{{ $img_file }}">
削除</span><img class="img-thumbnail" src="{{ asset(Storage::url($img_file)) }}"
alt="{{ class_basename($img_file) }}" style="height:100px;" /></label>
<div class="small text-muted text-center">{{ class_basename($img_file) }}</div>
</div>
@endforeach
@endisset
@else
{{-- そうでない画面 --}}
@foreach ($files as $img_file)
<div class="d-inline-block mr-1 mt-1"><img class="img-thumbnail" src="{{ asset(Storage::url($img_file)) }}"
alt="{{ class_basename($img_file) }}" style="height:100px;" />
<div class="small text-muted text-center">{{ class_basename($img_file) }}</div>
</div>
@endforeach
@endif
post.update()もう殆ど、posts.create()と同じ、変更点は3箇所
こういうのは、スコープを使って関数化するべきだった。反省。
//+
use Illuminate\Support\Facades\Storage;
-----------------
public function update(PostRequest $request, Post $post)
{
//+編集で画像を削除した場合、
if (isset($request->delete_customfile)) {
foreach ($request->delete_customfile as $delete_customfile) {
$path = $delete_customfile;
Storage::delete($path);
}
}
if ($request->has('cutomfile')) {
//+
if (is_null($post->img_path)) {
$img_path = uniqid("tmp_img_") . "/";
$request->merge(
['img_path' => $img_path],
);
} else {
$img_path = $post->img_path;
}
foreach ($request->file('cutomfile') as $key => $img_file) {
if ($img_file->isValid()) {
if (Str::is('image*', $img_file->getMimeType())) {
$img_files_path = $img_path . Str::of($img_file->getClientOriginalName())->replace(' ', '_');
$img_file->storeAs("public", $img_files_path);
$img_files_path = '';
}
}
}
}
$request->merge(
['user_id' => Auth::id()],
);
//+作成したデータが欲しかったらupdateOrCreate()を使う。
$post->update($request->except('cutomfile', '_token'));
return redirect()->route('posts.index')->with('status', ['success' => '編集完了']);
}
投稿を削除する機能を実装する
posts.destroyの作成
public function destroy(Post $post)
{
if (isset($post->img_path)) {
//削除ファイルの相対パス:storage\app\public\img\tmp\tmp_6125eaa96f4ffForest.jpg
//基本:storage\app\以下を取得して
// $path='public\img\tmp\tmp_6125eaa96f4ffForest.jpg';
// Storage::delete($path); //ファイルの場合
// Storage::deleteDirectory($path); //ディレクトリの場合
//dd($post->img_path); //= "tmp_img_615563e7bb427/"
// storage\app\public\tmp_img_61556798e1d93
$path ='public/'.$post->img_path;
Storage::deleteDirectory($path);
}
$post->delete();
return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
}
posts.index画面に削除リンクをはる
{{-- -詳細をコメントアウト --}}
{{-- <a href="{{ route('posts.show', $post) }}" class="btn btn-primary">詳細 コメントを投稿</a> --}}
<!-- +1 ここ d-flexで詳細ボタンと並べる -->
<div class="d-flex justify-content-between">
<a href="{{ route('posts.show', $post) }}" class="btn btn-primary">詳細 コメントを投稿</a>
<!-- posts.destroyはmethod('delete')で送る必要があるため、formを使う必要がる。 -->
<!-- layout.appのlogoutのリンクをコピペして利用できる。 -->
<!-- this.onclick=null;で1回だけ実行できる。 -->
<!-- javascript:void(0)は今では使っては行けないコードみたいです -->
<!-- 代わりに、href="#" or buttonを使うみたいです。 -->
<a class="btn btn-danger " href="javascript:void(0)"
onclick="deletePost({{ $post->id }}); this.onclick=null;">
削除
</a>
</div>
<!-- ここまで -->
</div>
</div>
@endforeach
<!-- +2 formは繰り返しをさけるため endforeach のあとに置く -->
<form action="" method="POST" id="delete_form" class="d-none">
@csrf
@method('delete')
</form>
-----------------------------------------
@endsection
<!-- +3 一番下 -->
@push('js')
<script>
function deletePost(post_id) {
url = `{{ route('posts.destroy','') }}/${post_id}`;
$('#delete_form').attr('action',url).submit();
}
</script>
@endpush
画像フォルダも削除できているか、確認する。
php artisan db:seed
アクセス制限機能を追加する。
編集、削除は投稿者だけに許可するようにアクセス制限を設ける。
laravelの主なアクセス制限機能3つ
1.手動で実装する場合
2.Gate(ゲート)を利用したアクセス制限
3.Policy(ポリシー)を用いたアクセス制限
###まず1番の、手動で実装する場合
流れ
1. 権限のない場合、フロント側でリンクを非表示にしてやる。
2. 権限のない場合、サーバー側で処理しない。
フロント側で非表示にする。
resources\views\posts\index.blade.php
@if ($post->user_id == Auth::id())
<a class="btn btn-danger " href="javascript:void(0)"
onclick="deletePost({{ $post->id }}); this.onclick=null;">
削除
</a>
@endif
resources\views\posts\show.blade.php
@if ($post->user_id == Auth::id())
<a class="btn btn-danger" href="{{ route('posts.edit', $post) }}">編集</a>
@endif
カスタムif文に変えてみる。
AppServiceProvider.phpのboot()の中に定義してやるだけ。
注意:AuthServiceProvider ではない。
//+
use Illuminate\Support\Facades\Blade;
public function boot()
{
//+
Blade::if('isPost', function ($post) {
return (auth()->check() && auth()->id() == $post->user_id);
});
}
resources\views\posts\index.blade.php
@isPost($post)
<a class="btn btn-danger " href="javascript:void(0)"
onclick="deletePost({{ $post->id }}); this.onclick=null;">
削除
</a>
{{-- @else --}}
@endisPost
サーバー側の処理は認証が拒否なら処理を拒否する。
if(Auth::user()->id !== $post->user_id){ return }
を追加するだけ。
public function update(PostRequest $request, Post $post)
{
//+
if(Auth::user()->id !== $post->user_id){
return redirect()->route('posts.index')->with('status', ['danger' => '編集権限がありません']);
}
--------------------------------
}
public function destroy(Post $post)
{
//+
if(Auth::user()->id !== $post->user_id){
return redirect()->route('posts.index')->with('status', ['danger' => '削除権限がありません']);
}
$post->delete();
return redirect()->route('posts.index')->with('status',['success'=>$post->id.'番を削除しました']);
}
非表示を解除して実行してみた。
###2番の、Gate(ゲート)を利用したアクセス制限
GateはAuthServiceProvider.phpで定義
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* GateはAuthServiceProvider.phpで定義を記述します。
* 定義を行う時はbootメソッドの中にGateファサードのdefineメソッドを使って行う。
*/
public function boot()
{
$this->registerPolicies();
//第一引数にはアクセス制限を行う際に利用する任意の名前、第二引数にはクロージャ―を設定します。
//クロージャ―の処理ではtrueかfalseかを戻します.
//クロージャーの第一引数には必ずユーザーが返される。第二引数に設定した引数がわたる
//定義:
Gate::define('isAdmin', function ($user, $post) {
return $user->id == $post->user_id;
});
//使い方:
//コントローラー側
//Gate::authorize('isAdmin',$post); true or false
//authorizeメソッド、allowsメソッド、deniesメソッドを利用できる。
//blade側
//@can('isAdmin')
//@can('isAdmin',$post)
//投稿一覧を表示するhtmlを記述
//@else
//<p>貴方には、権限がありません。管理者のみが表示されます。</p>
//@endcan
}
}
雛形をみて、実装する。
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
public function boot()
{
$this->registerPolicies();
//+
Gate::define('isPost', function ($user, $post) {
return $user->id == $post->user_id;
});
}
}
まずは、先程と一緒で、表示制限を実装する。
resources\views\posts\index.blade.php
@can('isPost', $post)
<a class="btn btn-danger " href="javascript:void(0)"
onclick="deletePost({{ $post->id }}); this.onclick=null;">
削除
</a>
{{-- @else --}}
@endcan
resources\views\posts\show.blade.php
@can('isPost', $post)
<a class="btn btn-danger" href="{{ route('posts.edit', $post) }}">編集</a>
{{-- @else --}}
@endcan
前後
サーバ側で処理を禁止する。
authorizeメソッド、allowsメソッド、deniesメソッドによって、
falseのときの処理がことなる。
use Illuminate\Support\Facades\Gate;/*FacadesのGate*/
// use Illuminate\Auth\Access\Gate;//こっちではない。
public function destroy(Post $post)
{
// authorizeメソッドを使用した場合は、
//false時、403のAuthorizationExceptionがthrowされます。
Gate::authorize('isPost', $post);
// allowsの場合は、if分により分岐処理を行う
// if (Gate::allows('isPost', $post)) {
// $post->delete();
// return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
// } else {
// return redirect()->route('posts.index')->with('status', ['danger' => '編集権限がありません']);
// }
//deniesはallowsと逆の処理を行うことができます
// if (Gate::denies('isPost', $post)) {
// return redirect()->route('posts.index')->with('status', ['danger' => '編集権限がありません']);
// } else {
// $post->delete();
// return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
// }
}
authorizeメソッドの時
allowsやdeniesの場合、falseを条件文でコントロール
その他のGateの機能
public function destroy(Post $post)
{
// authorizeメソッドを使用した場合は、
//false時、403のAuthorizationExceptionがthrowされます。
// Gate::authorize('isPost', $post);
// allowsの場合は、if分により分岐処理を行う
// if (Gate::allows('isPost', $post)) {
// $post->delete();
// return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
// } else {
// return redirect()->route('posts.index')->with('status', ['danger' => '編集権限がありません']);
// }
//deniesはallowsと逆の処理を行うことができます
// if (Gate::denies('isPost', $post)) { // ! で否定文にしている。
// return redirect()->route('posts.index')->with('status', ['danger' => '編集権限がありません']);
// } else {
// $post->delete();
// return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
// }
//削除権限のあるユーザーを取得
$other_user = User::find($post->user_id);
//アクセスしたユーザではなく別のユーザーの権限でアクセス制限機能を使える。
if(Gate::forUser($other_user)->allows('isPost',$post)){
$post->delete();
return redirect()->route('posts.index')->with('status', ['success' => $post->id . '番を削除しました']);
}
}
Gate機能でmiddlewareでの制限もできる。trueのユーザーのみアクセスできる。
この場合は、管理者のみアクセスできるようにしている。
Route::middleware('can:is_admin')->group(function () {
Route::resource('/users', UserController::class);
});
public function edit(Post $post)
{
//+
Gate::authorize('isPost', $post);
------------
}
public function update(PostRequest $request, Post $post)
{
//+
Gate::authorize('isPost', $post);
------------
}
3.Policy(ポリシー)を用いたアクセス制限
Policyでは特定のモデルに関連する処理に対してアクセス制限を行なっていきます。
流れは、同じ、
非表示にして、
処理をさせない。
と
思ったが、違う、サーバー側の制限しかない。
非表示にするとかいう機能はない。
ウ~ン、下の画像のような時、一括して処理できていいと思う。
今回のケースでは適切ではないと思う policyは今回ながして、
ECサイトのデモサイトで採用する。今、これとこれをlaravelで書き換えている。
どちらとも本当に素晴らしい入門書だ。
###403ページを作成してみる。
以下にファイルを作成するだけ。
laravel/resources/views/errors/403.blade.php
404とかも、同じ。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>403 Forbidden</title>
<style>
.error-wrap {
padding: 5px 20px;
border: 1px solid #dcdcdc;
display: inline-block;
box-shadow: 0px 0px 8px #dcdcdc;
}
h1 { font-size: 18px; }
p { margin-left: 10px; }
</style>
</head>
<body>
<div class="error-wrap">
<section>
<h1>403 Forbidden</h1>
<p>認証に失敗いたしました。</p>
</section>
</div>
</body>
</html>
はじめテンプレートサイトからコピペしたけど、記事が書けなくなったので、
変更しました。
非表示制限を解除して、非権限のbtnを押して見てください。
403ファイルが返ってきたらokです。
コメント機能を実装する
posts->commentsテーブルは1対多の関係にある。
users->commentsテーブルも、1対多の関係にある。
public function comments()
{
return $this->hasMany(Comment::class);
//return $this->hasMany(Comment::class,'user_id','id');
}
//+
public function comments()
{
return $this->hasMany(Comment::class);
// return $this->hasMany(Comment::class,'post_id','id');
}
//+
public function post()
{
return $this->belongsTo(Post::class);
// return $this->belongsTo(Post::class,'post_id','id');
}
//+
public function user()
{
// return $this->belongsTo(User::class);
return $this->belongsTo(User::class,'user_id','id');
}
//+
protected $appends =[
'diff_in_time',
];
public function getDiffInTimeAttribute(){
$updated_at = $this->updated_at;
$diff_in_time = now()->diffInMinutes($updated_at);
if($diff_in_time > 60*24){
$diff_in_days =now()->diffInDays($updated_at);
return $diff_in_days.'日前';
}
if($diff_in_time > 60){
$diff_in_hours = now()->diffInHours($updated_at);
return $diff_in_hours.'時間前';
}
return $diff_in_time.'分前';
}
protected $appendsのおかげでこんなデータが簡単に使えるようになる。
ルートを追加する
Route::middleware(['auth'])->group(function () {
Route::get('/', [App\Http\Controllers\PostController::class, 'index'])->name('posts.index');
Route::resource('/posts', App\Http\Controllers\PostController::class,['except'=>'index']);
//+
Route::resource('/comments', App\Http\Controllers\CommentController::class);
});
posts.show 画面にコメント機能を実装する。
posts\show.blade.php
PostController のshow()を編集する。
$postにcommentsとcomments.userを貼り付けてやる。
public function show(Post $post)
{
//+
//commentsとcomments.userでloadを使ってリレーションしている。
//commentだけではなく、commentのuserデータともリレーションできる。
$post->load(['comments','comments.user']);
//dd($post)
return view('posts.show', compact('post'));
}
}
しっかり、postデータに,postのcommentデータ、commentのuserデータが取得できている。
@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">{{ $post->title }}</div>
<div class="card-body">
<p>カテゴリー:<a href="">{{ $post->category->name }}</a></p>
<p>{{ $post->content }}</p>
@if (isset($post->img_path))
{{--laravelのコンポーネント :imgPath="$post->img_path" で値を渡している。 --}}
<x-img_views :imgPath="$post->img_path"></x-img_views>
@endif
</div>
<div class="card-footer">
<p>投稿時間:{{ $post->created_at }}</p>
<a href="{{ route('posts.index') }}">戻る</a>
@can('isPost', $post)
<a class="btn btn-danger" href="{{ route('posts.edit', $post) }}">編集</a>
{{-- @else --}}
@endcan
</div>
</div><-- /.card -->
{{-- +laravelのbladeコンポーネント --}}
{{-- +コメントの作成フォーム --}}
<x-comment.create :post="$post" />
{{-- +コメントの一覧画面 --}}
<x-comment.index :post="$post" />
</div>
</div>
</div>
@endsection
bladeコンポーネントは resources\views\componentsに、
*.blade.phpファイルを作成すればいいだけ、めちゃお手軽。
//上記のデータをオブジェクト化するといい感じになる。
<script>
post = @json($post);
console.log(post);
</script>
<script>
post = @json($post);
console.log(post);
//リレーション先のデータも簡単に取得できる。
console.log(post.category.name);
console.log(post.comments[0].content);
console.log(post.comments[0].user.name);
console.log(post.comments[1].content);
console.log(post.comments[1].user.name);
</script>
コメントはajaxでSPA CRUDにしてみた。
jqueryでvueっぽく作ってみた。
//vueのdataみたいにオブジェクトとしておいておく。
comments = @json($post->comments);
console.log(comments);
commentsをvueのdataのように、利用できる。
crudする度に、commentsを修正して、getCommentsでhtmlを再形成している。
CommentControllerを編集する。
<?php
namespace App\Http\Controllers;
use App\Models\Comment;
use Illuminate\Http\Request;
use App\Models\Post;
class CommentController extends Controller
{
// public function index(){}
// public function create(){}
//+
public function store(Request $request)
{
$request->merge(['user_id'=>auth()->id()]);
//create()の返り値は作成したデータが返ってくる滅茶苦茶便利。
//appends[]のdiff_in_timeも返ってくる最高ー。
$comment = Comment::create($request->all());
//modelがget()前なら、with()でリレーションしてからget()!
//modelがget()後なら,load()でリレーションするだけ。
// cereate()はget()後のデータを返すので、load()を使う。
$comment->load('user');
return $comment;
}
// public function show(Comment $comment){}
// public function edit(Comment $comment){}
//+
public function update(Request $request)
{
//updateOrCreate()の返り値は作成したデータが返ってくる滅茶苦茶便利。
//update()はupdateした数が返ってくるから、使うことはない。
//appends[]のdiff_in_timeも返ってくる最高ー。
$comment = Comment::updateOrCreate(['id'=>$request->id],$request->all());
$comment->load('user');
return $comment;
}
public function destroy(Request $request)
{
Comment::find($request->id)->delete();
return 'success';
}
}
commentsオブジェクトから、html要素を作成してbodyに貼り付けただけ。
@push('js')
<script>
//親元のコンポーネントにデータを置いておけば、
//子コンポーネントにわざわざ渡さたなくても利用できる。
comments = @json($post->comments);
auth_id = {{ auth()->id() }};
post_id_page = {{$post->id}};
//これで、commentsデータをhtml化して上に貼り付けている。
if (comments.length > 0) {
getComments(comments);
}
$(document).on('click', '.delete_btn', function() {
comments_index = $(this).parents('.card').data('comments_index');
comment_id = comments[comments_index].id;
$.ajax({
url: "{{ route('comments.destroy', 0) }}",
type: 'DELETE', //DELETE送信
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
data: {
id: comment_id
},
timeout: 6000, //タイムアウトの設定
}).done(function() {
comments.splice(comments_index,1);
getComments(comments);
comments_index='';
}).always(function() {
comment_id = '';
});
});
function getComments(comments) {
// console.log('\(^o^)/');
$result = $("#comment_cards"),
comment_cards = [];
$.each(comments, function(index, comment) {
comment_card =
`<div id="comment_id_${comment.id}" data-comments_index="${index}" class="card mt-3">
<div class="card-header bg-white py-1 row no-gutters align-items-center">
<strong class="text-muted">${ comment.user.name }</strong>`;
if(comment.user_id == auth_id ){
comment_card +=
`<a href="javascript:void(0)" class="badge badge-pill badge-warning position-absolute text-danger"
style="right:200px;top:-10px; padding:10px;font-size:15px" id="edit_form_btn" data-toggle="modal">
編集
</a>
<a href="javascript:void(0)" class="delete_btn badge badge-pill badge-danger position-absolute text-warning"
style="right:100px;top:-10px; padding:10px;font-size:15px">
削除
</a>`;
}
comment_card +=
`<small class="text-muted ml-auto diff_in_time">${ comment.diff_in_time }</small>
</div>
<div class="card-body content">
${ comment.content }
</div>
</div>`;
comment_cards.push(comment_card);
});
$result[0].innerHTML = comment_cards.join("");
}
</script>
@endpush
<div class="card mt-2">
<div class="card-body pb-1">
<div class="form-group row no-gutters">
<label for="content" class="col-6 col-form-label">コメント</label>
<div class="col-6 text-right">
<button id="create_form_btn" type="submit" class="btn btn-primary">
コメント
</button>
</div>
<div class="col-12 mt-2">
<textarea id="create_content" name="content" class="form-control mb-2">
</textarea>
</div>
</div>
</div>
</div>
@push('js')
<script>
$(document).on('click', '#create_form_btn', function() {
content = $('#create_content').val();
$.ajax({
url: "{{ route('comments.store') }}",
type: 'POST',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
data: {
'content': content,
'post_id': post_id_page,
},
timeout: 6000, //タイムアウトの設定
}).done(function(data) {
$('#create_content').val('');
//返り値をcommentsに追加
comments.push(data);
//getComments(comments);でhtmlを再形成
getComments(comments);
}).always(function(){
content = '';
});
});
</script>
@endpush
<!-- モーダル -->
<div class="modal" id="edit_form" tabindex="-1" role="dialog" aria-labelledby="edit_formlLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<!-- モーダルのヘッダー -->
<div class="modal-header">
<!-- モーダルのタイトル -->
<h5 class="modal-title" id="edit_formlLabel">コンテンツ編集</h5>
<!-- 閉じるアイコン -->
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">×</span></button>
</div>
<!-- モーダルのボディー -->
<div class="modal-body row no-gutters">
<textarea name="content" id="edit_content" class="col-12"></textarea>
<button type="button" id="edit_content_btn" class="btn btn-warning ml-auto mt-2"
data-dismiss="modal">編集</button>
</div>
</div>
</div>
</div>
</div>
@push('js')
<script>
$(document).on('click', '#edit_form_btn', function() {
card_data = $(this).parents('.card');
p = card_data.find('.card-body').text().trim();
comment_index = card_data.data('comments_index');
comment = comments[comment_index];
console.log(comment);
$('#edit_form').modal('show');
$('#edit_content').val(p).focus();
});
$(document).on('click', '#edit_content_btn', function() {
content = $('#edit_content').val();
comment_id = comment.id;
post_id = comment.post_id;
user_id = comment.user_id;
$.ajax({
url: "{{ route('comments.update', 0) }}",
type: 'PUT',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
data: {
'id': comment_id,
'content': content,
'post_id': post_id,
'user_id': user_id,
},
timeout: 6000, //タイムアウトの設定
}).done(function(data) {
//commentsのデータを返り値で上書きする。
comments[comment_index] = data;
//上書きしたcommentsでhtmlを再形成している。
getComments(comments);
edit_data_id = '';
comment='';
console.log('comment:', comment);
console.log('comments', comments[comment_index]);
}).always(function() {
card_data = '';
p = '';
content = '';
comment_id = '';
comment_data = '';
post_id = '';
user_id = '';
});
});
</script>
@endpush
ここで、commentsのCRUDの確認。投稿、編集、削除の確認。
###ajax送信時のバリデーション機能を追加する。
errorの返り値をどうhtmlに反映させる。
フォームリクエストの作成
php artisan make:request CommentRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CommentRequest extends FormRequest
{
public function authorize()
{
//trueに変更
return true;
}
public function rules()
{
return [
//+
'content' =>['required'],
];
}
}
//+
use App\Http\Requests\CommentRequest;
//修正
public function store(CommentRequest $request)
public function update(CommentRequest $request)
//+
.fail(function(jqXHR, textStatus, errorThrown) {
console.log(jqXHR.responseJSON.errors)
})
@push('js')
<script>
$(document).on('click', '#create_form_btn', function() {
content = $('#create_content').val();
$.ajax({
url: "{{ route('comments.store') }}",
type: 'POST',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
data: {
'content': content,
'post_id': post_id_page,
},
timeout: 6000, //タイムアウトの設定
}).done(function(data) {
$('#create_content').val('');
//+error関係を消去している。
$('#create_content').removeClass('is-invalid');
$.each($('.error_msg'),function(index,element){
element.remove();
});
comments.push(data);
getComments(comments);
//+
}).fail(function(jqXHR) {
errors = jqXHR.responseJSON.errors;
console.log(errors);
$.each(errors, function(error, error_msgs) {
$.each(error_msgs, function(index, error_msg) {
err =`<span class="error_msg text-danger d-block"><strong>${error_msg}</strong></span>`
$(`[name="${error}"]`).after(err);
})
})
$('#create_content').addClass('is-invalid')
}).always(function() {
content = '';
});
});
</script>
@endpush
空のリクエストして、バリデーションを確認する。
validationに 'min:1'を追加して複数のバリデーションエラーを確認する。
'content' =>['min:1','required',],
タグ機能を実装する。
このアプリで一番面白いところだと思う。
ポストに複数のタグを貼り付けて、検索できるようにする。
検索機能は後回しにして、タグ付けできるようにする。
// $request->contentから正規表現で#tagを抽出
//結果を$match[1]の配列で取得できる。
preg_match_all('/#([a-zA-Z0-90-9ぁ-んァ-ヶー一-龠]+)/u', $request->content, $match);
//dd($match);//画像上
//tag_idを保管する箱
$tag_ids = [];
//マッチした配列をtagsテーブルに保存
foreach ($match[1] as $tag) {
//同名の名前がなければ、createして、createしたデータを返してくれる。
$tag = Tag::firstOrCreate(['tag_name' => $tag]);
//createしたidを配列化する。
array_push($tag_ids, $tag->id);
}
//post_tag(中間)テーブルに保存する関数を使って先程のids配列を保存する。
//https://www.wakuwakubank.com/posts/387-laravel-relation-3/#%E5%A4%9A%E5%AF%BE%E5%A4%9A%E9%96%A2%E4%BF%82%E3%81%AE%E7%B4%90%E4%BB%98%E3%81%91
$post->tags()->attach($tag_ids);
tagを保存するTagモデルとtagsテーブルを作成する。
php artisan make:model Tag -a
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->id();
//+
$table->string('tag_name');
$table->timestamps();
});
}
class Tag extends Model
{
use HasFactory;
//+
protected $fillable = [
'tag_name',
];
}
postsとtagsは多対多の関係であるため、中間テーブルを作成する。
テーブル名はlaravelの命名規則ではアルファベット順で単数形
php artisan make:migration create_post_tag_table
Schema::create('tag_post', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->foreignId('tag_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->timestamps();
});
php artisan migrate
Postモデルの多対多のリレーション
public function tags()
{
return $this->belongsToMany(Tag::class);
}
Postコントローラーのstore()を修正する。
//+
use App\Models\Tag;
public function store(PostRequest $request)
{
if ($request->has('cutomfile')) {
$img_path = uniqid("tmp_img_") . "/";
$request->merge(
['img_path' => $img_path],
);
foreach ($request->file('cutomfile') as $key => $img_file) {
//ファイルが存在しているかに付け加え、isValidメソッドで問題なくアップロードできたのかを確認できたなら
if ($img_file->isValid()) {
//画像なら
if (Str::is('image*', $img_file->getMimeType())) {
//pathを作成(path/+ ファイル名(空白は '_' に置き換えている))
$img_files_path = $img_path . Str::of($img_file->getClientOriginalName())->replace(' ', '_');
// storage\appからの相対パス 画像を保存している。//取得はsrc="{{ asset('storage/'.$tmp_file)
$img_file->storeAs("public", $img_files_path);
$img_files_path = '';
}
}
}
}
$request->merge(
['user_id' => Auth::id()]
);
//+正規表現でマッチするデータを配列で取得
preg_match_all('/#([a-zA-Z0-90-9ぁ-んァ-ヶー一-龠]+)/u', $request->content, $match);
$tag_ids = [];
foreach ($match[1] as $tag) {
//同名の名前がなければ、createして、createしたデータを返してくれる。
$tag = Tag::firstOrCreate(['tag_name' => $tag]);
array_push($tag_ids, $tag->id);
}
//修正
$post = Post::create($request->except('cutomfile'));
//+ リレーションすることで、中間テーブルにデータを保存する。
$post->tags()->attach($tag_ids);
return redirect()->route('posts.index')->with('status', ['success' => '投稿完了']);
}
tagsテーブルとpost_tagにしっかりと保存されている。
###タグを表示する。
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<x-flashMessage />
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>投稿一覧</span>
<a href="{{ route('posts.create') }}">投稿</a>
</div>
<div class="card-body overflow-auto">
@foreach ($posts as $post)
<div class="
card mt-1">
<div class="card-body">
<h5 class="card-title">{{ $post->title }}</h5>
{{-- テーブルに修正した。 --}}
<table class="table table-hover">
<tr>
<th>投稿者</th>
<td><a href="" class="card-text">{{ $post->user->name }}</a></td>
</tr>
<tr>
<th>カテゴリー</th>
<td><a href="" class="card-text">{{ $post->category->name }}</a></td>
</tr>
<tr>
<th>タグ</th>
<td class="">
@foreach ($post->tags as $tag)
<a href="" class="card-text mr-1">#{{ $tag->tag_name }}</a>
@endforeach
</tr>
</table>
<p class="card-text mt-1">{!! nl2br(e($post->content)) !!}</p>
<div class="d-flex justify-content-between">
<a href="{{ route('posts.show', $post) }}" class="btn btn-primary">詳細 コメントを投稿</a>
@can('isPost', $post)
{{-- @isPost($post) --}}
<a class="btn btn-danger " href="javascript:void(0)"
onclick="deletePost({{ $post->id }}); this.onclick=null;">
削除
</a>
{{-- @endisPost --}}
@endcan
</div>
</div>
</div>
@endforeach
<form action="" method="POST" id="delete_form" class="d-none">
@csrf
@method('delete')
</form>
</form>
</div>
{{ $posts->links() }}
</div>
</div>
</div>
</div>
@endsection
@push('js')
<script>
function deletePost(post_id) {
url = `{{ route('posts.destroy', '') }}/${post_id}`;
$('#delete_form').attr('action', url).submit();
}
</script>
@endpush
タグが表示され、テーブルにしたことで見た目も改善された。
クエリが繰り返し発行されている。N+1問題が生じている。
public function index()
{
// $posts = Post::with(['category','user'])->get();
//tagsを追加するだけ。
$posts = Post::with(['category', 'user','tags'])->paginate(3);
// dd($posts);
return view('posts.index', compact('posts'));
}
select 省略 from tags
の発行回数が3回から1回になった。

名前、カテゴリー、タグ、テキスト検索機能を実装する。
###投稿者検索
投稿者のリンクから、ユーザーの投稿一覧とコメント一覧を取得する。
user.showを作成する。
・プロフィール
・投稿一覧
・コメント一覧
ルートを作成する。
Route::middleware(['auth'])->group(function () {
Route::get('/', [App\Http\Controllers\PostController::class, 'index'])->name('posts.index');
Route::resource('/posts', App\Http\Controllers\PostController::class,['except'=>'index']);
Route::resource('/comments', App\Http\Controllers\CommentController::class);
//+
Route::resource('/users', App\Http\Controllers\UserController::class);
});
{{-- テーブルに修正した。 --}}
<table class="table table-hover">
<tr>
<th>投稿者</th>
{{-- +{{ route('users.show',$post->user) }} --}}
<td><a href="{{ route('users.show',$post->user) }}" class="card-text">{{ $post->user->name }}</a></td>
</tr>
<tr>
<th>カテゴリー</th>
{{-- +{{ route('categories.show',$post->category) }} --}}
<td><a href="{{ route('categories.show',$post->category) }}" class="card-text">{{ $post->category->name }}</a></td>
</tr>
<tr>
<th>タグ</th>
<td class="">
@foreach ($post->tags as $tag)
{{-- +{{ route('tags.show',$tag) }} --}}
<a href="{{ route('tags.show',$tag) }}" class="card-text mr-1">#{{ $tag->tag_name }}</a>
@endforeach
</tr>
</table>
UserControllerを作成する。
php artisan make:controller UserController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
//+
use App\Models\User;
class UserController extends Controller
{
public function show(User $user)
{
//N+1対策 $user(1)->comments(多), $comment(1)->post(1)
// ↑(ココでN+1問題が生じる 1対1を繰り返すため)
$user = $user->load(['comments.user','comments.post']);
//苦肉の策 $user = $user->load(['posts.comments'])->loadCount('posts.comments')が出来なかった。
//$user->posts()でリレーション先のpostモデルに変換している。
// dd($user->load('posts')); //----\(^o^)/画像を見てくれ
// dd($user->posts()->get()); //----(^O^) 画像を見てくれよ
$posts = $user->posts()->withCount('comments')->get();
return view('users.show', compact('user', 'posts'));
}
}
dd($user->load('posts'));
Userモデルは変更されずに、リレーションとしてpostsを取得
UserモデルからPostモデルへ変換されてpostsを取得 モデルが変わっている
$user = $user->load('posts');
$post = $user->posts()->get();
dd($user->posts();
dd($user->posts()->getParent()); //----そのままuserモデル
dd($user->posts()->get()); //----ポストモデルに変換
補足画像
**create:
**users.show.blade.phpの作成
https://v4.bootstrap-guide.com/javascript/tabs/active
@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">{{ $user->name }}</div>
<div class="card-body">
<x-flashMessage></x-flashMessage>
<!-- タブ部分 -->
<ul id="myTab" class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a href="#home" id="home-tab" class="nav-link active" role="tab" data-toggle="tab"
aria-controls="home" aria-selected="true">プロフィール</a>
</li>
<li class="nav-item" role="presentation">
<a href="#profile" id="profile-tab" class="nav-link" role="tab" data-toggle="tab"
aria-controls="profile" aria-selected="false">投稿一覧</a>
</li>
<li class="nav-item" role="presentation">
<a href="#contact" id="contact-tab" class="nav-link" role="tab" data-toggle="tab"
aria-controls="contact" aria-selected="false">コメント一覧</a>
</li>
</ul>
<!-- パネル部分 -->
<div id="myTabContent" class="tab-content mt-3">
<div id="home" class="tab-pane active" role="tabpanel" aria-labelledby="home-tab">
<ul class="list-group list-group-flush">
<li class="list-group-item">ID : {{ $user->id }}</li>
<li class="list-group-item">NAME :{{ $user->name }}</li>
<li class="list-group-item">EMAIL:{{ $user->email }}</li>
</ul>
</div>
<div id="profile" class="tab-pane" role="tabpanel" aria-labelledby="profile-tab">
<x-posts.index :posts="$posts" />
</div>
<div id="contact" class="tab-pane" role="tabpanel" aria-labelledby="contact-tab">
<x-comment.index :post="$user" type="userShow" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
**create:
**components\posts\index.blade.phpの作成
<table class="table table-sm">
<thead>
<tr>
<th>タイトル</th>
<th class="text-center">コメント数</th>
</tr>
</thead>
<tbody>
@foreach ($posts as $post)
<tr>
<td>
<a href="{{ route('posts.show', $post) }}" class="text-secondary list-group-item border-0">
{{ $post->title }}
</a>
</td>
<td class="text-center">
<a href="" class="text-secondary list-group-item border-0">{{ $post->comments_count }}</a>
</td>
</tr>
@endforeach
</tbody>
</table>
**edit:
**components\comment\index.blade.phpの編集
<div id="comment_cards">
</div>
<x-comment.edit />
@push('js')
<script>
//親元のコンポーネントにデータを置いておけば、
//子コンポーネントにわざわざ渡さたなくても利用できる。
comments = @json($post->comments);
auth_id = {{ auth()->id() }};
post_id_page = {{$post->id}};
//+
post_uri = "{{ route('posts.show', '') }}";
//これで、commentsデータをhtml化して上に貼り付けている。
if (comments.length > 0) {
getComments(comments);
}
$(document).on('click', '.delete_btn', function() {
comments_index = $(this).parents('.card').data('comments_index');
comment_id = comments[comments_index].id;
$.ajax({
url: "{{ route('comments.destroy', 0) }}",
type: 'DELETE', //DELETE送信
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
data: {
id: comment_id
},
timeout: 6000, //タイムアウトの設定
}).done(function() {
comments.splice(comments_index,1);
getComments(comments);
comments_index='';
}).always(function() {
comment_id = '';
});
});
function getComments(comments) {
// console.log('\(^o^)/');
$result = $("#comment_cards"),
comment_cards = [];
$.each(comments, function(index, comment) {
comment_card =
`<div id="comment_id_${comment.id}" data-comments_index="${index}" class="card mt-3">
<div class="card-header bg-white py-1 row no-gutters align-items-center">
<strong class="text-muted">${ comment.user.name }</strong>`;
if(comment.user_id == auth_id ){
comment_card +=
`<a href="javascript:void(0)" class="badge badge-pill badge-warning position-absolute text-danger"
style="right:200px;top:-10px; padding:10px;font-size:15px" id="edit_form_btn" data-toggle="modal">
編集
</a>
<a href="javascript:void(0)" class="delete_btn badge badge-pill badge-danger position-absolute text-warning"
style="right:100px;top:-10px; padding:10px;font-size:15px">
削除
</a>`;
}
comment_card +=
`<small class="text-muted ml-auto diff_in_time">${ comment.diff_in_time }</small>
</div>
<div class="card-body content">
${ comment.content }
</div>
</div>`;
comment_cards.push(comment_card);
});
$result[0].innerHTML = comment_cards.join("");
}
//+
@isset($type)
@if ($type=='userShow')
$(document).on('mouseenter',".card-body.content", function() {
$(this).css('cursor', 'pointer');
$(document).on('click', ".card-body.content", function() {
comments_index = $(this).parents("[id ^= 'comment_id_' ]").data('comments_index');
post_id = String(comments[comments_index].post_id);
// console.log(comment_id);
location.href = `${post_uri}/${post_id}`;
});
});
@endif
@endisset
</script>
@endpush
投稿者のリンクから
・投稿者のプロフィール
・投稿一覧
・コメント一覧
を確認する。
###カテゴリー検索
カテゴリーの検索から、ポスト一覧を取得する。
categories.show()を作成する。
ルートは追加する。
//+
Route::resource('/categories',App\Http\Controllers\CategoryController::class);
CategoryControllerを作成する。
php artisan make:controller CategoryController -r
//先程、孫のloadCount()ができなかったので(´・ω・`)してたら、できた。
$category->load('posts')->loadCount('posts.comments');//これは駄目。
//モデルでWithCount()してやればいいだけだった。
return $this->hasMany(Post::class,'category_id','id')->withCount('comments');
public function posts()
{
//修正->withCount('comments')を追加
return $this->hasMany(Post::class,'category_id','id')->withCount('comments');
}
public function show(Category $category)
{
//N+1問題は生じないためloadは必要がない。
//$category->posts $categoryで繰り返しが生じていないため。
return view('categories.show',compact('category'));
}
create:
resources\views\categories\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">{{ $category->name }}</div>
<div class="card-body">
<x-flashMessage />
<x-posts.index :posts="$category->posts" />
</div>
</div>
</div>
</div>
</div>
@endsection
#####カテゴリーの追加、編集、削除を実装する。
カテゴリーを削除したら、相手先の
postsのcategory_idはnull値に変更する。
今のテーブル構造では
categoryが変更されたら、リレーション先も変更されるし、
変更:categoryが削除されたらリレーション先のpostも削除される。
変更:category_idはnull値が禁止されている。
$table->foreignId('foreign_id')->nullable()->constrained("table_name")->cascadeOnUpdate()->nullOnDelete();
変更前
カテゴリーIDを変更してみる。
php artisan tinker
$category = Category::find(1);
$category->update(['id'=>10]);
あれ、変更されない。なんでや?
変更された。update()やfill()では主キーのidの更新はできないみたい。
DB::update('update categories set id = 10 where id = 1');
categoriesのidを1から10に変更
リレーション先postsのcategory_idも1から10に変更されている。
削除してみる。
$category = Category::find(1);
$category ->delete();
postsのcategories_id 10番も全部消えた。
↑これを消去するのではなく、null値に変更するだけ。
外部キー関係はあとからテーブル変更するのは大変なので、
foreign_keyが残るためとか、色々原因がある。ので、作り直した方が
絶対にいいと思う。くだらんことで時間を無駄にしたらアカン。
テーブルを再編集する。
public function up()
{
//外部キー制約を無効
Schema::disableForeignKeyConstraints();
Schema::create('posts', function (Blueprint $table) {
$table->id();
//not nullableでリレーション先のrecordが更新されたら更新、削除されたら削除
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
$table->foreignId('category_id')
->nullable()
->constrained()
->nullOnDelete()
->cascadeOnUpdate();
$table->string('title');
$table->text('content');
$table->timestamps();
});
//外部キー制約を有効
Schema::enableForeignKeyConstraints();
}
php artisan migrate:fresh
php artisan db:seed
php artisan tinker
$category = Category::find(1);
$category ->delete();
Post::where('category_id',null)->count();
Post::where('category_id',null)->get();
今度は削除されずに null値 がセットされた。
null値になったのでエラーが出たので、デフォルト値を設定する。
public function category()
{
//デフォルト値を設定
return $this->belongsTo(Category::class,'category_id','id')->withDefault([
'id'=>'', 'name' => 'なし',
]);
}
エラーが回避された。
これで、カテゴリーの追加と編集と削除の準備が整ったので、実装していく。
###カテゴリーを追加する。
categories.create と categories.storeの作成
リンクを作成する
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
<!-- + -->
<li>
<a class="btn btn-primary"
href="{{ route('categories.create') }}">
カテゴリー
</a>
</li>
</ul>
//+
public function create()
{
$categories = Category::all();
return view('categories.create', compact('categories'));
}
//+
public function store(Request $request)
{
Category::create($request->all());
return redirect()->route('categories.create')->with('status',['success'=>'カテゴリーを追加しました。']);
}
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<x-flashMessage></x-flashMessage>
<div class="card">
<div class="card-header">カテゴリーを追加</div>
<div class="card-body">
<x-categories.from/>
</div>
</div>
</div>
</div>
</div>
@endsection
コンポーネントを作成する
resources\views\components\categories
・form.blade.php
@php
$url = route('categories.create');
$method = 'POST';
@endphp
<form method="POST" action="{{ $url }}" novalidate>
@method($method)
@csrf
<div class="form-group row no-gutters">
<label for="name" class="col-form-label text-md-right">カテゴリーネーム</label>
<button type="submit" class="btn btn-primary ml-auto">
追加
</button>
<div class="col-12 mt-2">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name"
value="{{ old('name', $category->name??'') }}" required autocomplete="name" autofocus>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
</form>
バリデーションを実装する。
//+
use Illuminate\Support\Facades\Validator;
public function store(Request $request)
{
//+
//validateメソッドによるへリダイレクト
$validator = Validator::make($request->all(), [
'name' => ['required', 'unique:categories'],
]);
if ($validator->fails()) {
return redirect()->route('categories.create')
->withErrors($validator)
->withInput();
}
Category::create($request->all());
return redirect()->route('categories.create')->with('status', ['success' => 'カテゴリーを追加しました。']);
}
編集と削除 categories.edit()とcategories.deleteを実装する
//+
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
//+
public function update(Request $request, Category $category)
{
//validateメソッドによるへリダイレクト
$validator = Validator::make($request->all(), [
'name' => ['required', 'unique:categories'],
]);
if ($validator->fails()) {
return redirect()->route('categories.edit',$category)
->with('status', ['danger' => 'バリデーションに失敗しました。'])
->withErrors($validator)
->withInput();
}
Category::updateOrCreate(['id' => $category->id], $request->all());
return redirect()->route('categories.create')->with('status', ['success' => 'カテゴリーを編集しました。']);
}
//+
public function destroy(Category $category)
{
$category->delete();
return redirect()->route('categories.create')->with('status', ['success' => 'カテゴリーを削除しました。']);
}
編集ボタンと削除ボタン、jsの追加
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<x-flashMessage></x-flashMessage>
<div class="card">
<div class="card-header">カテゴリーを追加</div>
<div class="card-body">
<x-categories.from/>
{{-- + --}}
<x-categories.index :categories="$categories"/>
</div>
</div>
</div>
</div>
</div>
@endsection
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<x-flashMessage></x-flashMessage>
<div class="card">
<div class="card-header">カテゴリーを編集</div>
<div class="card-body">
<x-categories.form :category="$category" type="edit"/>
</div>
</div>
</div>
</div>
</div>
@endsection
コンポーネントを作成する
resources\views\components\categories
・index.blade.php
・(修正)form.blade.php
<table class="table">
<tbody>
@foreach ($categories as $category)
<tr>
<td class="w-50">
<a class="list-group-item" href="{{ route('categories.show', $category) }}">
{{ $category->name }}
</a>
</td>
<td class="w-25">
<a class="list-group-item"
href="{{ route('categories.edit', $category) }}">編集</a>
</td>
<td class="w-25">
<a class="list-group-item" href="javascript:void(0)"
onclick="deleteCategory({{ $category->id }}); this.onclick=null;">
削除
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
<form id="delete-form" action="" method="POST" class="d-none">
@method('delete')
@csrf
</form>
@push('js')
<script>
url = "{{ route('categories.destroy','') }}"
function deleteCategory(category_id) {
$('#delete-form').attr('action',`${url}/${category_id}`).submit();
}
</script>
@endpush
@php
//修正
if (isset($type) == 'edit') {
$url = route('categories.update',$category);
$method = 'PUT';
}else{
$url = route('categories.create');
$method = 'POST';
}
@endphp
categories.create
categoriesのCRUDとバリデーションを確認する
###タグ検索機能を実装する。
tags.showを作成して、posts一覧を取得する。
ルートを追加する。
//+
Route::resource('/tags', App\Http\Controllers\TagController::class);
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
use HasFactory;
protected $fillable = [
'tag_name',
];
//+ 多対多のリレーション
public function posts(){
return $this->belongsToMany(Post::class)->withCount('comments');
//2番目の引数は、中間テーブル名
//3番目の引数は、検索する外部キー,$tag->posts() tag_idでpostsを検索している。
//4番目の引数は、関連付けるモデルの外部キー名 post_idと紐付けろ
//(例えばuser_idではなく)。
// return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
// return $this->belongsToMany(Post::class,'post_tag','tag_id','post_id');
}
}
Tagcontroller
//+
public function show(Tag $tag)
{
//N+1問題はおきない。多対多のリレーション $tag->posts $tagで繰り返しがない。
return view('tags.show',compact('tag'));
}
blade
@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">{{ $tag->name }}</div>
<div class="card-body">
<x-flashMessage />
<x-posts.index :posts="$tag->posts"/>
</div>
</div>
</div>
</div>
</div>
@endsection
投稿でタグを追加して、投稿一覧が取得できるか確認する。
コメント数も取得できているか確認する。
テキスト検索機能を実装する
Route::get('/', [App\Http\Controllers\PostController::class, 'index'])->name('posts.index');
Route::resource('/posts', App\Http\Controllers\PostController::class, ['except' => 'index']);
Route::resource('/comments', App\Http\Controllers\CommentController::class);
Route::resource('/users', App\Http\Controllers\UserController::class);
Route::resource('/categories', App\Http\Controllers\CategoryController::class);
Route::resource('/tags', App\Http\Controllers\TagController::class);
//+
Route::get('/search', [App\Http\Controllers\PostController::class, 'search'])->name('search');
ポストのタイトル、コンテンツ、
コメントのコンテンツを
検索する。
ナビバーに検索フォームを追加する。
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- + -->
<form class="form-inline mr-3" action="{{ route('search') }}" method="get">
<input class="form-control mr-sm-2" type="search" name="text_search">
<button type="submit" class="btn btn-outline-success my-2 my-sm-0">検索</button>
</form>
<!-- Authentication Links -->
@guest
//+
// 部分一致検索
public function scopeWhereLike($query, string $column, string $keyword)
{
return $query->where($column, 'like', '%' . addcslashes($keyword, '%_\\') . '%');
}
// 前方一致検索
public function scopeWhereLikeForward($query, string $column, string $keyword)
{
return $query->where($column, 'like', addcslashes($keyword, '%_\\') . '%');
}
お前らのLike検索は間違っている
php artisan make:trait TestTrait
できないっていうかない。
type nul > app\Models\Traits\WhereLike.php
空のファイルが作成されるので、
namespaceを忘れずに、
相対パスが、app\Models\Traits\WhereLike.phpなら、
namespaceは、App\Models\Traits;
trait 名前{ ここにスコープ関数をかくだけ。 }
<?php
namespace App\Models\Traits;
trait WhereLike
{
// 部分一致検索
public function scopeWhereLike($query, string $column, string $keyword)
{
return $query->where($column, 'like', '%' . addcslashes($keyword, '%_\\') . '%');
}
// 前方一致検索
public function scopeWhereLikeForward($query, string $column, string $keyword)
{
return $query->where($column, 'like', addcslashes($keyword, '%_\\') . '%');
}
}
あとは、使いたいモデルでuseするだけ。
//+
use App\Models\Traits\WhereLike;
class Post extends Model
{
//+
use HasFactory,WhereLike;
以上でWhereLikeの関数がモデルで使えるようになる。
Post::whereLike('name','山田')->get()
public function search(Request $request)
{
$keywords = $request->text_search ?? '';
$posts = Post::whereLike('content', $keywords)
->orWhere(function ($q) use ($keywords) {
$q->whereLike('title', $keywords);
})
//コメントも含めて検索する場合
/* 使用する場合、traitをCommentモデルにも追加する必要がある。
->orWhere(function ($q) use ($keywords) {
$comments_ids = Comment::whereLike('content', $keywords)->pluck('post_id');
$q->whereIn('id', $comments_ids);
})
*/
//)->withQueryString()検索クエリを持ち越してくれる。
->paginate(3)->withQueryString();
//dd($request->text_search);
//取得したposts一覧をposts.indexで表示してやる。
return view('posts.index', compact('posts', 'keywords'));
}
->withQueryString()
がない場合。
1ページ目のuriは ?text_search=''とクエリがある。
ページネーションも7件。
2ページ目をクリックすると、uriからクエリがクリアーされている。
ページネーションもクリアーされて、絞り込みも解除されている。
->withQueryString()がある場合。
2ページ以降もクエリーがクリアーされないで、繰り越してくれる。
クエリー文字列は dd($request->text_search);で取得できる。
{{-- + cardの上にコンポーネントを追記 --}}
@isset($keywords)
<x-search_result :paginator="$posts" :keywords="$keywords"/>
@endisset
<x-flashMessage/>
<div class="card">
コンポーネントの作成
・search_result.blade.php
{{-- 公式サイト ページネーション https://readouble.com/laravel/8.x/ja/pagination.html --}}
{{-- bootstrap4のalertをコピペ編集 --}}
<div id="search_msg" class="alert alert-warning alert-dismissible fade show" role="alert">
<p class="mb-0 py-1"><strong>検索キーワード:</strong>{{ $keywords }}</p>
{{-- $posts を $paginator に変更しています。component受け渡し時 --}}
<p class="mb-0 py-1"><strong>総件数:</strong>{{ $paginator->total() }} 件</p>
<button type="button" class="close" data-dismiss="alert" aria-label="閉じる">
<span aria-hidden="true">×</span>
</button>
</div>
@push('js')
<script>
// alertbuttonが閉じたら、route('posts.index')に遷移
$('#search_msg').on('close.bs.alert', function() {
window.location.href = "{{ route('posts.index') }}"
})
</script>
@endpush
スペースで、AND検索を実装する。
public function search(Request $request)
{
// 空の時はリターンさせる。
if (is_null($request->text_search)) {
return redirect()-> route('posts.index');
}
// 検索文字列全体の前後にある空白を除去
$keywords = preg_replace( '/\A[\p{C}\p{Z}]++|[\p{C}\p{Z}]++\z/u', '', $request->text_search);
// 1、空白を半角空白に統一する
$keywords = mb_convert_kana($keywords, 's');
// 2、半角空白で文字を区切り、配列にする。
$keywords_array = preg_split('/[\s]+/', $keywords);
// 3、配列をforeachで回し、それぞれ (WhereLike + orWhere) +(and)+ (WhereLike + orWhere)+(and)+・・・・
foreach($keywords_array as $keyword){
$posts = Post::whereLike('content', $keyword)
->orWhere(function ($q) use ($keyword) {
$q->whereLike('title', $keyword);
})
//コメントのcontent検索
/*
->orWhere(function ($q) use ($keywords) {
$comments_ids = Comment::whereLike('content', $keywords)->pluck('post_id');
$q->whereIn('id', $comments_ids);
})
*/
->paginate(3)->withQueryString();
}
return view('posts.index', compact('posts', 'keywords'));
}