今回は前回に続けて開発を進めていきましょう。
前章をまだご覧になっていない方は以下から開発を始めましょう。
Contents
12 章:管理画面作成
ではまず、管理画面でできる機能について整理していきましょう。
- スレッド一覧、紐づくコメントを全て閲覧できる。
- 不適切なスレッド、コメントを削除することができる。
一覧画面、詳細画面を表示する。
それではまずは、一覧画面、及び詳細画面を表示していきますが、流れを確認していきましょう。
- Admin\ThreadController.php を既存の ThreadController.php を複製する形で作成。
- 上記で作成した、Controller へ Admin のミドルウェアを設置
- web.php にルーティング作成
Blade に関しては既存のものを再利用していきます。
Admin\ThreadController.php 新規作成 <?php namespace App\Http\Controllers\Admin; use App\Repositories\ThreadRepository; use App\Http\Controllers\Controller; use App\Services\ThreadService; use Exception; class ThreadController extends Controller { /** * The ThreadService implementation. * * @var ThreadService */ protected $thread_service; /** * The ThreadRepository implementation. * * @var ThreadRepository */ protected $thread_repository; /** * Create a new controller instance. * * @param ThreadService $thread_service * @return void */ public function __construct( ThreadService $thread_service, ThreadRepository $thread_repository ) { $this->middleware('auth:admin'); $this->thread_service = $thread_service; $this->thread_repository = $thread_repository; } /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $threads = $this->thread_service->getThreads(3); $threads->load('messages.user', 'messages.images'); return view('threads.index', compact('threads')); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { $thread = $this->thread_repository->findById($id); $thread->load('messages.user', 'messages.images'); return view('threads.show', compact('thread')); } }
上記は既存の ThreadController.php を複製して変更を加えました。
変更箇所は、constructor です。Admin 側のコントローラであるため、 Adminのミドルウェアをつけましょう。
では次に、web.php にルーティングを記載していきましょう。
web.php // 略 Route::group(['prefix' => 'admin', 'middleware' => 'auth:admin', 'as' => 'admin.'], function () { Route::post('logout', 'Admin\LoginController@logout')->name('logout'); Route::get('home', 'Admin\HomeController@index')->name('home'); Route::resource('/threads', 'Admin\ThreadController')->except(['create', 'store', 'update']); });
それでは次に、Admin がログインした後に /admin/threads に画面遷移をするように変更をしましょう。
Admin がログインした後に /admin/threads に画面遷移するような実装をしましょう。
それでは解答です。
RouteServiceProvider.php /** * The path to the "admin/home" route for your application. * * @var string */ public const ADMIN = '/admin/home'; // 変更前 public const ADMIN = '/admin/threads'; // 変更後
Admin がスレッドを削除出来る
現在、Admin ログインをすると、/admin/threads にリダイレクトされ以下のような画面を確認することができます。

今回は、これらの投稿を Admin が削除できるように実装していきます。
まずは、削除をするビジネスロジックを実装していきます。
ThreadRepository.php // 略 /** * Delete thread from id * * @param integer $id * @return void */ public function deleteThread(int $id) { $thread = $this->findById($id); return $thread->delete(); }
Admin/ThreadController // 略 /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { try { $this->thread_repository->deleteThread($id); } catch (Exception $error) { return redirect()->route('admin.threads.index')->with('error', 'スレッドの削除に失敗しました。'); } return redirect()->route('admin.threads.index')->with('success', 'スレッドの削除に失敗しました。'); }
上記で、削除に関するビジネスロジックの実装が完了しました。
では、実際に削除ボタンを実装していきます。
message-create.blade.php <form method="POST" action="{{ route('messages.store', $thread->id) }}" enctype="multipart/form-data" class="mb-1"> @csrf <div class="form-group"> <label for="thread-first-content">内容</label> <textarea name="body" class="form-control" id="thread-first-content" rows="3" required></textarea> </div> <div class="form-group"> <label for="message-images">画像</label> <input type="file" class="form-control-file" id="message-images" name="images[]" multiple> </div> <button type="submit" class="btn btn-primary">書き込む</button> </form> <form action="{{ route('admin.threads.destroy', $thread->id) }}" method="post" class="mb-4"> @csrf @method('DELETE') <input type="submit" class="btn btn-danger" value="削除" onclick="return confirm('スレッドを削除します。本当に実行してよろしいでしょうか?')"> </form>
上記のコンポーネントに、削除用のコンポーネントを追加しました。
また、デザインを少し整えるために、margin を変更しております。
ここまでで、どのような表示になっているかを確認しましょう。


