今回は前回に続けて開発を進めていきましょう。
前章をまだご覧になっていない方は以下から開発を始めましょう。
7 章:スレッド一覧とメッセージ投稿
それではこの章では、スレッド一覧画面とメッセージ投稿機能を作成しましょう。
この章は説明は少なめ、手を動かしていただきます!
こちらを参考にします。
http://ai.2ch.sc/newsalpha/
では、まず最初に ThreadRepository にページネートされた Thread を取得するメソッドを追加します。
ThreadRepository.php // 略 /** * Get paginated threads. * * @param int $per_page * @return Thread $threads */ public function getPaginatedThreads(int $per_page) { return $this->thread->paginate($per_page); }
paginate の引数を変数とし、拡張性を持たせました。
ここでもし、paginate(10) などとすると、毎回 10 になってしまうので引数として指定できるようにしています。
では、次に ThreadService に ページネートされた threads を取得するメソッドを作成していきます。
ThreadService.php // 略 /** * Get paginated threads * * @param integer $per_page * @return Thread $threads */ public function getThreads(int $per_page) { $threads = $this->thread_repository->getPaginatedThreads($per_page); $threads->load('user', 'messages.user'); return $threads; } }

load で Eager load しているな。確か、これをする理由は「N + 1 問題」を回避するためだよな。
そうです!N + 1 問題に関しは様々な記事がありますので、詳細を知りたい方はググってみてください。
簡潔に言うと、「クエリ(データベースへの問い合わせ)をイーガーロードを用いることで減らせる」です。
このままですと、モデルにリレーションのためのメソッドがない為エラーになります。
モデルにメソッドを追加していきます。
Message.php // 略 public function user() { return $this->belongsTo('App\User'); } }
Thread.php // 略 public function user() { return $this->belongsTo('App\User'); } public function messages() { return $this->hasMany('App\Message'); } }
それでは、作成した getThreads メソッドをコントローラで使用しましょう。
ThreadController.php // 略 /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $threads = $this->thread_service->getThreads(3); return view('threads.index', compact('threads')); } // 略
では次に View を編集していきましょう。
こちらは、Bootstrap を用いて作成していきます。
「Bootstrap card」や「Bootstrap form」などと検索すると、様々な再利用可能なコードが出てきますので、組み込んでいきましょう。
views\threads\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> {{ $threads->links() }} </div> </div> <div class="row justify-content-center"> @foreach ($threads as $thread) <div class="col-md-8 mb-5"> <div class="card text-center"> <div class="card-header"> <h3 class="m-0">{{ $thread->name }}</h3> </div> @foreach ($thread->messages as $message) <div class="card-body"> <h5 class="card-title">{{ $loop->iteration }} 名前:{{ $message->user->name }}:{{ $message->created_at }}</h5> <p class="card-text">{{ $message->body }}</p> </div> @endforeach <div class="card-footer"> <form method="POST" action="" class="mb-5"> @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> <button type="submit" class="btn btn-primary">書き込む</button> </form> <a href="#">全部読む</a> <a href="#">最新50</a> <a href="#">1-100</a> <a href="#">リロード</a> </div> </div> </div> @endforeach </div> <div class="row justify-content-center"> <div class="col-md-8"> @include('layouts.flash-message') <div class="card"> <h5 class="card-header">新規スレッド作成</h5> <div class="card-body"> <form method="POST" action="{{ route('threads.store') }}"> @csrf <div class="form-group"> <label for="thread-title">スレッドタイトル</label> <input name="name" type="text" class="form-control" id="thread-title" placeholder="タイトル" required> </div> <div class="form-group"> <label for="thread-first-content">内容</label> <textarea name="content" class="form-control" id="thread-first-content" rows="3" required></textarea> </div> <button type="submit" class="btn btn-primary">スレッド作成</button> </form> </div> </div> </div> </div> </div> @endsection
ここまでできましたら、localhost/threads にアクセスし確認してみましょう。


