Laravelの基本はこちらから学べます

Laravel でスレッド掲示板を作成(中級)。12 章管理画面作成




今回は前回に続けて開発を進めていきましょう。
前章をまだご覧になっていない方は以下から開発を始めましょう。

12 章:管理画面作成

油そば

今回は管理画面作成か。現状だと、ログイン後に文字列が表示されるだけだから質素だよな

ではまず、管理画面でできる機能について整理していきましょう。

  1. スレッド一覧、紐づくコメントを全て閲覧できる。
  2. 不適切なスレッド、コメントを削除することができる。

一覧画面、詳細画面を表示する。

それではまずは、一覧画面、及び詳細画面を表示していきますが、流れを確認していきましょう。

  1. Admin\ThreadController.php を既存の ThreadController.php を複製する形で作成。
  2. 上記で作成した、Controller へ Admin のミドルウェアを設置
  3. 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 のログイン画面に遷移してしまいます。

全部読むを押すと
Use ログイン画面へリダイレクトされる。
油そば

これは、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

もしこの記事をいいねと思ったたり、ご参考になりましたら下記ボタンからサポートしていただけますと、とても励みになります!




コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です