tag:crieit.net,2005:https://crieit.net/tags/Laravel%E3%81%A7%E8%B3%AA%E5%95%8F%E7%AE%B1%E3%81%BF%E3%81%9F%E3%81%84%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E4%BD%9C%E3%82%8B/feed 「Laravelで質問箱みたいなサービスを作る」の記事 - Crieit Crieitでタグ「Laravelで質問箱みたいなサービスを作る」に投稿された最近の記事 2020-02-11T23:52:01+09:00 https://crieit.net/tags/Laravel%E3%81%A7%E8%B3%AA%E5%95%8F%E7%AE%B1%E3%81%BF%E3%81%9F%E3%81%84%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E4%BD%9C%E3%82%8B/feed tag:crieit.net,2005:PublicArticle/15715 2020-02-11T23:48:05+09:00 2020-02-11T23:52:01+09:00 https://crieit.net/posts/b42ed137feb9160fe2ed7ec882f9cdff 未回答のものを絞り込む機能を追加する <p>前回作った受け取った質問一覧ページは未回答のものも回答済みのものも表示されていて未回答のものを探しにくいため、未回答の質問を絞り込む機能を作成します。</p> <h2 id="アクションを検索に対応"><a href="#%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E6%A4%9C%E7%B4%A2%E3%81%AB%E5%AF%BE%E5%BF%9C">アクションを検索に対応</a></h2> <p>まずはアクション内の絞り込みの部分を調整します。URLに<code>no_answer=1</code>というパラメータが付いた場合だけ未回答の絞り込みを行うようにします。</p> <p>検索条件は質問に紐づく回答が存在するかになります。SQLでいうとquestionsに紐づくanswersが存在しないもの、ということになります。具体的にはLaravelのQuestionControllerのindexメソッドを下記のように変更しました。</p> <pre><code class="php"> public function index(Request $request) { $query = Question::with('answer') ->where('received_user_id', Auth::id()) ->orderByDesc('id'); if ($request->input('no_answer')) { $query->whereDoesntHave('Answer'); } $questions = $query->paginate(); return view('question.index', compact('questions')); } </code></pre> <p>これで例えば<code>http://localhost:8000/questions?no_answer=1</code>のようなURLにアクセスすると未回答のものだけを表示できるようになりました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b04a0c9df.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b04a0c9df.png?mw=700" alt="" /></a></p> <p>前回Debugbarを導入して、実際に発行されているSQLを簡単に見ることができるようになったため見てみると、下記のようなSQLが発行されるようになりました(整形及び一部省略しています)</p> <pre><code class="sql">select * from `questions` where `received_user_id` = 1 and not exists( select * from `answers` where `questions`.`id` = `answers`.`question_id` ) order by `id` desc </code></pre> <h2 id="ページ分けに対応"><a href="#%E3%83%9A%E3%83%BC%E3%82%B8%E5%88%86%E3%81%91%E3%81%AB%E5%AF%BE%E5%BF%9C">ページ分けに対応</a></h2> <p>このままだと、ページ分けされた際に別ページのリンクをクリックした際に<code>no_answer=1</code>のパラメータが解除されてしまいます。そのため、パラメータを維持できるようにページネーションの表示を下記のように変更します。</p> <pre><code class="html"> <span>{</span><span>{</span> $questions->appends(request()->only(['no_answer']))->links() <span>}</span><span>}</span> </code></pre> <p>このようにlinksメソッドを呼ぶ前にappendsを挟んで追加パラメータを指定することで、ページネーションのリンクにも追加のパラメータが維持されるようになります。</p> <p>ちなみに<code>$query->paginate(2)</code>の様に表示されるデータより少ない数のlimitを指定すればデータを増やさなくても確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b20f896a6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b20f896a6.png?mw=700" alt="" /></a></p> <h2 id="実際に切り替えられるようにする"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%82%89%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B">実際に切り替えられるようにする</a></h2> <p>あとは実際にno_answer付きと無しのURLを切り替えられるようにしておきます。テンプレートに下記のような記述を追記しました。</p> <pre><code class="html"> <nav class="nav justify-content-center m-3"> <a class="nav-link @if (!request()->input('no_answer')) active @endif" href="<span>{</span><span>{</span> url('questions') <span>}</span><span>}</span>"> すべて </a> <a class="nav-link @if (request()->input('no_answer')) active @endif" href="<span>{</span><span>{</span> url('questions?no_answer=1') <span>}</span><span>}</span>"> 未回答のみ </a> </nav> </code></pre> <p>Bootstrapの下記のコンポーネントを利用しています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.4/components/navs/">Navs · Bootstrap</a></p> <p>ちなみにこの横並びの表記にはFlexboxが利用されています。センタリングしておきたかったので<code>nav</code>のクラスに<code>justify-content-center</code>を追加しています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.4/utilities/flex/#justify-content">Justify content - Flex · Bootstrap</a></p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b5540d50e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42b5540d50e.png?mw=700" alt="" /></a></p> <h2 id="SQLの調整"><a href="#SQL%E3%81%AE%E8%AA%BF%E6%95%B4">SQLの調整</a></h2> <p>これで一通り対応はできました。ただしこのサービスの場合、質問と回答のデータはメインになる、且つ一番増える可能性が高いため、上記で実装したようにリレーションを利用しての検索は後々重くなりすぎて問題になる可能性があります。そのため一旦ちょっと軽量化のための対策を行います。</p> <p>動作としてはすでに出来ており、必須ではありませんので読み飛ばしても問題ありません。</p> <p>具体的な対応方法として、リレーションして判断させるのではなく、回答がついた際に質問データ側に回答済みフラグを付けておくようにします。そのようにすることで質問データのみを使ったSQLで機能を実現できるようになります。</p> <h3 id="マイグレーションを作成"><a href="#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90">マイグレーションを作成</a></h3> <p>まずはその回答済みフラグをquestionsテーブルに追加するためのマイグレーションを追加します。</p> <pre><code>php artisan make:migration add_has_answer_to_questions --table questions </code></pre> <p>下記のようなマイグレーションを書きます。has_answerというカラムを追加します。</p> <pre><code class="php"> public function up() { Schema::table('questions', function (Blueprint $table) { $table->boolean('has_answer')->default(false)->after('body'); }); } public function down() { Schema::table('questions', function (Blueprint $table) { $table->dropColumn('has_answer'); }); } </code></pre> <p>これを実行すればDB側の調整は完了です。あとは実際の処理を入れていきます。</p> <h3 id="回答された際に回答済みフラグを更新するようにする"><a href="#%E5%9B%9E%E7%AD%94%E3%81%95%E3%82%8C%E3%81%9F%E9%9A%9B%E3%81%AB%E5%9B%9E%E7%AD%94%E6%B8%88%E3%81%BF%E3%83%95%E3%83%A9%E3%82%B0%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B">回答された際に回答済みフラグを更新するようにする</a></h3> <p>AnswerControllerのstoreアクションの回答データ登録後に下記の処理を追加しておきます。</p> <pre><code class="php"> $question->has_answer = true; $question->save(); </code></pre> <p>問題なければ回答時にhas_answerフラグがオンになります。(※answersテーブルの作成マイグレーションの外部キー作成が間違っていたので修正済みです。<a href="https://crieit.net/posts/33685639291474c3aabfdaafeafa87cd">質問に回答する機能を作る</a>)</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42bbffc3781.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e42bbffc3781.png?mw=700" alt="質問に回答する機能を作る" /></a></p> <h3 id="検索クエリを変更する"><a href="#%E6%A4%9C%E7%B4%A2%E3%82%AF%E3%82%A8%E3%83%AA%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B">検索クエリを変更する</a></h3> <p>先程作成した回答済みのみを抽出するSQLをフラグを使ったシンプルな形に変更します。さきほど実装したwhereDoesntHaveを変更します。</p> <pre><code class="php"> if ($request->input('no_answer')) { $query->where('has_answer', true); } </code></pre> <p>これで一通り完了です。動作確認しておきましょう。クエリも下記のようなシンプルな形になりました。</p> <pre><code class="sql">select * from `questions` where `received_user_id` = 1 and `has_answer` = 1 order by `id` desc </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>これで未回答のものだけで絞り込みができるようになりました。</p> <p>次はナビゲーションなどがまだ未整備で使いづらいためそのあたりを調整していきたいと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15712 2020-02-09T18:53:13+09:00 2020-02-09T18:58:15+09:00 https://crieit.net/posts/Laravel-Debugbar-5e3fd689cd5a8 LaravelのDebugbarを導入する <p>すこし横道にそれますが、Debugbarという便利ツールを導入してみます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/barryvdh/laravel-debugbar">barryvdh/laravel-debugbar: Laravel Debugbar (Integrates PHP Debug Bar)</a></p> <p>Debugbarは開発時に様々な情報を表示してくれる便利なツールです。これを導入しておくと、わざわざダンプしなくても簡単に画面上で様々な値を確認することが出来ます。APIサーバーなどの場合は意味がありませんが、普通にページを表示するアプリケーションの場合に有用です。ページ表示時だけでなく、ajaxで通信した値なども自動的に表示してくれたりします。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e3fb2803b85f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5e3fb2803b85f.png?mw=700" alt="" /></a></p> <h2 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h2> <p>Composerでインストールします。</p> <pre><code>composer require barryvdh/laravel-debugbar --dev </code></pre> <p>古いバージョンのLaravelでなければ基本的にこれだけで導入は完了です。環境変数で<code>APP_DEBUG=true</code>の設定にしておけば表示されます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>ということで次でちょっとLaravelが発行しているSQLを見ながら話を進めたかったのでDebugbarの回を挟んでみました。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15537 2019-11-11T07:50:57+09:00 2020-02-08T16:52:18+09:00 https://crieit.net/posts/ace4ccb8595768287c0183f9b1a05d65 受け取った質問一覧ページを作る <p>次は受け取った質問の一覧ページを作成します。一通り主要な詳細ページは作ってきましたが、今のままではサービスとしては利用できません。受け取った質問一覧ページを作成し、全ての質問にアクセスできるようにします。</p> <h2 id="質問一覧ページを作成"><a href="#%E8%B3%AA%E5%95%8F%E4%B8%80%E8%A6%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E6%88%90">質問一覧ページを作成</a></h2> <p>実際に質問一覧ページを作成していきます。</p> <h3 id="アクションを作成"><a href="#%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90">アクションを作成</a></h3> <p>まずはQuestionControllerに一覧ページ用のアクションを追加します。getではなくpaginateを使うことでページ切り替えをして全て閲覧できるようにしています。</p> <pre><code class="php"> public function index() { $questions = Question::where('received_user_id', Auth::id()) ->orderByDesc('id') ->paginate(); return view('question.index', compact('questions')); } </code></pre> <h3 id="ルーティングを作成"><a href="#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E4%BD%9C%E6%88%90">ルーティングを作成</a></h3> <p>このアクション用のルーティングを設定します。authミドルウェアが設定されているQuestionControllerの設定にindexを追加します。元々storeのルーティングが作成済みだったのでそちらに追記します。</p> <pre><code class="php"> Route::resource('questions', 'QuestionController')->only(['index', 'store']); </code></pre> <h3 id="テンプレートを作成"><a href="#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90">テンプレートを作成</a></h3> <p>次に<code>resources/views/question/index.blade.php</code>を作成します。</p> <pre><code class="html">@extends('layouts.app') @section('content') <section class="text-center"> <h1 style="font-size: 1.5rem">届いた質問一覧</h1> @foreach ($questions as $question) <a href="<span>{</span><span>{</span> url("questions/{$question->id}/received") <span>}</span><span>}</span>"> <div class="card mb-4"> <div class="card-body"> <span>{</span><span>{</span> $question->body <span>}</span><span>}</span> </div> </div> </a> @endforeach <div class="text-center d-inline-block"> <span>{</span><span>{</span> $questions->links() <span>}</span><span>}</span> </div> </section> @endsection </code></pre> <p>これでログインした状態で<code>http://localhost:8000/questions</code>にアクセスすれば表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7500405e29.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7500405e29.png?mw=700" alt="" /></a></p> <p>これでアクションの<code>paginate()</code>を<code>paginate(2)</code>等にして1ページ内のデータ表示数を試しに変えてみることでページの表示も確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7506545ea1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7506545ea1.png?mw=700" alt="" /></a></p> <h2 id="回答したかどうかを表示する"><a href="#%E5%9B%9E%E7%AD%94%E3%81%97%E3%81%9F%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">回答したかどうかを表示する</a></h2> <p>このままだと回答したかどうかが分かりづらいため、未回答かどうかも表示するようにしてみます。</p> <h3 id="表示を調整する"><a href="#%E8%A1%A8%E7%A4%BA%E3%82%92%E8%AA%BF%E6%95%B4%E3%81%99%E3%82%8B">表示を調整する</a></h3> <p>Questionは一つのAnswerを持っているため、そのデータがあるかどうかで判断ができます。これは<code>$question->answer</code>のようにして取得できます。テンプレートを調整していきますが、回答済、未回答を表示するのはBootstrapの下記のbadgeコンポーネントが良さそうですのでこれを利用します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/badge/">Badges · Bootstrap</a></p> <p>下記のように調整しました。</p> <pre><code class="html"> @foreach ($questions as $question) <a href="<span>{</span><span>{</span> url("questions/{$question->id}/received") <span>}</span><span>}</span>"> <div class="card mb-4"> <div class="card-body"> <span>{</span><span>{</span> $question->body <span>}</span><span>}</span> @if ($question->answer) <span class="badge badge-success">回答済</span> @else <span class="badge badge-warning">未回答</span> @endif </div> </div> </a> @endforeach </code></pre> <p>表示状はもうこれでOKです。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7530a6c113.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7530a6c113.png?mw=700" alt="" /></a></p> <h3 id="SQLの実行を最適化する"><a href="#SQL%E3%81%AE%E5%AE%9F%E8%A1%8C%E3%82%92%E6%9C%80%E9%81%A9%E5%8C%96%E3%81%99%E3%82%8B">SQLの実行を最適化する</a></h3> <p>しかし一つ問題点として、ループの中で<code>$question->answer</code>を実行しているため、ループの中で何回もSQLのデータ処理が行われてしまいます。データが20件あれば最初の一覧取得と毎回の回答取得で21回も呼ばれてしまうため負荷的には非効率です。</p> <p>そのため、LaravelのEagerローディングを利用します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://readouble.com/laravel/6.x/ja/eloquent-relationships.html#eager-loading">Eagerロード</a></p> <p>これだと最初に質問一覧を取った時、その質問のIDに紐づく回答を全て取得して紐付けておいてくれるため、今回の場合だと合計2回のクエリ実行で済みます。アクションのデータ取得部分を下記のように調整します。</p> <pre><code class="php"> $questions = Question::with('answer') ->where('received_user_id', Auth::id()) ->orderByDesc('id') ->paginate(); </code></pre> <p>withをつけることでEagerローディングが行われるようになります。</p> <h2 id="次は"><a href="#%E6%AC%A1%E3%81%AF">次は</a></h2> <p>あとは未回答のものだけを絞り込む機能をつけようかと思いましたが、せっかくLaravel Mixを使っているのでVue.jsのコンポーネントを使った簡単なJavaScriptの処理を入れて実装してみたいと思いますので次回に分けることにします…と思いましたがそこまで必要性を感じられていないのでとりあえず普通にJavaScript無しで実装していきます。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15495 2019-10-21T07:45:11+09:00 2019-10-21T07:45:11+09:00 https://crieit.net/posts/Twitter-OGP Twitterシェア用のOGPを作成する <p>質問、回答と作成しましたので、次にその回答をツイッターに画像つきで投稿する必要があります。ツイートされた時に画像を表示するため、そのOGP用の画像を生成する処理を作成します。</p> <p>こんな画像を作成します。</p> <p><a href="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5da72758df7f4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5da72758df7f4.png?mw=700" alt="" /></a></p> <p>ちなみに実は以前別の記事でもLaravelでOGPを作る方法を解説しました。</p> <p><a href="https://crieit.net/posts/Laravel-OPG">LaravelでOGPを作る</a></p> <p>上記ではGDを使って生成しています。同じような解説になってしまってもあまり意味がないので、今回はImagickを使って生成してみます。サーバーにはImageMagick自体とPHPのImagickがインストールされている必要があります。準備が難しい場合はGDのパターンで試してみてください。</p> <h2 id="背景画像を用意する"><a href="#%E8%83%8C%E6%99%AF%E7%94%BB%E5%83%8F%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B">背景画像を用意する</a></h2> <p>簡単にOGPをカッコよくしたいので、背景には枠画像を使うことにしました。下記でフリー素材をダウンロードしてきました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://sozai-good.com/illust/free-background/town/29850">街中の住宅シルエット フレーム素材 枠-囲み 247 | 素材Good</a></p> <p>これを<code>resources/images/ogp.png</code>として保存しておきます。</p> <h2 id="フォントを用意する"><a href="#%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B">フォントを用意する</a></h2> <p>文字を描画するためにはフォントが必要です。Googleフォントから日本語フォントをダウンロードしてきました。これを<code>resources/fonts</code>フォルダに保存しておきます。</p> <h2 id="画像の生成処理を作る"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E7%94%9F%E6%88%90%E5%87%A6%E7%90%86%E3%82%92%E4%BD%9C%E3%82%8B">画像の生成処理を作る</a></h2> <p>素材が準備できましたので、Questionモデルに画像の生成処理を作ります。</p> <p>まずはuseを追加します。</p> <pre><code class="php">use Imagick; use ImagickDraw; </code></pre> <p>そしてメソッドを追加します。各行の文字数が長いと画像からはみ出してしまうため、適当に折り返しを行っています。</p> <pre><code class="php"> public function generateOgp(): Imagick { $body = $this->getWordwrapedBody(); $image = new Imagick(resource_path('images/ogp.png')); $draw = new ImagickDraw(); $draw->setFont(resource_path('fonts/MPLUS1p-Regular.ttf')); $draw->setFontSize(24); $draw->setTextAlignment(Imagick::ALIGN_CENTER); $lines = explode("\n", $body); $draw->annotation(300, 157 - (count($lines) - 1) * 12, $body); $image->drawImage($draw); return $image; } private function getWordwrapedBody(): string { $lines = explode("\n", $this->body); $result = []; foreach ($lines as $line) { $length = mb_strlen($line); for ($start = 0; $start < $length; $start += 20) { $result[] = mb_substr($line, $start, 20); } } return join("\n", $result); } </code></pre> <h2 id="画像を表示する"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">画像を表示する</a></h2> <p>実際にコントローラに画像を表示するためのアクションを追加します。AnswerControllerに追加します。</p> <pre><code class="php"> public function ogp($id) { $answer = Answer::findOrFail($id); $image = $answer->question->generateOgp(); return response($image, 200) ->header('Content-Type', 'image/png'); } </code></pre> <p>順番が前後してしまいましたが、上記でanswerから関連するquestionを取得するようにしているため、Models/Answer.phpにリレーションを定義しておきます。</p> <pre><code class="php"> public function question() { return $this->belongsTo(Question::class); } </code></pre> <p>アクセスできるようにルーティングを設定します。誰でもアクセスできるようにする必要があるので認証ミドルウェアのグループ外に記述します。</p> <pre><code class="php">Route::get('answers/{id}/ogp.png', 'AnswerController@ogp'); </code></pre> <p>これで<code>http://localhost:8000/answers/1/ogp.png</code>のようなURLにアクセスすると、画像が表示されます。(ホスト名とIDは適宜置き換えてください)</p> <h2 id="回答の詳細ページを作成"><a href="#%E5%9B%9E%E7%AD%94%E3%81%AE%E8%A9%B3%E7%B4%B0%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E6%88%90">回答の詳細ページを作成</a></h2> <p>画像ができましたので、次に実際にTwitterにURLをシェアしてもらうための回答詳細ページを作成します。URLは<code>answers/ID</code>にします。questionsのreceived機能とほとんど同じではあるのですが、あちらは質問を受け取った登録済みユーザー専用のページになります。表示もほとんど同じで良いと思いますが、同じページで共通化して条件分岐などでの対処にしてしまうと、不具合が発生した時に誰でも質問を見れてしまう場合が発生してしまう可能性があります。そういった問題を避けるためにも多少冗長化し、answersデータがあるものだけは一般公開で表示する、という安全な仕様にしています。</p> <p>とりあえずAnswerControllerにメソッドを追加します。</p> <pre><code class="php"> public function show($id) { $answer = Answer::findOrFail($id); return view('answer.show', compact('answer')); } </code></pre> <p>ここにアクセスできるようにルーティングを追加しておきます。ogpの下辺りにshowメソッドの追加を並べておきます。</p> <pre><code class="php">Route::get('answers/{id}/ogp.png', 'AnswerController@ogp'); Route::resource('answers', 'AnswerController')->only(['show']); </code></pre> <p>表示する時に回答者の名前も表示するため、回答データから回答ユーザーを取得できるように、<code>Models/Answer.php</code>にリレーションの定義を追加しておきます。</p> <pre><code class="php"> public function user() { return $this->belongsTo(User::class); } </code></pre> <p>これで<code>$answer->user</code>のような形でアクセスできます。</p> <p>そしてテンプレートを<code>resources/views/answer/show.blade.php</code>として作成します。基本的にreceivedからコピーして不要部分を削っただけです。せっかくなので質問の箇所はOGPと同じ画像を表示するようにしています。軽くはないと思うのでリリース後はCloudflareでキャッシュしたいですね。</p> <pre><code class="html">@extends('layouts.app') @section('title', "{$answer->user->name}さんへの質問") @section('description', $answer->body) @section('ogp', url("answers/{$answer->id}/ogp.png")) @section('content') <section class="text-center"> <h1 style="font-size: 1.5rem">届いた質問</h1> <div class="mb-4"> <img src="<span>{</span><span>{</span> url("answers/{$answer->id}/ogp.png") <span>}</span><span>}</span>"> </div> <h2 style="font-size: 1.2rem"><span>{</span><span>{</span> $answer->user->name <span>}</span><span>}</span>さんからの回答</h2> <div class="card"> <div class="card-body"> <span>{</span><span>{</span> $answer->body <span>}</span><span>}</span> </div> </div> </section> @endsection </code></pre> <p>sectionでtitle, description, ogpを指定しています。この様にして、各ページごとに共通レイアウトに渡したい値を指定することが出来ます。共通レイアウト側にこれを受けて表示をするための処理を追加しておきます。headタグ内に下記を追加します。(タイトルの部分は条件分岐を追加しています)</p> <pre><code class="html"> @hasSection('title') <title>@yield('title') - <span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span></title> <meta property="og:title" content="@yield('title') - <span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span>"> @else <title><span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span></title> <meta property="og:title" content="<span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span>"> @endif @hasSection('description') <meta name="description" content="@yield('description')"> <meta property="og:description" content="@yield('description')"> @else <meta name="description" content="質問と回答ができるサービスです"> <meta property="og:description" content="質問と回答ができるサービスです"> @endif <meta property="og:type" content="website"> <meta property="og:site_name" content="Crieit"> @hasSection('ogp') <meta property="og:image" content="@yield('ogp')"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:image" content="@yield('ogp')"> @endif </code></pre> <p>こんな感じで表示できるようになりました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dac76a488a46.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dac76a488a46.png?mw=700" alt="" /></a></p> <p>今はローカル環境で試しているのでダメですが、サーバーにアップしている状態であればこのページのURLをツイートすればTwitterでも画像が表示されるようになっているはずです。</p> <p>ちなみに上記のスクリーンショットのようにちょっと左右のズレがあるようです。画像のサイズと画面の横幅があっていないため、共通レイアウトに直接書かれている<code>600px</code>という表記を適当に全て<code>700px</code>に変更しておきます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>とりあえずメインの機能はおおよそできてきました。このあと面白くなくなってくる可能性があるのでこの記事はあとにすればよかったと後悔しています。次回は細々としたところを調整していくか、ハロウィン仕様への変更を予定しています。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15470 2019-10-09T21:39:36+09:00 2020-02-11T23:34:29+09:00 https://crieit.net/posts/33685639291474c3aabfdaafeafa87cd 質問に回答する機能を作る <p>質問機能は作成済みですので、同様にメインとなる機能である回答機能を作成していきます。</p> <h2 id="回答テーブルを作成する"><a href="#%E5%9B%9E%E7%AD%94%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">回答テーブルを作成する</a></h2> <p>とりあえず回答用のテーブルのマイグレーションを作成しておきます。</p> <pre><code>php artisan make:migration create_answers --create answers </code></pre> <p>中身はこんな感じです。</p> <pre><code class="php">class CreateAnswers extends Migration { public function up() { Schema::create('answers', function (Blueprint $table) { $table->bigIncrements('id'); $table->bigInteger('question_id')->unsigned()->index(); $table->bigInteger('user_id')->unsigned()->index(); $table->text('body'); $table->timestamps(); $table->foreign('question_id')->references('id')->on('questions')->onDelete('cascade'); }); } public function down() { Schema::dropIfExists('answers'); } } </code></pre> <p>どの質問への回答か、どのユーザーが回答したか、回答の内容のカラムを追加しています。user_idは実際には不要で、question_idでquestionsを検索し、questionsのreceived_user_idでどのユーザが回答したかは分かります。</p> <p>ただし、回答者用の自分の回答一覧ページを作る際に、わざわざ上記のような形でJOINして検索を行わなくてはならないため負荷が増える可能性があります。具体的には下記のようなSQLです。個人的にはJOIN先のみでWHEREするのは非常に微妙だと思っています。</p> <pre><code class="sql">SELECT answers.* FROM answers LEFT JOIN questions ON questions.id = answers.question_id WHERE questions.received_user_id = <span>{</span><span>{</span>ユーザーID<span>}</span><span>}</span> </code></pre> <p>そのためリレーショナルデータベース的には不要ですが、検索を軽量化するための冗長化としてuser_idも入れています。これで下記のシンプルなSQLで取得が可能となります。むちゃくちゃ軽いです。</p> <pre><code class="sql">SELECT * FROM answers WHERE user_id = <span>{</span><span>{</span>ユーザーID<span>}</span><span>}</span> </code></pre> <p>何にしろマイグレーションを実行しておきます。</p> <pre><code>php artisan migrate </code></pre> <h2 id="回答ページを作成"><a href="#%E5%9B%9E%E7%AD%94%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E6%88%90">回答ページを作成</a></h2> <p>本来であれば自分に来た質問一覧ページからリンクをクリックして回答ページへ行く流れですが、とりあえず回答処理を作ってしまいたいので直接回答ページから作っていきます。</p> <p>回答するページのURLは<code>questions/{id}/received</code>とします。とりあえずルーティングを設定します。authミドルウェアの中に入れましょう。</p> <pre><code class="php">Route::group(['middleware' => ['auth']], function () { Route::get('questions/{id}/received', 'QuestionController@received'); Route::resource('questions', 'QuestionController')->only(['store']); }); </code></pre> <p>回答フォーム用の処理を入れていきます。QuestionControllerに下記のようなアクションを入れます。</p> <pre><code class="php"> public function received($questionId) { $question = Question::where('id', $questionId) ->where('received_user_id', Auth::id()) ->firstOrFail(); return view('question.received', compact('question')); } </code></pre> <p>取得する質問データが自分に宛てられたものかどうかもチェックしており、違う場合は本来存在しないURLですのでfirstOrFailで404にしています。このあたりはチームでやっているのであれば各プロジェクトのルールによって適宜適切なエラー処理のルールで対応していくと良いと思います。</p> <p>続いて表示するためのテンプレートを<code>resources/views/question/received.blade.php</code>として作成します。</p> <pre><code class="html">@extends('layouts.app') @section('content') <section class="text-center pt-4"> <h1 style="font-size: 1.5rem">回答する</h1> <div class="card"> <div class="card-body"> <span>{</span><span>{</span> $question->body <span>}</span><span>}</span> </div> </div> </section> @endsection </code></pre> <p>とりあえず回答時に見るための質問表示です。Bootstrapのカードで適当に表示しています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/card/">Cards · Bootstrap</a></p> <p><code>http://localhost:8000/questions/1/received</code> にアクセスすると下記が表示されます。ホスト名、ポート、ID(URLの例で1になっているところ)は適宜自分の環境に合わせてください。認証ガードをしていますのでうまく表示されない場合はログインしておいてください。表示が崩れる場合は<code>yarn hot</code>の実行も必要です。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9b4de0a61e2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9b4de0a61e2.png?mw=700" alt="" /></a></p> <p>続いてcard(の閉じタグ)の下にフォームを入れていきます。質問フォームとほとんど同じです。</p> <pre><code class="html"> <div class="card mb-4"> <div class="card-body"> <span>{</span><span>{</span> $question->body <span>}</span><span>}</span> </div> </div> {!! Form::open(['url' => url('answers')]) !!} {!! Form::hidden('question_id', $question->id) !!} <div class="form-group"> {!! Form::textarea('body', null, [ 'class' => 'form-control', 'placeholder' => '回答する', 'required' => true, ]) !!} </div> {!! Form::Submit('回答する', ['class' => 'btn btn-primary']) !!} {!! Form::close() !!} </code></pre> <p>質問とくっついてしまうので質問のclassに<code>mb-4</code>でmargin-bottomも入れています。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bc575e3e36.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bc575e3e36.png?mw=700" alt="" /></a></p> <h2 id="回答を登録する"><a href="#%E5%9B%9E%E7%AD%94%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B">回答を登録する</a></h2> <p>formもできたので回答を登録できるようにします。まずは回答処理用のコントローラを作成します。</p> <pre><code>php artisan make:controller AnswerController </code></pre> <p>ついでにモデルも使うと思うので作っておきます</p> <pre><code>php artisan make:model Models/Answer </code></pre> <p>Questionの時と同様、guardedを設定しておきます。</p> <pre><code class="php">class Answer extends Model { protected $guarded = ['id']; } </code></pre> <p>回答データを保存する際、質問データの子として保存しますので、その関係を質問のモデルにも定義しておきます。<code>Models/Question.php</code>をに下記のような定義を追加します。</p> <pre><code class="php"> public function answer() { return $this->hasOne(Answer::class); } </code></pre> <p>これはそのままの通り、Question has one answer、つまり質問データは一つの回答データを持つ、という定義でリレーションといいます。このようにリレーションを定義しておくことでデータを保存する際や取得する時に簡単に処理を作成することができるようになっています。詳しくは下記の公式マニュアルにも書かれており、他にも様々な種類のリレーションの定義ができるようになっています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://readouble.com/laravel/6.0/ja/eloquent-relationships.html">Eloquent:リレーション 6.0 Laravel</a></p> <p>実際に回答登録処理を作成します。まずは回答用のルーティングを作成します。answersというところです。なんとなくですが、問題がでるところでなければアルファベット順で書くと増えた時に見やすいと思います。</p> <pre><code class="php">Route::group(['middleware' => ['auth']], function () { Route::resource('answers', 'AnswerController')->only(['store']); Route::get('questions/{id}/received', 'QuestionController@received'); Route::resource('questions', 'QuestionController')->only(['store']); }); </code></pre> <h3 id="resourceとは"><a href="#resource%E3%81%A8%E3%81%AF">resourceとは</a></h3> <p>ところでこの<code>resource</code>とは何かを一旦解説します。resourceというのは、基本的な処理のためのアクションをまとめてルーティングに登録できる機能です。Laravelでは具体的には下記になっています。</p> <div class="table-responsive"><table> <thead> <tr> <th>メソッド</th> <th>アクション</th> <th>意味</th> </tr> </thead> <tbody> <tr> <td>GET</td> <td>index</td> <td>一覧</td> </tr> <tr> <td>GET</td> <td>create</td> <td>新規登録ページ</td> </tr> <tr> <td>POST</td> <td>store</td> <td>新規登録処理</td> </tr> <tr> <td>GET</td> <td>edit</td> <td>編集ページ</td> </tr> <tr> <td>PUT, PATCH</td> <td>update</td> <td>更新処理</td> </tr> <tr> <td>DELETE</td> <td>destroy</td> <td>削除</td> </tr> </tbody> </table></div> <p>アクションというのはコントローラのメソッド名ですので分かりやすいと思います。今回の例でいうと回答の登録処理にはstoreというメソッドを追加します。</p> <p>メソッドというのはちょっと最初はわかりにくいかもしれませんが、アプリケーションにどのような処理をするか、というのを伝えるためには実はURLだけでなく、URLとメソッドの組み合わせが必要になっています。</p> <p>普段ブラウザでWebにアクセスする場合は意識していないと思いますが、単にURLにアクセスしているだけではなく、実はGETメソッドでそのURLにアクセスを行っています。そしてformで何かを登録する際も、その登録先のURLにただアクセスしているわけではなく、POST, PUT, PATCH, DELETEのどれかでアクセスを行っています。</p> <p>つまり例えば同じ<code>/questions</code>というURLでも、ブラウザで単にアクセスした場合はGETのアクセスになるので一覧ページが表示され、formでPOSTした場合はデータが登録される、ということになります。とりあえず今のところは深く考える必要はなく、上記のテーブルのメソッドとアクションの組み合わせでコントローラに処理を書けば良い、ということになります。そして、そもそも<code>Route::resource</code>というメソッドがそれを勝手にいい感じに処理してくれるため、とりあえずは深く考える必要はありません。</p> <p>回答ページである<code>questions/{id}/received</code>のように、resourceの形ではなく独自で定義したい場合に<code>Route::get</code>や<code>Route::post</code>等を利用することができる、という感じです。</p> <p>マニュアルでもこのあたりは書かれています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://readouble.com/laravel/5.7/ja/controllers.html#resource-controllers">リソースコントローラ</a></p> <h3 id="回答処理を作成"><a href="#%E5%9B%9E%E7%AD%94%E5%87%A6%E7%90%86%E3%82%92%E4%BD%9C%E6%88%90">回答処理を作成</a></h3> <p>さて、では実際にAnswerControllerに回答の登録処理を作成します。</p> <pre><code class="php">namespace App\Http\Controllers; use App\Models\Answer; use App\Models\Question; use Auth; use Illuminate\Http\Request; class AnswerController extends Controller { public function store(Request $request) { $question = Question::where('id', $request->input('question_id')) ->where('received_user_id', Auth::id()) ->firstOrFail(); $answer = new Answer($request->all()); $answer->user_id = Auth::id(); $question->answer()->save($answer); session()->flash('success', '回答しました。'); return redirect("questions/{$question->id}/received"); } } </code></pre> <p>最初は回答フォームの処理と同様、自分に来た質問かどうかをチェックしています。</p> <p>次に回答データを作成しています。<code>$question->answer()->save($answer);</code>という処理により、その<code>$question</code>の配下になるように回答を保存してくれます。この保存の仕方だとわざわざ<code>$answer->question_id</code>を自分で代入しなくても勝手に入れて保存してくれます。先程モデルに定義したリレーションによってこれができるようになっています。</p> <p>これで実際に回答フォームから回答を登録すると正常にデータが入ります。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bd5d4614cf.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bd5d4614cf.png?mw=700" alt="" /></a></p> <h2 id="回答ページに回答済みの内容を表示する"><a href="#%E5%9B%9E%E7%AD%94%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AB%E5%9B%9E%E7%AD%94%E6%B8%88%E3%81%BF%E3%81%AE%E5%86%85%E5%AE%B9%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">回答ページに回答済みの内容を表示する</a></h2> <p>既に回答した質問に回答フォームを表示してしまうとおかしいため、回答済みの場合は回答データを表示するようにしておきます。回答ページのテンプレートの回答フォームを条件分岐で表示を切り替えます。ちょこちょこ見出しも変えたりしたためまるごと下記に書きます。</p> <pre><code class="html"><section class="text-center pt-4"> <h1 style="font-size: 1.5rem">届いた質問</h1> <div class="card mb-4"> <div class="card-body"> <span>{</span><span>{</span> $question->body <span>}</span><span>}</span> </div> </div> <h2 style="font-size: 1.2rem">回答</h2> @if ($question->answer) <div class="card"> <div class="card-body"> <span>{</span><span>{</span> $question->answer->body <span>}</span><span>}</span> </div> </div> @else {!! Form::open(['url' => url('answers')]) !!} {!! Form::hidden('question_id', $question->id) !!} <div class="form-group"> {!! Form::textarea('body', null, [ 'class' => 'form-control', 'placeholder' => '回答する', 'required' => true, ]) !!} </div> {!! Form::Submit('回答する', ['class' => 'btn btn-primary']) !!} {!! Form::close() !!} @endif </section> </code></pre> <p>リレーションを定義しているため、<code>$question->answer</code>という形で簡単に回答が取れます。回答データがあれば<code>@if</code>の方が実行されて回答が表示されます。回答がなければnullになりますので、<code>@else</code>の方が実行され、回答フォームが表示されます。回答データを直接phpMyAdmin等で削除してみたり再度回答してみたりすると切り替わるのが分かると思います。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bdaa76946e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9bdaa76946e.png?mw=700" alt="" /></a></p> <p>これで一旦回答機能の作成は完了です。</p> <h2 id="微調整"><a href="#%E5%BE%AE%E8%AA%BF%E6%95%B4">微調整</a></h2> <p>少し気になるところが出てきたため一旦微調整しておきます。</p> <h3 id="ナビを調整"><a href="#%E3%83%8A%E3%83%93%E3%82%92%E8%AA%BF%E6%95%B4">ナビを調整</a></h3> <p>ナビがcontainerの中に入って短く切れている感じが変だったので、containerから出してbodyの最初に配置しました。また、ナビの中にcontainerをいれて全体的な横幅を揃えました。</p> <pre><code class="html"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container" style="max-width: 600px"> : : </div> </nav> <div class="container" style="max-width: 600px"> @if (session('success')) <div class="alert alert-success" role="alert"> <span>{</span><span>{</span> session('success') <span>}</span><span>}</span> </div> @endif @yield('content') </div> </code></pre> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9c96ff13982.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9c96ff13982.png?mw=700" alt="" /></a></p> <h3 id="最初の余白を調整"><a href="#%E6%9C%80%E5%88%9D%E3%81%AE%E4%BD%99%E7%99%BD%E3%82%92%E8%AA%BF%E6%95%B4">最初の余白を調整</a></h3> <p>質問ページも回答ページも最初の要素に<code>pt-4</code>をいれて、ナビとくっつきすぎるのを防いでいました。しかし全てのページでこれをやるのは微妙すぎるため、ナビのmargin-bottomをつけて共通化するようにしました。</p> <p>具体的には各ページの<code>pt-4</code>を削除し、ナビに<code>mb-4</code>を入れました。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>質問形サービスの根幹となる機能がまた一つできました。まだ細かいところも大きいところもちょこちょこ残っていますので、引き続き進めていきます。次回は片手で小銭を最大何枚持てるかを検証していきたいと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15443 2019-10-01T08:01:35+09:00 2019-10-07T23:44:10+09:00 https://crieit.net/posts/Bootstrap Bootstrapでベースデザインを整える <p>今回は一旦Bootstrapでデザインを整えてみたいと思います。デザインを整えないまま開発を進めすぎてしまうと、いざデザインを組み込む時に今まで作ったページを一通り調整し直さなければなりません。そのためベースだけは最初に整えておいて、新規にページを作る際などは最初からデザインを組み込んでおく方が楽です。</p> <h2 id="npmの利用"><a href="#npm%E3%81%AE%E5%88%A9%E7%94%A8">npmの利用</a></h2> <p>BootstrapはCDNで提供されているURLから直接読み込んで利用することもできます。軽いですし個人的にはおすすめです。ただ、今回はnpmを利用して利用していきたいと思います。ビルドは重くなりますが、色のカスタマイズなどが必要になった時に簡単できるようになったりします。</p> <p>また、JavaScriptを使う際などにもどのみちnpmを利用していきますので、JavaScriptもCSSも一括でビルドされます。</p> <h3 id="Node.jsをインストール"><a href="#Node.js%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Node.jsをインストール</a></h3> <p>ということでPCにNode.jsが入っていない場合はNode.jsをインストールしておいてください。<a target="_blank" rel="nofollow noopener" href="https://nodejs.org/ja/">公式サイト</a>のインストーラを使う形でも構いませんし、Windows意外、もしくはWindowsの場合はWSLが使えるようであれば<a target="_blank" rel="nofollow noopener" href="https://github.com/nvm-sh/nvm">nvm</a>を使うと簡単にバージョンを切り替えられるためおすすめです。</p> <p>今回Node.js v10.16.3を利用していますので、合わせていただくと問題が出にくいと思います。(とはいえ最新のバージョン12とかでも恐らく動くのではないかと思いますが)</p> <h3 id="Yarnを利用"><a href="#Yarn%E3%82%92%E5%88%A9%E7%94%A8">Yarnを利用</a></h3> <p>また、パッケージマネージャとして<a target="_blank" rel="nofollow noopener" href="https://yarnpkg.com/lang/ja/">Yarn</a>を利用して解説していきます。npmをそのまま使う形でも問題ないですが、合わせたい場合はそちらもインストールしておいてください。</p> <h2 id="Bootstrapのインストール"><a href="#Bootstrap%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Bootstrapのインストール</a></h2> <p>本来であればYarnなどを利用してインストールの設定を行っていきますが、Laravelには<code>laravel/ui</code>という外部パッケージがあり、そちらでJavaScriptとCSSを初期化すると自動的にBootstrapもインストールされます。以前記事を書きましたので、先に下記を参考にVue.jsを入れるパターンでインストールをしておきます。</p> <p><a href="https://crieit.net/posts/Laravel-6-Vue-js-React">Laravel 6 でVue.jsやReactを使う</a></p> <h2 id="デザインを整える"><a href="#%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%92%E6%95%B4%E3%81%88%E3%82%8B">デザインを整える</a></h2> <p>では前回作成したユーザー詳細ページを調整していきます。<code>http://localhost:8000/users/ユーザー名</code>というURLです。<code>yarn hot</code>でホットリローディングした状態で開発を進めていきます。</p> <h3 id="ビルドしたjs、cssファイルを読み込む"><a href="#%E3%83%93%E3%83%AB%E3%83%89%E3%81%97%E3%81%9Fjs%E3%80%81css%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80">ビルドしたjs、cssファイルを読み込む</a></h3> <p>まずは共通レイアウトである<code>resources/views/layouts/app.blade.php</code>に<code>yarn hot</code>コマンドでLaravel Mixによってビルドされたjsファイルとcssファイルを読み込んでおきます。</p> <p>headタグ内にCSSの読み込みを追加します。</p> <pre><code class="html"> <link rel="stylesheet" href="<span>{</span><span>{</span> mix('css/app.css') <span>}</span><span>}</span>" type="text/css"> </code></pre> <p>bodyタグの最後にJavaScriptファイルの読み込みを追加します。</p> <pre><code class="html"> <script src="<span>{</span><span>{</span> mix('js/app.js') <span>}</span><span>}</span>"></script> </code></pre> <p>これでページをリロードして確認してみましょう。正常に読み込めていれば少し背景色や体裁などが変わります。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d914dbeb4c3c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d914dbeb4c3c.png?mw=700" alt="" /></a></p> <h2 id="Formを整えてみる"><a href="#Form%E3%82%92%E6%95%B4%E3%81%88%E3%81%A6%E3%81%BF%E3%82%8B">Formを整えてみる</a></h2> <p>BootstrapにはFormのコンポーネントが用意されています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/forms/">Forms · Bootstrap</a></p> <p>これで作成したフォームを整えてみます。また、箱系のサービスは大体中央寄せにしているのでそのあたりも調整してみます。</p> <p>こんな感じになりました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92057f6332b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92057f6332b.png?mw=700" alt="" /></a></p> <p>DevToolsのスマホ表示で見てみるとこんな感じです。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9205c9e7553.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d9205c9e7553.png?mw=700" alt="" /></a></p> <h3 id="レイアウトの調整"><a href="#%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E3%81%AE%E8%AA%BF%E6%95%B4">レイアウトの調整</a></h3> <p>全体レイアウトは具体的には下記のようにしました。</p> <pre><code class="html"><!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title><span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span></title> <link rel="stylesheet" href="<span>{</span><span>{</span> mix('css/app.css') <span>}</span><span>}</span>" type="text/css"> </head> <body> @if (session('success')) <div class="success"> <span>{</span><span>{</span> session('success') <span>}</span><span>}</span> </div> @endif <div class="container" style="max-width: 600px"> @yield('content') </div> <script src="<span>{</span><span>{</span> mix('js/app.js') <span>}</span><span>}</span>"></script> </body> </html> </code></pre> <ul> <li>viewportが抜けていたので入れました。これを入れないとスマホ表示が正しく行われません。Bootstrapの<a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/getting-started/introduction/#starter-template">Starter template</a>を参考にしています。</li> <li>コンテンツを<code>class="container"</code>で囲みました。Bootstrapはだいたいとりあえずcontainerで囲みます。</li> <li>横幅が広すぎるとtextareaが不格好だったので最大サイズを600pxにしました。</li> </ul> <h3 id="質問ページの調整"><a href="#%E8%B3%AA%E5%95%8F%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E8%AA%BF%E6%95%B4">質問ページの調整</a></h3> <p>質問ページは具体的には下記のようにしました。</p> <pre><code class="html"><section class="text-center pt-4"> <h1 style="font-size: 1.5rem"><span>{</span><span>{</span> $user->name <span>}</span><span>}</span></h1> <div class="mb-4">への質問</div> {!! Form::open(['url' => url('questions')]) !!} {!! Form::hidden('user', $user->unique_id) !!} <div class="form-group"> {!! Form::textarea('body', null, [ 'class' => 'form-control', 'placeholder' => '質問をする', 'required' => true, ]) !!} </div> {!! Form::Submit('質問する', ['class' => 'btn btn-primary']) !!} {!! Form::close() !!} </section> </code></pre> <ul> <li>全体をtext-centerクラスで囲んでセンタリングしました。箱系のサービスは大体そういうレイアウトだったような気がしますので。(参考:<a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/utilities/text/">Text · Bootstrap</a>)</li> <li><code>mb-4</code>を入れてマージンを調整しました。(参考:<a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/utilities/spacing/">Spacing · Bootstrap</a>)</li> <li>Formにクラスを指定して整えました。(参考:<a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/forms/">Form · Bootstrap</a>)</li> <li>requiredが抜けていたので入れておきました。ブラウザ側で簡易な入力必須チェックを行ってくれます。</li> </ul> <h3 id="雑にやる"><a href="#%E9%9B%91%E3%81%AB%E3%82%84%E3%82%8B">雑にやる</a></h3> <p>仕事でもなく一人でやるので、styleを直書きしたりととにかく雑で良いと思います。開発しづらくなってきたら共通化したり整えたりしましょう。</p> <h3 id="アラートの調整"><a href="#%E3%82%A2%E3%83%A9%E3%83%BC%E3%83%88%E3%81%AE%E8%AA%BF%E6%95%B4">アラートの調整</a></h3> <p>Flashメッセージの表示用にAlertコンポーネントを使います。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92114fd444f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92114fd444f.png?mw=700" alt="" /></a></p> <p>レイアウトを下記のように調整します。(ついでにcontainerの中に入れておきました)</p> <pre><code class="html"> @if (session('success')) <div class="alert alert-success" role="alert"> <span>{</span><span>{</span> session('success') <span>}</span><span>}</span> </div> @endif </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/alerts/">Alerts · Bootstrap</a></p> <h3 id="ナビゲーションバーを入れる"><a href="#%E3%83%8A%E3%83%93%E3%82%B2%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%90%E3%83%BC%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B">ナビゲーションバーを入れる</a></h3> <p>ナビゲーションバーをヘッダ部分に入れてみます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://getbootstrap.com/docs/4.3/components/navbar/#toggler">Navbar · Bootstrap</a></p> <p>このあたりから適当に選んで入れてみました。containerの最初に入れています。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92142a8597a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d92142a8597a.png?mw=700" alt="" /></a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>とりあえず今回はこんなところでしょうか。とりあえずざっと最初の体裁が決まったので今後もこれに沿って進めていきます。あとはまた必要な物が出たら順次検討していきます。次回はツイートは何分に1回なら引かれないかを検証してみたいと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15420 2019-09-24T07:30:49+09:00 2019-10-06T17:03:00+09:00 https://crieit.net/posts/3202fc623a9e4c7cfef33b4d3088834f 質問できるようにする <p>質問をできるようにするために、前回は質問を受け付ける側のユーザー登録を作りました。次の流れとして質問をしてもらうために質問を受け付けるためのユーザー詳細ページを作る必要があります。</p> <p>しかし、どちらかというと早く質問機能の方を作りたいため、受付ページは適当にデザインもなしの最低限で作成し、質問できる機能を進めていきたいと思います。</p> <h2 id="雑な質問投稿ページを作る"><a href="#%E9%9B%91%E3%81%AA%E8%B3%AA%E5%95%8F%E6%8A%95%E7%A8%BF%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E3%82%8B">雑な質問投稿ページを作る</a></h2> <p>とりあえず超雑に質問ページを作ります。URLは<code>users/ユニークID</code>にします。</p> <h3 id="ベースとなるテンプレートを作成"><a href="#%E3%83%99%E3%83%BC%E3%82%B9%E3%81%A8%E3%81%AA%E3%82%8B%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90">ベースとなるテンプレートを作成</a></h3> <p>「html5 雛形」で検索すると <a target="_blank" rel="nofollow noopener" href="https://qiita.com/NERD_009/items/2a8e1121a1960efcc977">HTML5の雛形&ざっくり解説 - Qiita</a> というページがあったのでこれをコピペして作ります。</p> <p>まず共通レイアウトを<code>resources/views/layouts/app.blade.php</code>として作成します。</p> <pre><code class="html"><!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title><span>{</span><span>{</span> config('app.name') <span>}</span><span>}</span></title> </head> <body> @if (session('success')) <div class="success"> <span>{</span><span>{</span> session('success') <span>}</span><span>}</span> </div> @endif @yield('content') </body> </html> </code></pre> <p><code>config('app.name')</code> には.envのAPP_NAMEで指定されているものが入ります。そして<code>yield('content')</code>には各ページのコンテンツが入ります。</p> <h3 id="ユーザーページを作成"><a href="#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E6%88%90">ユーザーページを作成</a></h3> <p>ユーザーページを作成していきます。とりあえずUserControllerを作ります。</p> <pre><code class="sh">php artisan make:controller UserController </code></pre> <p>app/Http/Controllers/UserController.phpが出来上がりますのでそれにuseやメソッドを追加します。</p> <pre><code class="php">namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; class UserController extends Controller { public function show($uniqueId) { $user = User::where('unique_id', $uniqueId)->firstOrFail(); return view('user.show', compact('user')); } } </code></pre> <p>次に表示するためのテンプレートを<code>resources/views/user/show.blade.php</code>として作成します。</p> <pre><code class="html">@extends('layouts.app') @section('content') <h1><span>{</span><span>{</span> $user->name <span>}</span><span>}</span></h1> @endsection </code></pre> <p>アクセスできるように<code>routes/web.php</code>にルーティングを追加します。</p> <pre><code class="php">Route::resource('users', 'UserController')->only(['show']); </code></pre> <p>これでアクセスしてみると自分のTwitter名が表示されます。URLは</p> <p><a target="_blank" rel="nofollow noopener" href="http://localhost:8000/users/dala00">http://localhost:8000/users/dala00</a></p> <p>という感じです。最後は自分のTwitterプロフィールのURLの末尾と同じものです。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8786f788f15.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8786f788f15.png?mw=700" alt="" /></a></p> <p>ひとまずこれでページの準備はできました。</p> <h2 id="質問機能を作っていく"><a href="#%E8%B3%AA%E5%95%8F%E6%A9%9F%E8%83%BD%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%84%E3%81%8F">質問機能を作っていく</a></h2> <p>とりあえずページができたので実際に機能を作っていきます。</p> <h3 id="Formヘルパーをインストール"><a href="#Form%E3%83%98%E3%83%AB%E3%83%91%E3%83%BC%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Formヘルパーをインストール</a></h3> <p>Formは自分で直接書くのではなく、下記のFormヘルパーを利用します。CRSFトークンを勝手に書いてくれたり何かと便利です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://laravelcollective.com/docs/6.0/html">LaravelCollective | HTML v6.0</a></p> <p>インストールします。</p> <pre><code>composer require laravelcollective/html </code></pre> <h3 id="Formを書く"><a href="#Form%E3%82%92%E6%9B%B8%E3%81%8F">Formを書く</a></h3> <p>先程のshowページに実際にフォームを書いてみます。</p> <pre><code class="html">{!! Form::open(['url' => url('questions')]) !!} {!! Form::hidden('user', $user->unique_id) !!} {!! Form::textarea('body') !!} {!! Form::Submit('質問する') !!} {!! Form::close() !!} </code></pre> <p>とりあえず大雑把にこんな感じでしょうか。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8789f7d0d4c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8789f7d0d4c.png?mw=700" alt="" /></a></p> <h3 id="モデルを作る"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E4%BD%9C%E3%82%8B">モデルを作る</a></h3> <p>次に実際にデータを保存するためのテーブルやそれ用のモデルなどを作っていきます。まずはマイグレーションを作ります。</p> <pre><code class="sh">php artisan make:migration create_questions --create=questions </code></pre> <p>とりあえずこんな感じでしょうか。昔の仕様を覚えてないのですが、ひとまず今は質問するのにもログインが必要そうですのでその仕様で進めます。サービスの内容的にログインさせていないと悪質なユーザーが現れた時に対処も難しそうですし。</p> <p>質問を作ったユーザーがuser_idで、受け取ったユーザーがreceived_user_idとしてあります。</p> <pre><code class="php"> Schema::create('questions', function (Blueprint $table) { $table->bigIncrements('id'); $table->bigInteger('user_id')->unsigned()->index(); $table->bigInteger('received_user_id')->unsigned()->index(); $table->text('body'); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('received_user_id')->references('id')->on('users')->onDelete('cascade'); }); </code></pre> <p>次にモデルを作ります。</p> <pre><code class="sh">php artisan make:model Models/Question </code></pre> <p>フォームから受け取った値でモデルを作成できるよう、<code>guarded</code>オプションを付けておきます。<code>new</code>したり<code>fill</code>メソッドでプロパティに一括で値を入れることができるようになります。</p> <pre><code class="php">class Question extends Model { protected $guarded = ['id']; } </code></pre> <p>そして質問を実際に登録するためのアクションを作成します。先程Formに指定した<code>questions</code>というURLにPOSTします。</p> <pre><code class="sh">php artisan make:controller QuestionController </code></pre> <p>実際にQuestionControllerにアクションを定義します。</p> <pre><code class="php">namespace App\Http\Controllers; use App\Models\Question; use App\Models\User; use Auth; use Illuminate\Http\Request; class QuestionController extends Controller { public function store(Request $request) { $user = User::where('unique_id', $request->input('user'))->firstOrFail(); $question = new Question(); $question->user_id = Auth::id(); $question->received_user_id = $user->id; $question->body = $request->input('body'); $question->save(); session()->flash('success', '質問を投稿しました。'); return redirect("users/{$user->unique_id}"); } } </code></pre> <p>アクセスできるように<code>routes/web.php</code>にルーティングを追加しておきます。authのmiddlewareを入れ、ログイン時にしか実行できないようにします。</p> <pre><code class="php">Route::group(['middleware' => ['auth']], function () { Route::resource('questions', 'QuestionController')->only(['store']); }); </code></pre> <p>これで一通り完了です。実際にフォームから投稿してみると下記のように完了メッセージが表示されます。メッセージは共通layoutにて表示しています。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d87982bde696.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d87982bde696.png?mw=700" alt="" /></a></p> <p>データを見てみると正しく追加されています。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8798bead79c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d8798bead79c.png?mw=700" alt="" /></a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>まだ色々細かい部分が抜けてはいますが、とりあえず質問機能のベース部分ができました。これから細かい部分をブラッシュアップしていきます。次回は洗濯物を干す時のベストな間隔について検討してみたいと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15405 2019-09-18T07:48:19+09:00 2019-12-29T23:17:59+09:00 https://crieit.net/posts/Laravel-Socialite-Twitter Laravel Socialiteを使ってTwitterアカウントでログイン機能 <p>まず最初にどこから作成していけばよいかを考えてみます。</p> <p>質問箱のようなサービスは、まずTwitterに投稿されるツイートを見て、そのツイートからサービスにアクセスし、そこから各ユーザーに質問を送る、という流れになっています。そのため、質問を送る側にログインは不要ですが、質問される側はユーザーとしての登録が必要になります。ということで全てのベースとなるユーザー登録の部分を作ってみたいと思います。</p> <h2 id="Twitterアカウントでログイン"><a href="#Twitter%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%A7%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3">Twitterアカウントでログイン</a></h2> <p>ログインはTwitterアカウントでログインできるようにします。基本的にほとんどがTwitterユーザーのアクセスになると思いますので、数回のクリックで簡単にログインできるTwitterログインが一番適していると思います。</p> <h3 id="Twitterアプリケーションを作成"><a href="#Twitter%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90">Twitterアプリケーションを作成</a></h3> <p>事前準備として、TwitterのデベロッパーダッシュボードにてTwitterアプリケーションを作成する必要があります。下記で作成を行ってください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/">Twitter Developer</a></p> <p>具体的な登録方法は下記などが参考になると思います。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kngsym2018/items/2524d21455aac111cdee">Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報 - Qiita</a></p> <h2 id="Laravel Socialiteをインストール"><a href="#Laravel+Socialite%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Laravel Socialiteをインストール</a></h2> <p>LaravelでのSNS認証はLaravel Socialiteを使うと楽です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://laravel.com/docs/6.x/socialite">Laravel Socialite - Laravel - The PHP Framework For Web Artisans</a></p> <p>とりあえずインストールだけ先にしておきましょう。</p> <pre><code class="sh">composer require laravel/socialite </code></pre> <h3 id="Twitterとの連携情報を設定"><a href="#Twitter%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA%E6%83%85%E5%A0%B1%E3%82%92%E8%A8%AD%E5%AE%9A">Twitterとの連携情報を設定</a></h3> <p>次に.envにTwitterアプリケーションとの連携情報を入れておきます。Twitter Developerページのアプリケーション詳細のここのConsumer API Keysというところです。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7ede1bec1a7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7ede1bec1a7.png?mw=700" alt="image.png" /></a></p> <pre><code class="env">TWITTER_CONSUMER_KEY=yourtwitterconsumerkey TWITTER_CONSUMER_SECRET=yourtwitterconsumersecret TWITTER_REDIRECT=http://localhost:8000/auth/twitter/callback </code></pre> <p>一応<code>.env.example</code>にも空のものをコピーしておきます。</p> <pre><code class="env">TWITTER_CONSUMER_KEY= TWITTER_CONSUMER_SECRET= TWITTER_REDIRECT= </code></pre> <p>上記は単に環境変数として指定しただけですので、これをLaravel側にも割り当てます。Socialiteには <a target="_blank" rel="nofollow noopener" href="https://socialiteproviders.netlify.com/providers/twitter.html">Socialite Provider</a>というページがあり、様々な認証方法が掲載されています。Twitterの設定方法の解説どおりにconfig/services.phpに下記を追記します。</p> <pre><code class="php"> 'twitter' => [ 'client_id' => env('TWITTER_CONSUMER_KEY'), 'client_secret' => env('TWITTER_CONSUMER_SECRET'), 'redirect' => env('TWITTER_REDIRECT'), ], </code></pre> <p>上記の設定を利用するためのパッケージをインストールします。</p> <pre><code class="sh">composer require socialiteproviders/twitter </code></pre> <p>このパッケージを有効化するため、config/app.phpのproviders配列に下記を追加します。</p> <pre><code class="php"> \SocialiteProviders\Manager\ServiceProvider::class, </code></pre> <p>これでLaravel側への設定自体は完了です。最後にTwitter側にリダイレクトURLを設定しておく必要があります。Twitter Developerのアプリケーション詳細画面から編集ボタンをクリックして編集画面を開き、先程.envに設定したリダイレクトURLである<code>http://localhost:8000/auth/twitter/callback</code>を下記の箇所に設定します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7ee59b92559.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7ee59b92559.png?mw=700" alt="" /></a></p> <h2 id="ログイン処理を実装"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E5%AE%9F%E8%A3%85">ログイン処理を実装</a></h2> <p>諸々の準備ができたので実際にログイン処理を実装していきます。とりあえずログイン用のコントローラを作成します。</p> <pre><code class="sh">php artisan make:controller AuthController </code></pre> <p>できあがったapp/Http/Controllers/AuthController.phpにログインの処理を入れるとひとまず下記のようになります。Socialiteをuseしてログイン用のアクションを入れています。</p> <pre><code class="php">namespace App\Http\Controllers; use Illuminate\Http\Request; use Socialite; class AuthController extends Controller { public function login() { return Socialite::with('Twitter')->redirect(); } public function callback() { $user = Socialite::driver('Twitter')->user(); dd($user); } } </code></pre> <p>上記をルーティングに追加してアクセスできるようにするためroutes/web.phpに下記を追記します。</p> <pre><code class="php">Route::prefix('auth')->group(function () { Route::get('twitter', 'AuthController@login'); Route::get('twitter/callback', 'AuthController@callback'); }); </code></pre> <p>これで一旦試してみましょう。下記にアクセスします。</p> <p><a target="_blank" rel="nofollow noopener" href="http://localhost:8000/auth/twitter">http://localhost:8000/auth/twitter</a></p> <p>問題なければ一旦Twitterのが画面が現れて、その後callback URLに戻り、callbackメソッドのddで認証したユーザー情報が表示されていると思います。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7eeb08ac314.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7eeb08ac314.png?mw=700" alt="" /></a></p> <p>エラーになる場合はなにか間違っている可能性があります。.envを編集した後は状況によってはキャッシュが効いている可能性もあるかもしれませんので<code>php artisan serve</code>等で起動している場合は一度起動し直してみたほうが良いかもしれません。</p> <p>何にしろここまでできたらほぼできたも同然です。あとは取得した情報をDBに入れて実際にそれでログインするだけです。</p> <h2 id="DBにユーザーデータの保存領域を作成"><a href="#DB%E3%81%AB%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E4%BF%9D%E5%AD%98%E9%A0%98%E5%9F%9F%E3%82%92%E4%BD%9C%E6%88%90">DBにユーザーデータの保存領域を作成</a></h2> <p>Laravelでは元々ユーザーデータの保存用のマイグレーションがあります。そのためその後の拡張用のマイグレーションを作っていきます。</p> <h3 id="SNS認証用のテーブル作成"><a href="#SNS%E8%AA%8D%E8%A8%BC%E7%94%A8%E3%81%AE%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E4%BD%9C%E6%88%90">SNS認証用のテーブル作成</a></h3> <p>SNS認証用のデータはsocial_usersというテーブルに保存することにします。usersに直接いれても良いのですが、セキュリティ上重要な情報を誤って漏らしてしまったり、今後GoogleやFacebook認証も追加したい、という時に大変になってしまう場合があるためusersテーブルと連携するためのテーブルとして作っておきます。</p> <p>ひとまずsocial_users用のマイグレーションファイルを作成します。<code>--create</code>オプションで新規テーブル作成のマイグレーション雛形を作ってくれます。</p> <pre><code class="sh">php artisan make:migration create_social_users --create social_users </code></pre> <p>database/migrationsフォルダにファイルが追加されているため、そちらを下記のように編集します。</p> <pre><code class="php"> Schema::create('social_users', function (Blueprint $table) { $table->bigIncrements('id'); $table->bigInteger('user_id')->unsigned()->index(); $table->string('provider_user_id')->index(); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); </code></pre> <p>今回はTwitterのAPIなどは利用しないため、上記だけにしています。利用する場合は適宜保存しておくと良いでしょう。</p> <p>ちなみにprovider_user_idというのは先程ddで出力した情報の<code>id</code>というところです。これはTwitter側でユニークですので、ログイン時にそれと照合し、存在しなければ新規ユーザー、存在すれば単にログインするだけ、という判定に使います。</p> <p>桁数が増えすぎて数値だと扱えない言語やライブラリがあったりするため、文字列でも提供されています。アプリケーション側でも文字列として扱います。</p> <p>provider_usr_idもuser_idも検索に利用しますのでインデックスを付けておきます。</p> <h3 id="ユーザーテーブルの拡張"><a href="#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E6%8B%A1%E5%BC%B5">ユーザーテーブルの拡張</a></h3> <p>ユーザー用のテーブルも拡張します。既存のマイグレーションは編集せず、追加のマイグレーションファイルを作成します。<code>--table</code>オプションで既存テーブル用のマイグレーション雛形を作ってくれます。</p> <pre><code>php artisan make:migration add_social_columns_to_users --table users </code></pre> <p>さっきのddでダンプした情報を見ながら必要な情報を入れるためのカラムを追加していきます。こんな感じでしょうか。</p> <pre><code class="php"> public function up() { Schema::table('users', function (Blueprint $table) { $table->string('unique_id')->after('id'); $table->string('avatar')->after('password'); $table->text('bio')->after('avatar'); $table->string('email')->nullable()->change(); $table->string('password')->nullable()->change(); }); } public function down() { Schema::table('users', function (Blueprint $table) { $table->string('password')->change(); $table->string('email')->change(); $table->dropColumn('bio'); $table->dropColumn('avatar'); $table->dropColumn('unique_id'); }); } </code></pre> <p>unique_idはTwitterのプロフィールのURLで利用されている値です。それをそのまま作成するアプリケーション側でも利用します。(実際にはTwitter側で変更されて競合したりおかしくなったりするパターンもありますが、ひとまずそれは無視します。必要に応じてそのあたりの処理は追々追加してください)</p> <h3 id="マイグレーションを実行する"><a href="#%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B">マイグレーションを実行する</a></h3> <p>なにはともあれここまでで一度マイグレーションしておきましょう。emailとpasswordは空でもOKなように変更しています。変更機能は下記のパッケージが必要ですので先にインストールしておきます。</p> <pre><code class="sh">composer require doctrine/dbal </code></pre> <p>マイグレーションを実行します。</p> <pre><code class="sh">php artisan migrate </code></pre> <p>問題なければ下記のように表示されます。</p> <pre><code>Migrating: 2019_09_16_020812_create_social_users Migrated: 2019_09_16_020812_create_social_users (0.03 seconds) Migrating: 2019_09_16_021714_add_social_columns_to_users Migrated: 2019_09_16_021714_add_social_columns_to_users (0.07 seconds) </code></pre> <h2 id="ログイン時にデータを保存する"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%99%82%E3%81%AB%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B">ログイン時にデータを保存する</a></h2> <p>DB側のデータも準備できたので、実際にログイン時の処理を書いていきます。先程ddしていただけのcallbackメソッドのところです。</p> <h3 id="モデルの配置を変える"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E9%85%8D%E7%BD%AE%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B">モデルの配置を変える</a></h3> <p>……とその前に、現在ユーザーのモデルがappフォルダ直下にあります。モデルが増える度にここにファイルが増えると邪魔ですので、app/Modelsフォルダを作ってそちらに移動しましょう。また、それに伴い各ファイルの中身も修正します。</p> <p>app/Models/User.php はnamespaceを合わせます。</p> <pre><code class="php">namespace App\Models; </code></pre> <p>あとは全体置換で<code>App\User</code>となっているところを<code>App\Models\User</code>に置換しておきます。置換前後でcommitしておくと<code>git reset</code>ですぐもとに戻したりおかしな差分を探せますので安心です。</p> <h3 id="モデルの連携"><a href="#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E9%80%A3%E6%90%BA">モデルの連携</a></h3> <p>まだsocial_usersのモデルがないため作っておきます。</p> <pre><code class="sh">php artisan make:model Models/SocialUser </code></pre> <p>そしてUserモデルと連携させましょう。User.phpに下記のメソッドを追加しておきます。</p> <pre><code class="php"> public function socialUsers() { return $this->hasMany(SocialUser::class); } </code></pre> <p>これでUserは複数のSocialUserを持っている、というアソシエーションの設定ができるため、例えば<code>$user->socialUsers</code>等で簡単に呼び出すことができるようになります。</p> <p>あとはSocialUser.phpに逆も定義しておきます。</p> <pre><code class="php">class SocialUser extends Model { public function user() { return $this->belongsTo(User::class); } } </code></pre> <h3 id="callbackの実装"><a href="#callback%E3%81%AE%E5%AE%9F%E8%A3%85">callbackの実装</a></h3> <p>さて、いい加減にcallbackメソッドを実装していきます。</p> <pre><code class="php"> public function callback() { $providerUser = Socialite::driver('Twitter')->user(); // 既に存在するユーザーかを確認 $socialUser = SocialUser::where('provider_user_id', $providerUser->id)->first(); if ($socialUser) { // 既存のユーザーはログインしてトップページへ Auth::login($socialUser->user, true); return redirect('/'); } // 新しいユーザーを作成 $user = new User(); $user->unique_id = $providerUser->nickname; $user->name = $providerUser->name; $user->avatar = $providerUser->user['profile_image_url_https']; $user->bio = $providerUser->user['description']; $socialUser = new SocialUser(); $socialUser->provider_user_id = $providerUser->id; DB::transaction(function () use ($user, $socialUser) { $user->save(); $user->socialUsers()->save($socialUser); }); Auth::login($user, true); return redirect('/'); } </code></pre> <p>これに合わせてuseも増やしています。</p> <pre><code class="php">use App\Models\User; use App\Models\SocialUser; use Auth; use DB; use Illuminate\Http\Request; use Socialite; </code></pre> <p>これで再度ログインURLにアクセスして認証し、トップページに戻ってくれば正常に完了できています。あとは念の為もう一度そのままログインを試して、データが増えてしまわないか、重複エラーなどが発生しないかも確認しておきましょう。問題なければユーザ登録&ログインは完成です。</p> <h2 id="ログインユーザーを表示"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%82%92%E8%A1%A8%E7%A4%BA">ログインユーザーを表示</a></h2> <p>しかしこれだとログインできているか分かりづらいため、welcome.blade.phpに下記のようなコードを追加してアイコンを表示してみましょう。</p> <pre><code class="html">@auth <div> <img src="<span>{</span><span>{</span> Auth::user()->avatar <span>}</span><span>}</span>" width="48" height="48"> </div> @endif </code></pre> <p>表示されました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7f0e89caf4d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5d7f0e89caf4d.png?mw=700" alt="" /></a></p> <p>ちなみに、<a target="_blank" rel="nofollow noopener" href="https://github.com/barryvdh/laravel-debugbar">barryvdh/laravel-debugbar</a>を導入しておけばフッターに表示されるデバッグバーでログイン状態も確認できます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>Twitterアカウントでログインできるところまでを作成しました。次回はカレーは何日寝かせれば一番美味しいかを検証していきたいと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15403 2019-09-17T08:52:07+09:00 2019-11-11T07:58:00+09:00 https://crieit.net/posts/5239e4c7dc9ec89b5d6cdf1419d6af90 はじめに <p>PHP & Laravelを使って質問箱のようなTwitterに投稿して遊ぶ質問サービスを作るチュートリアルの連載を書いていきます。実際に僕自身が1から作りながらWeb上に公開するまでのその工程と書いたソースの全てを書いていくことで、簡単なサービスづくりの基礎を誰でも学べるようにできればと考えています。</p> <h2 id="どのようなものが出来上がるか"><a href="#%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA%E3%82%82%E3%81%AE%E3%81%8C%E5%87%BA%E6%9D%A5%E4%B8%8A%E3%81%8C%E3%82%8B%E3%81%8B">どのようなものが出来上がるか</a></h2> <p>こんな感じでOGP付きのツイートが出来るようになります。</p> <p><a href="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5da72758df7f4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5da72758df7f4.png?mw=700" alt="" /></a></p> <p>届いた質問の管理</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7530a6c113.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5dc7530a6c113.png?mw=700" alt="" /></a></p> <p>(実際に作りながらここに画像などを増やしていきます)</p> <h2 id="前提"><a href="#%E5%89%8D%E6%8F%90">前提</a></h2> <p>いくつかの前提を説明します。</p> <h3 id="環境構築について"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">環境構築について</a></h3> <p>環境構築については悩みましたが、書かない予定です。色々なやり方があったり、やり方によってはお持ちのPCでできないものだったりする可能性もあるため、環境構築については世の中にある多くの記事を参考にして行っていただければと思います。もしくはDockerが使える方はdocker-composeを使うのが一番簡単かもしれません。一応Dockerによる構築は先日書いた <a href="https://crieit.net/posts/Docker-Laravel">DockerでLaravelのローカル開発環境構築を行う</a> かだいぶ前に書いた <a target="_blank" rel="nofollow noopener" href="https://qiita.com/dala00/items/963a8f65a7730ede8f3b">Qiitaの様なサービス作成中 ローカル環境構築 連載(1)</a> あたりが参考になるかもしれません。</p> <p>チュートリアルで使用する言語などは下記になります。</p> <ul> <li>PHP 7.3</li> <li>Laravel 6.0</li> <li>MySQL 5.7</li> <li>Node 12.10</li> <li>Vue.js 2.6</li> <li>Yarn</li> </ul> <p>各々連載中に説明をしてはいきますが、最低でも下記は準備して頂く必要があります。もしわからないところがある場合はコメント頂ければ追加で記事を書くか、分かりやすい記事の紹介などさせていただきます。もしくは何か書いていただけたらここからもリンクさせていただきます。</p> <h4 id="PHPのインストール"><a href="#PHP%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">PHPのインストール</a></h4> <p>コンソールで<code>php -v</code>を実行した時にPHPのバージョン7.3が表示されるようにしてください。</p> <h4 id="Laravelのインストール"><a href="#Laravel%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Laravelのインストール</a></h4> <p>Laravelの公式マニュアルに従い、インストールを行ってください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://readouble.com/laravel/6.0/ja/installation.html">インストール 6.0 Laravel</a></p> <p><code>php artisan serve</code>でサーバーを起動して開発していきます。</p> <h4 id="MySQLのインストール"><a href="#MySQL%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">MySQLのインストール</a></h4> <p>MySQLをインストールしてください。バージョンは5.6、5.7、8.0あたりであればどれでも大丈夫です。というか別のPostgresやSQLite等、他のものでも大丈夫かもしれません。</p> <p><code>.env</code>を編集し、接続できるようにしてください。最低でも、<code>php artisan migrate</code>で最初から用意されているユーザーテーブルなどが作成されるところまで確認を行ってください。</p> <h4 id="Nodeのインストール"><a href="#Node%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Nodeのインストール</a></h4> <p>JavaScriptのビルドに必要です。nvmを利用すると準備は簡単です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nvm-sh/nvm">nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions</a></p> <h3 id="内容について"><a href="#%E5%86%85%E5%AE%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">内容について</a></h3> <p>作りながら書き進めますので、開発中に間違ったことに気づいた場所などがあれば適宜記事の方も修正していくと思います。そのため書いたばかりの記事には誤りや最適でない内容などが含まれる可能性があります。</p> だら@Crieit開発者