お、なんだかそれっぽくなってきたな。
それでは、次にメッセージを送信できるようにしていきましょう。
メッセージ保存機能をご自身で作成できそう!と言う方は、ぜひ挑戦してみてください。
実装の流れとしては
- Form の送信先( MessageController.php )を作成する。
- ルーティング( web.php )に MessageController.php を追加する
- MessageRequest.php にバリデーションを追加する
- MessageController.php store メソッドに処理を追加する。
- MessageController.php store メソッドの処理をサービス( MessageService.php )とリポジトリ( MessageRepository.php ) に処理をうつす
上記のように実装をしていきます。
実装する内容に対して、どのように実装するかを脳内で整理することも大切ですね。
では、やっていきましょう。
laradock % docker-compose exec workspace bash /var/www# php artisan make:controller MessageController --resource Controller created successfully.

–resource でリソースコントローラを作成しているな。
以前も使用したが、覚えてない方はドキュメントを確認してみよう。
では次にルーティングです。
web.php // 略 Route::resource('/threads/{thread}/messages', 'MessageController')->except(['create', 'update']); // 略
少々、URL の部分を工夫しました。

例えば、ID : 5 のスレッドに対するメッセージを保存したい時には
/threads/5/messages を利用すると言うことか。
そうだね。URL をわかりやすくすることで、後ほどの実装もより簡易になることがあるから、しっかり検討してから実装していこう。
では次にリクエストを作成していきましょう。
laradock % docker-compose exec workspace bash /var/www# php artisan make:request MessageRequest Request created successfully.
MessageRequest.php <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class MessageRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'body' => 'required', ]; } public function messages() { return [ 'body.required' => trans('validation.required'), ]; } }
では次に MessageController.php に保存処理を記入していきましょう。
use 文を省略しているので、皆さんは適切なクラスを use するようにしてください。
MessageController.php // 略 /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } // 略 /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function store(MessageRequest $request, int $id) { $thread = Thread::find($id); // $id と一致する thread を検索 $data = $request->validated(); // バリデーションした値を変数へ。 $data['user_id'] = Auth::id(); // ログイン中のユーザー id を配列に追加。 $thread->messages()->create($data); // 新規データを保存 return redirect()->route('threads.index'); } // 略
これで一旦保存処理ができるようにできましたね。
では次にサービス・リポジトリに切り出し、エラーハンドリングもしていきましょう。
ではまずはリポジトリに必要なメソッドを切り出しましょう。
今回で言うと、Thread を id で検索するメソッドですね。
ThreadRepository.php // 略 /** * Find a thread by id * * @param int $id * @return Thread $thread */ public function findById(int $id) { return $this->thread->find($id); } }
では次にサービスにビジネスロジックを切り出しましょう。
2ch ├─ app │ ├─ Services │ │ └─ ThreadService.php │ │ └─ MessageService.php //新規作成
MessageService.php <?php namespace App\Services; use Exception; use App\Repositories\ThreadRepository; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; class MessageService { /** * @var ThreadRepository */ protected $thread_repository; /** * ThreadService constructor. * * @param ThreadRepository $thread_repository */ public function __construct( ThreadRepository $thread_repository ) { $this->thread_repository = $thread_repository; } /** * Create new message and first new message. * * @param array $data * @return Tread $thread */ public function createNewMessage(array $data, string $thread_id) { DB::beginTransaction(); try { $thread = $this->thread_repository->findById($thread_id); $message = $thread->messages()->create($data); } catch (Exception $error) { DB::rollBack(); Log::error($error->getMessage()); throw new Exception($error->getMessage()); } DB::commit(); return $message; } }
こちらでビジネスロジックを切り出すことができました。
では最後にコントローラを変更していきましょう。
MessageController.php // 略 /** * The MessageService implementation. * * @var MessageService */ protected $message_service; /** * Create a new controller instance. * * @return void */ public function __construct( MessageService $message_service ) { $this->middleware('auth'); $this->message_service = $message_service; } // 略 /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function store(MessageRequest $request, int $id) { try { $data = $request->validated(); $data['user_id'] = Auth::id(); $this->message_service->createNewMessage($data, $id); } catch (Exception $error) { return redirect()->route('threads.index')->with('error', 'メッセージの投稿ができませんでした。'); } return redirect()->route('threads.index')->with('success', 'メッセージを投稿しました'); }
いかがでしょうか。ここまででメッセージの保存処理ができるようになりました。