削除ボタンが実装できたな。
でもこれって、Admin ではなくても誰でも押せちゃうよな
その通りです。なので、これから Admin のみに削除ボタンを表示できるように変更していきましょう。
Admin のみに削除ボタンを表示
ログイン中のユーザー情報を取得するために、よく使う Auth::user() がありますね。
今回は、Auth::guard($guard)->check() を使用し、現在のログイン中のユーザーは、Admin なのか User なのかを判定します。
message-create.blade.php @if (Auth::guard('admin')->check()) <form action="{{ route('admin.threads.destroy', $thread->id) }}" method="post" class="mb-4"> @csrf @method('DELETE') <input type="submit" class="btn btn-danger" value="削除" onclick="return confirm('スレッドを削除します。本当に実行してよろしいでしょうか?')"> </form> @else <form method="POST" action="{{ route('messages.store', $thread->id) }}" enctype="multipart/form-data" class="mb-1"> @csrf <div class="form-group"> <label for="thread-first-content">内容</label> <textarea name="body" class="form-control" id="thread-first-content" rows="3" required></textarea> </div> <div class="form-group"> <label for="message-images">画像</label> <input type="file" class="form-control-file" id="message-images" name="images[]" multiple> </div> <button type="submit" class="btn btn-primary">書き込む</button> </form> @endif
こちらで完了です。ログイン中のユーザーが Admin でない場合には、Auth::guard(‘admin’)->check() が false を返し、削除ボタンの表示をしない実装にすることができます。
また、Admin にはメッセージ投稿は不要なので、表示をしないように条件分岐を行いました。
それでは削除してみましょう。

あれ、削除に失敗しました。と表示されるぞ?

① なぜ削除が失敗しているのか考えてみましょう
② 削除の原因を知るためにはどうすれば良いか考えてみましょう。

削除ボタン ⇨ Admin\ThreadController@destroy だから、削除の処理で何か不具合が出ているのかな。
では、どうやったら削除の原因(エラーメッセージ)を確認できるでしょうか。

destroy メソッドでエラーの際にリダイレクト処理をしていたね。
そこで dd でエラーを確認できそう。
Admin\ThreadController // 略 public function destroy($id) { try { $this->thread_repository->deleteThread($id); } catch (Exception $error) { dd($error); return redirect()->route('admin.threads.index')->with('error', 'スレッドの削除に失敗しました。'); } return redirect()->route('admin.threads.index')->with('success', 'スレッドの削除に失敗しました。'); }
そうですね。try catch の処理で受け取った $error を確認することでエラーメッセージを表示することが可能になります。
では実際に、もう一度削除ボタンを押してエラー内容を確認しましょう。

エラー画面が表示されました。ではなぜ、エラーが出ているのでしょうか。
外部キーで紐づくレコードを削除する

