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

Laravel でスレッド掲示板を作成(中級)。7 章メッセージ投稿機能




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

7 章:スレッド一覧とメッセージ投稿

それではこの章では、スレッド一覧画面とメッセージ投稿機能を作成しましょう。
この章は説明は少なめ、手を動かしていただきます!

油そば

これまでは、スレッドが果たして投稿できているのか MySQL コンテナを確認しなきゃいけなかったもんな。
やっと楽になる。

こちらを参考にします。
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 にアクセスし確認してみましょう。

油そば

お、なんだかそれっぽくなってきたな。

それでは、次にメッセージを送信できるようにしていきましょう。

挑戦してみよう

メッセージ保存機能をご自身で作成できそう!と言う方は、ぜひ挑戦してみてください。

実装の流れとしては

  1. Form の送信先( MessageController.php )を作成する。
  2. ルーティング( web.php )に MessageController.php を追加する
  3. MessageRequest.php にバリデーションを追加する
  4. MessageController.php store メソッドに処理を追加する。
  5. 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

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




コメントを残す

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