今回は前回に続けて開発を進めていきましょう。
前章をまだご覧になっていない方は以下から開発を始めましょう。
8 章:スレッド詳細画面
それではこの章では、サクッとスレット詳細画面を作成していきましょう。
ThreadController.php // 略 /** * 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')->except('index'); $this->thread_service = $thread_service; $this->thread_repository = $thread_repository; } // 略 /** * 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'); return view('threads.show', compact('thread')); }
それでは次に、詳細画面を作成していきましょう。
views\threads\show.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-10"> @include('layouts.flash-message') <h3>{{ $thread->name }}</h3> </div> <div class="col-md-10 mb-3"> <a href="{{ route('threads.index') }}" class="btn btn-primary">掲示板に戻る</a> </div> </div> <div class="row justify-content-center"> <div class="col-md-10 mb-5"> @foreach ($thread->messages as $message) <div class="card mb-2"> <div class="card-body"> <p>{{ $loop->iteration }} {{ $message->user->name }} {{ $message->created_at }}</p> <p class="mb-0">{{ $message->body }}</p> </div> </div> @endforeach </div> </div> <div class="row justify-content-center"> <div class="col-md-10"> <div class="card"> <h5 class="card-header">レスを投稿する</h5> <div class="card-body"> <form method="POST" action="{{ route('messages.store', $thread->id) }}" class="mb-4"> @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> </div> </div> </div> </div> </div> @endsection
それでは、localhost/threads/1 にアクセスして画面を確認してみましょう。

見た目はひとまずとし、詳細画面を作成することができました。
現状、コメントを投稿すると、localhost/threads にリダイレクトされてしまいます。
なので、詳細画面にリダイレクトできるようにしましょう。
現状、コメントを投稿すると、localhost/threads にリダイレクトされてしまいます。
なので、詳細画面にリダイレクトできるようにしましょう。
それでは解答です。
MessageController.php // 略 /** * 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.show', $id)->with('error', 'メッセージの投稿ができませんでした。'); } return redirect()->route('threads.show', $id)->with('success', 'メッセージを投稿しました'); }
MessageController.php の store メソッドのリダイレクト先を変更しました。これで、メッセージを保存、失敗した際には詳細画面にリダイレクトされるように変更しました。
では次に、index.blade.php から詳細画面に遷移できるようにしていきます。
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> // 略
全部読む、最新50、1-100 とありますが全て同じ遷移先としました。
また、リロードの遷移先は、index としリロードできるようにしております。
9 章:URLを検出し、リンク化する

コメントに URL を投稿しても、クリックできないな。これは不便だ。
そうですね。せっかく URL を投稿してもらったのにも関わらずリンク化できないのは UX がよくないですね( UX と言うワード使い所間違っていたらご指摘願います )。
では、「PHP URL 正規表現 aタグ」などとググってみましょう。
沢山の記事が出てきますね。ありがたい。
今回はその中の一つ https://wemo.tech/2160 こちらの記事を参考にさせていただきます。
それでは、実装のイメージを立てましょう。
- MessageService に 新規メソッドを作る。
引数に message を受け取り、リンクに変換して message を返す。 - index 及び show blade で上記のメソッドを使用。URL に変換できるようになる。
ではサービスにメソッドを追加しましょう。
MessageService.php // 略 /** * Convert link from message * * @param string $message * @return string $message */ public function convertUrl(string $message) { $pattern = '/((?:https?|ftp):\/\/[-_.!~*\'()a-zA-Z0-9;\/?:@&=+$,%#]+)/'; $replace = '<a href="$1" target="_blank">$1</a>'; $message = preg_replace($pattern, $replace, $message); return $message; }
こちらは先ほどの記事 https://wemo.tech/2160 を参考にさせていただきました。
preg_replace を用いて検索及び置換を行っています。
preg_replace のドキュメントは以下。
https://www.php.net/manual/ja/function.preg-replace.php
それでは、次にこのメソッドをまずは show.blade.php に組み込みましょう。