Thread を今回削除しているが、Thread には複数の Messages が存在する。
Message には thread_id という外部キーがあるため、Thread を削除すると、thread_id が存在しなくなり、エラーが出ているんではないか?
正解です。Message の thread_id は外部キーで、Thread を削除してしまうとデータの整合性がなくなってしまいます。そのため、エラーが出ていました。
では、解決方法として「外部キーで紐づいているレコードを同時に削除」するようにしましょう。
これをするには、マイグレーションファイルを修正する必要があります。
2021_04_13_081550_create_messages_table.php // 略 $table->foreign('thread_id')->references('id')->on('threads')->onDelete('cascade');
2021_04_13_081557_create_images_table.php // 略 $table->foreign('message_id')->references('id')->on('messages')->onDelete('cascade');
->onDelete(‘cascade’) を追加しました。
https://readouble.com/laravel/6.x/ja/migrations.html
こちらを追記することで、外部キーが参照するレコードが削除された際に同時に削除できるように設定しています。
それでは、マイグレーションファイルを修正しましたので migration を行いましょう。
laradock % docker-compose exec workspace php artisan migrate:fresh --seed
こちらで完了です。
fresh を行ったため、テストデータは全て消えてしまいました。また新しいテストデータを投稿してください。
テストデータを用意し、実際にスレッドが削除できるようになっているかを確認しよう。
詳細画面への遷移
現在、Admin画面から詳細画面へ遷移をすると、User のログイン画面に遷移してしまいます。



これは、index.blade.php のリンクがおかしい気がするな。
threads\index.blade.php // 略 <a href="{{ route('threads.show', $thread->id) }}">全部読む</a> <a href="{{ route('threads.show', $thread->id) }}">最新50</a> <a href="{{ route('threads.show', $thread->id) }}">1-100</a> <a href="{{ route('threads.index') }}">リロード</a>
正解です。こちらのリンクのアクション先が、Admin になっていませんね。
よって、user 側のログイン画面に遷移されています。
では、先ほどの guard を使用し、Admin ログイン際と User ログインの際の条件を分岐しましょう。
また、こちらもコンポーネント化をしていきます。
threads\index.blade.php // 略 // 変更前 <a href="{{ route('threads.show', $thread->id) }}">全部読む</a> <a href="{{ route('threads.show', $thread->id) }}">最新50</a> <a href="{{ route('threads.show', $thread->id) }}">1-100</a> <a href="{{ route('threads.index') }}">リロード</a> // 変更後 @include('components.show-links', compact('thread'))
components\show-links.blade.php @if (Auth::guard('admin')->check()) <a href="{{ route('admin.threads.show', $thread->id) }}">全部読む</a> <a href="{{ route('admin.threads.show', $thread->id) }}">最新50</a> <a href="{{ route('admin.threads.show', $thread->id) }}">1-100</a> <a href="{{ route('admin.threads.index') }}">リロード</a> @else <a href="{{ route('threads.show', $thread->id) }}">全部読む</a> <a href="{{ route('threads.show', $thread->id) }}">最新50</a> <a href="{{ route('threads.show', $thread->id) }}">1-100</a> <a href="{{ route('threads.index') }}">リロード</a> @endif
Admin がログイン中の場合と、User がログイン中のリンクの表示を変更しました。
また、show.blade.php にある 「掲示板に戻る」ボタンも同様に修正しましょう。
show.blade.php // 略 // 修正前 <a href="{{ route('threads.index') }}" class="btn btn-primary">掲示板に戻る</a> // 修正後 @include('components.thread-index-back')
components\thread-index-back.blade.php @if (Auth::guard('admin')->check()) <a href="{{ route('admin.threads.index') }}" class="btn btn-primary">掲示板に戻る</a> @else <a href="{{ route('threads.index') }}" class="btn btn-primary">掲示板に戻る</a> @endif
掲示板に戻るボタンについても、ログイン中の権限に基づいて、アクション先を変更しました。
メッセージを削除できるようにしよう。
それではメッセージを削除できるようにしましょう。
Admin がメッセージを削除できる実装に挑戦してみましょう。
MessageRepository.php // 略 /** * Find a message by id * * @param int $id * @return Message $thread */ public function findById(int $id) { return $this->message->find($id); } /** * Delete message from id * * @param integer $id * @return void */ public function deleteMessage(int $id) { $message = $this->findById($id); return $message->delete(); }
まずはリポジトリに削除のロジックを追加しました。
では次にコントローラで呼び出し、削除できるようにしましょう。
Admin\MessageController // 新規作成 <?php namespace App\Http\Controllers\Admin; use App\Thread; use App\Http\Controllers\Controller; use App\Repositories\MessageRepository; class MessageController extends Controller { /** * The MessageRepository implementation. * * @var MessageRepository */ protected $message_repository; /** * Create a new controller instance. * * @return void */ public function __construct( MessageRepository $message_repository ) { $this->middleware('auth:admin'); $this->message_repository = $message_repository; } /** * Remove the specified resource from storage. * * @param Thread $thread * @param int $id * @return \Illuminate\Http\Response */ public function destroy(Thread $thread, $id) { try { $this->message_repository->deleteMessage($id); } catch (Exception $error) { return redirect()->route('admin.threads.show', $thread->id)->with('error', 'メッセージの削除に失敗しました。'); } return redirect()->route('admin.threads.show', $thread->id)->with('success', 'メッセージの削除に成功しました。'); } }
次に web.php にてルーティングを設定しましょう。
web.php // 略 Route::group(['prefix' => 'admin', 'middleware' => 'auth:admin', 'as' => 'admin.'], function () { Route::post('logout', 'Admin\LoginController@logout')->name('logout'); Route::get('home', 'Admin\HomeController@index')->name('home'); Route::resource('/threads', 'Admin\ThreadController')->except(['create', 'store', 'update']); Route::resource('/threads/{thread}/messages', 'Admin\MessageController')->only(['destroy']); // 追加 });
ここまででロジックが完了しました。
では実際に、削除ボタンを実装していきましょう。
削除ボタンのコンポーネントを作成します。
components\message-delete.blade.php @if (Auth::guard('admin')->check()) <form action="{{ route('admin.messages.destroy', [$thread, $message->id]) }}" method="post" class="my-2"> @csrf @method('DELETE') <input type="submit" class="btn btn-danger" value="削除" onclick="return confirm('メッセージを削除します。本当に実行してよろしいでしょうか?')"> </form> @endif