メッセージを投稿した際に、latest_comment_time を更新しなくて良いのか?
忘れていたね。では更新処理を追加していきましょう
ThreadRepository.php // 略 /** * Update thread latest_comment_time * * @param int $id * @return Thread $thread */ public function updateTime(int $id) { $thread = $this->findById($id); $thread->latest_comment_time = Carbon::now(); return $thread->save(); }
MessageService.php // 略 /** * Create new message and first new message. * * @param array $data * @return Tread $thread */ public function createNewMessage(array $data, string $thread_id) { DB::beginTransaction(); try { $thread = $this->thread_repository->findById($thread_id); $thread->messages()->create($data); $this->thread_repository->updateTime($thread_id); } catch (Exception $error) { DB::rollBack(); Log::error($error->getMessage()); throw new Exception($error->getMessage()); } DB::commit(); return $thread; }
これで、メッセージを投稿した際に該当するスレッドの latest_comment_time も更新できるようになりましたね。
では、ここまでで動作確認を行いましょう。
メッセージを投稿し、MySQL コンテナでデータが入っているかを確認しましょう。
では、少しこれから手直しをしていきます。
ThreadRepository.php // 略 /** * Get paginated threads. * * @param int $per_page * @return Thread $threads */ public function getPaginatedThreads(int $per_page) { return $this->thread->orderBy('latest_comment_time', 'desc')->paginate($per_page); }
先ほど追加した latest_comment_time で並び替えを行いました。
一覧画面も少し修正していきましょう。
index.blade.php @extends('layouts.app') @section('content') <div class="container"> @include('layouts.flash-message') <div class="row justify-content-center"> <div class="col-md-8"> {{ $threads->links() }} </div> </div> <div class="row justify-content-center"> @foreach ($threads as $thread) <div class="col-md-8 mb-5"> <div class="card text-center"> <div class="card-header"> <h3 class="m-0">{{ $thread->name }}</h3> </div> @foreach ($thread->messages as $message) <div class="card-body"> <h5 class="card-title">{{ $loop->iteration }} 名前:{{ $message->user->name }}:{{ $message->created_at }}</h5> <p class="card-text">{{ $message->body }}</p> </div> @endforeach <div class="card-footer"> <form method="POST" action="{{ route('messages.store', $thread->id) }}" class="mb-5"> @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> <button type="submit" class="btn btn-primary">書き込む</button> </form> <a href="#">全部読む</a> <a href="#">最新50</a> <a href="#">1-100</a> <a href="#">リロード</a> </div> </div> </div> @endforeach </div> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <h5 class="card-header">新規スレッド作成</h5> <div class="card-body"> <form method="POST" action="{{ route('threads.store') }}"> @csrf <div class="form-group"> <label for="thread-title">スレッドタイトル</label> <input name="name" type="text" class="form-control" id="thread-title" placeholder="タイトル" required> </div> <div class="form-group"> <label for="thread-first-content">内容</label> <textarea name="content" class="form-control" id="thread-first-content" rows="3" required></textarea> </div> <button type="submit" class="btn btn-primary">スレッド作成</button> </form> </div> </div> </div> </div> </div> @endsection
ここまで修正すると、以下の画像のようになると思います。


アプリ名が「Laravel」から「2ch」に変更されているがどこを変更したんだ?
.env APP_NAME=Laravel // 変更前 APP_NAME=2ch // 変更後
.env にある APP_NAME を変更しましょう!
7 章での変更点は以下からご確認いただけます。
https://github.com/t-aburasoba/thread-board/pull/6/files
これまでは、スレッドが果たして投稿できているのか MySQL コンテナを確認しなきゃいけなかったもんな。
やっと楽になる。