blade ファイルで service クラスのメソッドを呼ぶことはできるのか?
今までだと、controller から 変数として渡していたよな。
Bladeテンプレート 「サービス注入 service injection 」
https://readouble.com/laravel/6.x/ja/blade.html
こちらを用いると、サービスを blade に inject して使うことができるようになります。
では実際にサービス注入(日本語だと変名前)をしていきます。
show.blade.php @inject('message_service', 'App\Services\MessageService') // 略 <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> // 略
実装ができたので、実際に URL がリンクに変換されるか確認してみましょう。


お、。。。できては、ないな。
リンクには変換できているので、サービス注入は成功していますね。
では、なぜこのようにリンクが文字列として表示されてしまっているのでしょうか。
Tip!! Bladeの
https://readouble.com/laravel/6.x/ja/blade.html{{ }}
記法はXSS攻撃を防ぐため、自動的にPHPのhtmlspecialchars
関数を通されます。

htmlspecialchars 関数か。
https://www.php.net/manual/ja/function.htmlspecialchars.php
これかな。XSS を防ぐために自動的にエスケープしてくれているのか
そうですね。XSS 皆さんご存知でしょうか。WEB サービスを作成する時には注意しなければならない脆弱性です。
実際に htmlspecialchars がないとどうなってしまうのか試してみましょう。
デフォルトでブレードの
https://readouble.com/laravel/6.x/ja/blade.html{{ }}
文はXSS攻撃を防ぐために、PHPのhtmlspecialchars
関数を自動的に通されます。しかしデータをエスケープしたくない場合は、以下の構文を使ってください。

{!! !!} を用いることで、htmlspecialchars
関数を自動的に通さずにできそうだな。
これでリンクが表示されるんじゃないか!
では変更していきましょう。
show.blade.php // 略 <p class="mb-0">{!! $message_service->convertUrl($message->body) !!}</p> // 略
変更が完了しましたら、確認してみましょう。

クリックできるようになりましたね。
では、XSS の実践をしていきましょう。
レスを投稿で、下記の投稿を書き込んでみましょう。
実際にウイルスを入れているわけではないのでご安心ください。(ハッカーほどの技術はない T です。ご了承ください。)
<script>alert('ウイルスに感染させたよ🥺');</script>


おお、大変だ。{!! !!} これを使用すると、<script></script>を埋め込まれると作動してしまうのか。
と言うことで、危険な掲示板ができましたね。
では、どのように実装すれば ①リンクを表示させつつ ② スクリプトはエスケープできるでしょうか。
MessageService.php // 略 /** * Convert link from message * * @param string $message * @return string $message */ public function convertUrl(string $message) { $message = e($message); $pattern = '/((?:https?|ftp):\/\/[-_.!~*\'()a-zA-Z0-9;\/?:@&=+$,%#]+)/'; $replace = '<a href="$1" target="_blank">$1</a>'; $message = preg_replace($pattern, $replace, $message); return $message; }
Laravel が用意している「ヘルパ」関数 e() を利用しました。
ヘルパ 「 e( ) 」
https://readouble.com/laravel/6.x/ja/helpers.html
このヘルパ関数は、htmlspecialchars 関数を実行しエスケープをしてくれます。
つまり、上記の convertUrl メソットでは
- ユーザーから受け取ったメッセージをまずはエスケープ処理(安全に)
- エスケープ後の中から URL を検索しリンクに変更
この流れで実装しているため、安全にリンクを表示することが可能になりましたので、実際に確認してみましょう。

では、index.blade.php にも サービスインジェクトを行い、URL がリンクで表示できるようにしておきましょう。
では次の章では画像投稿をできるようにしていきましょう。
8, 9 章での変更点は以下からご確認いただけます。
https://github.com/t-aburasoba/thread-board/pull/7/files
別の章で作成した、findById が再利用できたな。
なるほど、repository の良さが出てきたな。