コンポーネントにしておくと、色んな場所で使えて非常に便利だな。
そうですね。今回のこの削除ボタンは、一覧画面と詳細画面で使用します。
コンポーネントにしておくことで再利用が可能になり、コードの量を減らすことができますね。
threads\show.blade.php // 略 <div class="card mb-2"> <div class="card-body"> <p>{{ $loop->iteration }} {{ $message->user->name }} {{ $message->created_at }}</p> <p class="mb-0">{!! $message_service->convertUrl($message->body) !!}</p> <div class="row"> @if (!$message->images->isEmpty()) @foreach ($message->images as $image) <div class="col-md-3"> <img src="{{ $image_service->createTemporaryUrl($image->s3_file_path) }}" class="img-thumbnail" alt=""> </div> @endforeach @endif </div> @include('components.message-delete', compact('thread', 'message')) // 追加 </div> </div> // 略
threads\index.blade.php // 略 <div class="card-body"> <h5 class="card-title">{{ $loop->iteration }} 名前:{{ $message->user->name }}:{{ $message->created_at }}</h5> <p class="card-text">{!! $message_service->convertUrl($message->body) !!}</p> <div class="row"> @if (!$message->images->isEmpty()) @foreach ($message->images as $image) <div class="col-md-3"> <img src="{{ $image_service->createTemporaryUrl($image->s3_file_path) }}" class="img-thumbnail" alt=""> </div> @endforeach @endif </div> @include('components.message-delete', compact('thread', 'message')) </div> // 略
それでは、ここまでで Admin がスレッド、メッセージを削除することができるようになりました。
12 章での変更点は以下からご確認いただけます。
https://github.com/t-aburasoba/thread-board/pull/10/files
今回は管理画面作成か。現状だと、ログイン後に文字列が表示されるだけだから質素だよな