20210613のPHPに関する記事は16件です。

【個人開発】独学開始から2ヶ月の実務未経験者が、制作期間1ヶ月で作ったポートフォリオアプリ

0. はじめに 実務未経験・独学開始から約2ヶ月のいわゆる”駆け出しエンジニア”の私が、制作期間約1ヶ月でポートフォリオのWebアプリを0から作ってみたので、ご紹介させて頂きたいと思います。 昨今いろんな先輩方のポートフォリオ作成記事が多く存在しますが、この記事も同じ初学者の方にとって、何か参考になれば幸いです。  簡単に自己紹介 現在24歳、大学を卒業し金融系企業に勤める3年目OL Web・IT技術に惹かれて独学でのプログラミング学習を開始し、さらにプログラミングの虜になりエンジニアへの転身を決意 好きなマンガはワンピース(人生の教科書) 1.使用技術 フロントエンド:JavaScript / jQuery 3.6.0 / Ajax バックエンド:PHP 7.3.11 / Laravel 8.38.0 データベース:MySQL インフラ:Heroku バージョン管理:Git / GitHub 2.実際のアプリ紹介 タイトル「Golf Score Share」 URL(スマホ・タブレット専用) http://golf-score-share.herokuapp.com/golf ※heorku無料版では30分ごとに使っていなければwebサーバーが停止する(スリープ状態)ようなので、起動に時間がかかるかと思います。ご了承くださいm(__)m GitHub https://github.com/hinakonagao/golf-score-share アプリ概要 ゴルフコースを回る際、Web上でスコアカードを共有し、一緒に回るメンバーが各々のスマホから共有のスコアカードを見たり、スコアを書き込めるアプリです。 使い方 スコアカード作成者 1. start a new gameをタップします。 2. ゲーム名・パスワードを決め、start a new gameをタップしてスコアカードを作成します。※過去に使われているゲーム名は使えません。 3. スコアカード画面に遷移します。 スコアカードへの参加者 1. join the game をタップします。 2. 作成者が登録したのと同じゲーム名・パスワードを入力し、join the gameをタップしてスコアカードに参加します。※ゲーム名・パスワードの一致が確認できなけれなエラーになります。 3. スコアカード画面に遷移します。 スコアカード画面の使い方 ・作成者・参加者の誰でも、スコアカードに書き込むことが出来ます。 ・スコアカードに書き込んだ時点で、自動的にデータは更新されます。(Ajax通信でデータ保存) ・他の人が更新した内容を自分の画面に反映させる為には、updateボタンを押してください。 (例)Aさんがスコアカードにhole1のスコアを入力。→Bさんが自分のスマホのupdateボタンを押すと、Aさんが入力したスコアがBさんの画面にも反映する。 開発背景 私自身ゴルフが好きでコースを回ることがあり、こんなサービスが欲しいなと思っていました。ゴルフ仲間にも相談すると、「実現すれば是非使いたい」「便利で面白い」と賛同が得られ、ゴルフ好きには需要があると考えました。 競合のいないブルーオーシャンだった。(個人でスコアカードを保存するスマホアプリや、同じスマホアプリをダウンロードしている人同士でスコアカードを共有できるサービスはありますが、誰でも準備不要ですぐに使えるサービスは見つかりませんでした。) 3.実装した機能 スコアカード新規作成 スコアカードが作成された際、プレーヤーのデータを自動生成 スコアカードに参加(既に作成されたスコアカードと一致するゲーム名・パスワードを入力すると、そのスコアカードに参加できる) スコアカード編集(スコアカードに参加した人は誰でも編集可能) ※編集したデータはAjax通信でデータベースへ保存 機能は少なく、シンプルなアプリかと思います。 ですがそれまでudemyなどで学習した、基本的なCRUD機能を持ったアプリやSNSアプリとは全く構成が違うので、どういう仕組みにすれば作りたい機能を実現できるのか0から考える事が難しくもあり、機能を実現するプログラムの流れを自分なりに考えて調べて実装できた時は本当に楽しいと感じました。 4.サービス設計・ER図 スコアカードを作成するとRoomテーブルにレコードが作成されると同時に、RoomPlayerテーブルにプレーヤー4人のレコードが自動生成されます。 スコアカードのマス目一つ一つがinputタグになっており、RoomPlayerテーブルへデータをPOSTする窓口であると同時に、データベースに保存されているスコアを表示する機能も兼ねています。 テーブル名 説明 Room スコアカード管理 RoomPlayer スコアカードに紐づくプレーヤーの名前・スコアを管理 5.工夫したこと バックエンド SQL発行回数を最小限に 〈当初〉RoomPlayerテーブルに4つのレコード(プレーヤー4人)を生成する際、1レコード生成するにあたり1回SQLを発行していました。 〈改善〉配列とinsert()を使って一気にデータ保存することで、SQL発行回数を4回→1回に抑えました。 * スコアカード更新の際の通信削減 〈当初〉スコアカードの値をひとつ変更する毎に、スコアカードのカラム全てについてデータベースの値を更新する処理を行なっていました。 〈改善〉フォームのinputタグにdata属性を指定し、jQueryで変更のあったカラムのみを取得することで、変更のあったカラムのみデータベースを更新する処理を可能にしました。 * Ajaxを使用した非同期通信によるデータ保存 スコアカードを更新した際は、Ajaxでデータベースへデータを保存するようにしました。 フロントエンド(UI) スコアカードの名前入力欄には、「名前を変更」という背景文字を表示 この点は、以下のようにユーザーの声を反映して改善を繰り返してきました。 ①プレーヤーのレコード生成時に、「プレーヤー1」「プレーヤー2」…というように名前の初期値をデータベースへ登録する。 →プレーヤー名を変更できることに気づいてもらえなかった。 ②データベースへ登録する初期値を「名前を変更」にする。 →名前を変更する際、一度消さなければいけないのが面倒 ③プレーヤーのレコード生成時、名前の初期値はnullとする。代わりにHTMLのinputタグのplaceholder属性で、「名前を変更」と背景文字を設定する。 スコアカードの表示方法 まずスコアカード上にRoomPlayerテーブルの該当レコード4つをforeachで表示しまし、スコアカードの縦横をCSSで入れ替える事で、スマホで見やすい縦向きのスコアカードを実現しました。 6.開発の進め方 このアプリ開発に取り組む前に、udemyの教材でLaravel・PHP・MySQLについて学びました。 【2日でできる】はじめての PHP 7 x Laravel 6 入門 https://www.udemy.com/course/php7study/ PHP+MySQL(MariaDB) Webサーバーサイドプログラミング入門 https://www.udemy.com/course/php7basic/ これらでMVCモデルの全体像やPHP・MySQLの基本を勉強し、あとは作りながら、必要なことを学ぶというやり方で進めました。 つまり、実装する機能(アウトプット)をまず決めて、そのために必要な知識を勉強(インプット)するということです。 その際によく活用したサイトも紹介させていただきます。 Laravel学習帳 https://laraweb.net/ ReaDouble https://readouble.com あとは公式ドキュメントや、偉大な先輩方のQiita記事もたくさん参考にさせていただきました。 udemyで学習する際には公式ドキュメントなどほとんど読んだ事がありませんでしたが、しっかり読んでみると公式ドキュメントが一番分かりやすかったりして、勉強になりました。 7.まだできていない・これからやりたいこと まだまだ出来ていない事はたくさんあります? また、開発を進める途中で実装したいなと思う機能もどんどん出てきました。 PHPUnitでのテスト AWSでデプロイ デバッグツールの活用(xdebug) Https化 ログイン・ユーザー登録機能を追加し、過去のスコアカードを振り返れるようにする。 現在はスコアカードを更新する際、変更したデータの保存のみAjaxで行なっているが、他の人のスコアカードへの反映もAjaxで実装する。 RoomPlayerテーブルにTotalカラムを追加する。 コンペ機能を追加する。(複数組でラウンドを回る際、他の組みのスコア進捗も見られるように) 前半後半の9ホールずつ表示し、タブで切り替えられるSPAにする。 8.さいごに 初めての個人開発だったので、どのぐらい時間が掛かるものなのか、目標設定の見当もつきませんでした。 そこでまず1ヶ月を目安として、あれこれと欲張らず最低限の機能だけでも「やり切る」ことを重視して開発に取り組みました。 結果、メイン機能は実装を終え無事デプロイまで辿り着きました。 先述の通り、まだまだ出来ていない事・やりたい事はあるので、これから少しずつ改修していきたいなと思います。 開発の進め方については、実装する機能(アウトプット)をまず決めて、そのために必要な知識を勉強(インプット)する方法は、やって良かった事と思っていて、これは是非同じ初学者の方々にもおすすめしたいです。 正直、udemyで作ったSNS風アプリをアレンジする…というようなことから始めれば良かったと思うこともあり、何度も挫けそうになりました。 ですが、udemyを真似て作るのとは比べ物にならないほど、自分で0から考えて調べてコードを書くと理解が深まりました。何より、自分の力で書いたプログラムが動く瞬間は、嬉しくて楽しくてニヤけずにはいられませんでした(笑) 今後の最優先課題 MVCモデルやプログラムの流れについては少しだけ理解が出来たかなと思う一方、Webの根本的な技術・仕組みについてもっと知らなければいけないという課題を感じたので、勉強していこうと思います。 この記事について間違っている箇所があれば、ご指摘頂けると幸いです。 拙い文章でしたが、最後までご覧いただき本当にありがとうございました!!?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】独学開始から2ヶ月の実務未経験者が、制作期間1ヶ月で作ったポートフォリオアプリについて

0. はじめに 実務未経験・独学開始から約2ヶ月のいわゆる”駆け出しエンジニア”の私が、制作期間約1ヶ月でポートフォリオのWebアプリを0から作ってみたので、ご紹介させて頂きたいと思います。 昨今いろんな先輩方のポートフォリオ作成記事が多く存在しますが、この記事も同じ初学者の方にとって、何か参考になれば幸いです。  簡単に自己紹介 現在24歳、大学を卒業し金融系企業に勤める3年目OL Web・IT技術に惹かれて独学でのプログラミング学習を開始し、さらにプログラミングの虜になりエンジニアへの転身を決意 好きなマンガはワンピース(人生の教科書) 1.使用技術 フロントエンド:JavaScript / jQuery 3.6.0 / Ajax バックエンド:PHP 7.3.11 / Laravel 8.38.0 データベース:MySQL インフラ:Heroku バージョン管理:Git / GitHub 2.実際のアプリ紹介 タイトル「Golf Score Share」 URL(スマホ・タブレット専用) http://golf-score-share.herokuapp.com/golf ※heorku無料版では30分ごとに使っていなければwebサーバーが停止する(スリープ状態)ようなので、起動に時間がかかるかと思います。ご了承くださいm(__)m GitHub https://github.com/hinakonagao/golf-score-share アプリ概要 ゴルフコースを回る際、Web上でスコアカードを共有し、一緒に回るメンバーが各々のスマホから共有のスコアカードを見たり、スコアを書き込めるアプリです。 使い方 スコアカード作成者 1. start a new gameをタップします。 2. ゲーム名・パスワードを決め、start a new gameをタップしてスコアカードを作成します。※過去に使われているゲーム名は使えません。 3. スコアカード画面に遷移します。 スコアカードへの参加者 1. join the game をタップします。 2. 作成者が登録したのと同じゲーム名・パスワードを入力し、join the gameをタップしてスコアカードに参加します。※ゲーム名・パスワードの一致が確認できなけれなエラーになります。 3. スコアカード画面に遷移します。 スコアカード画面の使い方 ・作成者・参加者の誰でも、スコアカードに書き込むことが出来ます。 ・スコアカードに書き込んだ時点で、自動的にデータは更新されます。(Ajax通信でデータ保存) ・他の人が更新した内容を自分の画面に反映させる為には、updateボタンを押してください。 (例)Aさんがスコアカードにhole1のスコアを入力。→Bさんが自分のスマホのupdateボタンを押すと、Aさんが入力したスコアがBさんの画面にも反映する。 開発背景 私自身ゴルフが好きでコースを回ることがあり、こんなサービスが欲しいなと思っていました。ゴルフ仲間にも相談すると、「実現すれば是非使いたい」「便利で面白い」と賛同が得られ、ゴルフ好きには需要があると考えました。 競合のいないブルーオーシャンだった。(個人でスコアカードを保存するスマホアプリや、同じスマホアプリをダウンロードしている人同士でスコアカードを共有できるサービスはありますが、誰でも準備不要ですぐに使えるサービスは見つかりませんでした。) 3.実装した機能 スコアカード新規作成 スコアカードが作成された際、プレーヤーのデータを自動生成 スコアカードに参加(既に作成されたスコアカードと一致するゲーム名・パスワードを入力すると、そのスコアカードに参加できる) スコアカード編集(スコアカードに参加した人は誰でも編集可能) ※編集したデータはAjax通信でデータベースへ保存 機能は少なく、シンプルなアプリかと思います。 ですがそれまでudemyなどで学習した、基本的なCRUD機能を持ったアプリやSNSアプリとは全く構成が違うので、どういう仕組みにすれば作りたい機能を実現できるのか0から考える事が難しくもあり、機能を実現するプログラムの流れを自分なりに考えて調べて実装できた時は本当に楽しいと感じました。 4.サービス設計・ER図 スコアカードを作成するとRoomテーブルにレコードが作成されると同時に、RoomPlayerテーブルにプレーヤー4人のレコードが自動生成されます。 スコアカードのマス目一つ一つがinputタグになっており、RoomPlayerテーブルへデータをPOSTする窓口であると同時に、データベースに保存されているスコアを表示する機能も兼ねています。 テーブル名 説明 Room スコアカード管理 RoomPlayer スコアカードに紐づくプレーヤーの名前・スコアを管理 5.工夫したこと バックエンド SQL発行回数を最小限に 〈当初〉RoomPlayerテーブルに4つのレコード(プレーヤー4人)を生成する際、1レコード生成するにあたり1回SQLを発行していました。 〈改善〉配列とinsert()を使って一気にデータ保存することで、SQL発行回数を4回→1回に抑えました。 * スコアカード更新の際の通信削減 〈当初〉スコアカードの値をひとつ変更する毎に、スコアカードのカラム全てについてデータベースの値を更新する処理を行なっていました。 〈改善〉フォームのinputタグにdata属性を指定し、jQueryで変更のあったカラムのみを取得することで、変更のあったカラムのみデータベースを更新する処理を可能にしました。 * Ajaxを使用した非同期通信によるデータ保存 スコアカードを更新した際は、Ajaxでデータベースへデータを保存するようにしました。 フロントエンド(UI) スコアカードの名前入力欄には、「名前を変更」という背景文字を表示 この点は、以下のようにユーザーの声を反映して改善を繰り返してきました。 ①プレーヤーのレコード生成時に、「プレーヤー1」「プレーヤー2」…というように名前の初期値をデータベースへ登録する。 →プレーヤー名を変更できることに気づいてもらえなかった。 ②データベースへ登録する初期値を「名前を変更」にする。 →名前を変更する際、一度消さなければいけないのが面倒 ③プレーヤーのレコード生成時、名前の初期値はnullとする。代わりにHTMLのinputタグのplaceholder属性で、「名前を変更」と背景文字を設定する。 スコアカードの表示方法 まずスコアカード上にRoomPlayerテーブルの該当レコード4つをforeachで表示しまし、スコアカードの縦横をCSSで入れ替える事で、スマホで見やすい縦向きのスコアカードを実現しました。 6.開発の進め方 このアプリ開発に取り組む前に、udemyの教材でLaravel・PHP・MySQLについて学びました。 【2日でできる】はじめての PHP 7 x Laravel 6 入門 https://www.udemy.com/course/php7study/ PHP+MySQL(MariaDB) Webサーバーサイドプログラミング入門 https://www.udemy.com/course/php7basic/ これらでMVCモデルの全体像やPHP・MySQLの基本を勉強し、あとは作りながら、必要なことを学ぶというやり方で進めました。 つまり、実装する機能(アウトプット)をまず決めて、そのために必要な知識を勉強(インプット)するということです。 その際によく活用したサイトも紹介させていただきます。 Laravel学習帳 https://laraweb.net/ ReaDouble https://readouble.com あとは公式ドキュメントや、偉大な先輩方のQiita記事もたくさん参考にさせていただきました。 udemyで学習する際には公式ドキュメントなどほとんど読んだ事がありませんでしたが、しっかり読んでみると公式ドキュメントが一番分かりやすかったりして、勉強になりました。 7.まだできていない・これからやりたいこと まだまだ出来ていない事はたくさんあります? また、開発を進める途中で実装したいなと思う機能もどんどん出てきました。 PHPUnitでのテスト AWSでデプロイ デバッグツールの活用(xdebug) Https化 ログイン・ユーザー登録機能を追加し、過去のスコアカードを振り返れるようにする。 現在はスコアカードを更新する際、変更したデータの保存のみAjaxで行なっているが、他の人のスコアカードへの反映もAjaxで実装する。 RoomPlayerテーブルにTotalカラムを追加する。 コンペ機能を追加する。(複数組でラウンドを回る際、他の組みのスコア進捗も見られるように) 前半後半の9ホールずつ表示し、タブで切り替えられるSPAにする。 8.さいごに 初めての個人開発だったので、どのぐらい時間が掛かるものなのか、目標設定の見当もつきませんでした。 そこでまず1ヶ月を目安として、あれこれと欲張らず最低限の機能だけでも「やり切る」ことを重視して開発に取り組みました。 結果、メイン機能は実装を終え無事デプロイまで辿り着きました。 先述の通り、まだまだ出来ていない事・やりたい事はあるので、これから少しずつ改修していきたいなと思います。 開発の進め方については、実装する機能(アウトプット)をまず決めて、そのために必要な知識を勉強(インプット)する方法は、やって良かった事と思っていて、これは是非同じ初学者の方々にもおすすめしたいです。 udemyを真似て作るのとは比べ物にならないほど、自分で0から考えて調べてコードを書くと理解が深まりました。何より、自分の力で書いたプログラムが動く瞬間は、嬉しくて楽しくてニヤけずにはいられませんでした(笑) 今後の最優先課題 MVCモデルやプログラムの流れについては少しだけ理解が出来たかなと思う一方、Webの根本的な技術・仕組みについてもっと知らなければいけないという課題を感じたので、勉強していこうと思います。 この記事について間違っている箇所があれば、ご指摘頂けると幸いです。 拙い文章でしたが、最後までご覧いただき本当にありがとうございました!!?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ラジオボタンのバリデーションエラー時の値の保持で悩んだ話

背景 今回の共同開発においてラジオボタンを実装をする必要があったがラジオボタンを実装した経験がなくどのようにするのがわからず苦労したのでそのことについてまとめていく。 仕様 カテゴリーは「Laravel」「PHP」「Docker」「web基礎」とする。 バリデーションがエラーになった場合は画面をリダイレクトする。 この際に値を保持する。(これに悩まされた。) ラジオボタンについて サンプルコード <input type="radio" name="example" value="サンプル">サンプル ※<label>を用いれば丸い箇所だけではなく、文字の部分をクリックしても選択することができる。 この場合<input>にidを、<label>にforを用いてお互いを同じ名前で紐付けしておく必要がある。 type="radio" ・・・ radioを指定すると作成できる。 name=" " ・・・ 複数のラジオボタンを作成する場合同じ名前にすると他の部分を選択したときに自動的に選択が解除される。 value= " " ・・・ 選択されているときに送信される内容。必須。 checked ・・・ この属性が指定されたラジオボタンは最初から選択された状態になる。(グループの中で一つだけ) 実際のコード create.php <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="category_id" id="category1" value="1" {{ old('category_id') == 1 ? 'checked' : '' }}/> <label class="form-check-label" for="category1">Laravel</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="category_id" id="category2" value="2" {{ old('category_id') == 2 ? 'checked' : '' }}/> <label class="form-check-label" for="category2">PHP</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="category_id" id="category3" value="3" {{ old('category_id') == 3 ? 'checked' : '' }}/> <label class="form-check-label" for="category3">Docker</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="category_id" id="category4" value="4" {{ old('category_id') == 4 ? 'checked' : '' }}/> <label class="form-check-label" for="category4">web基礎</label> </div> バリデーションエラー時の値の保持 Laravelにおいてバリデーションエラー時の値の保持はグローバルヘルパ関数のoldを用いることで利用できる。 <input type="text" name="title" value="{ old('フォームのキー名')}" この場合ではold('title')とすれば入力されたタイトルが保持される。 ここまではわかっていたがラジオボタンの場合どうやって値を保持するのかがわからなく苦戦。最終的には三項演算子を用いるという答えにたどり着いた。 三項演算子とは 条件式 ? 真の場合 : 偽の場合 条件式を評価してその答えが真であれば真の場合の式を返し、偽の場合は偽の場合の式を返す。 例 example.php <?php $var = 100; $str = ($var % 10 === 0) ? 10の倍数である : 10の倍数でない; echo $var."は".$srt; ///結果)100は10の倍数である ?> つまりはIF文と同じだが、IF文と違って一文でまとめられてスッキリしており、加えて上記のように変数に代入することもできるメリットが有る。 実際に使ってみた <input class="form-check-input" type="radio" name="category_id" id="category1" value="1" {{ old('category_id') == 1 ? 'checked' : '' }}/> ざっくりと表すと {{ category_idが選択されているかいないか比較 ? checked : なにも選択しない }} ということになる。三項演算子ってこういう風に使うのかと感動。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 5.8.38でマルチユーザー認証を実装する

Laravel 5.8.38でマルチユーザー認証を実装したいと思ったが、あまり参考記事がなかったので色々調べて何とかできたのでその方法を記載します。 config/auth.php(認証設定)の設定 以下のように設定します。 config.php <?php return [ /* |-------------------------------------------------------------------------- | Authentication Defaults |-------------------------------------------------------------------------- | | This option controls the default authentication "guard" and password | reset options for your application. You may change these defaults | as required, but they're a perfect start for most applications. | */ 'defaults' => [ 'guard' => 'web', 'passwords' => 'users', ], /* |-------------------------------------------------------------------------- | Authentication Guards |-------------------------------------------------------------------------- | | Next, you may define every authentication guard for your application. | Of course, a great default configuration has been defined for you | here which uses session storage and the Eloquent user provider. | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | Supported: "session", "token" | */ 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', 'hash' => false, ], /** 以下を追加 **/ 'user' => [ 'driver' => 'session', 'provider' => 'users', ], 'admin' => [ 'driver' => 'session', 'provider' => 'admins', ], /** ここまで **/ ], /* |-------------------------------------------------------------------------- | User Providers |-------------------------------------------------------------------------- | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | If you have multiple user tables or models you may configure multiple | sources which represent each model / table. These sources may then | be assigned to any extra authentication guards you have defined. | | Supported: "database", "eloquent" | */ 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], /** 以下を追加 **/ 'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, ], /** ここまで **/ ], /* |-------------------------------------------------------------------------- | Resetting Passwords |-------------------------------------------------------------------------- | | You may specify multiple password reset configurations if you have more | than one user table or model in the application and you want to have | separate password reset settings based on the specific user types. | | The expire time is the number of minutes that the reset token should be | considered valid. This security feature keeps tokens short-lived so | they have less time to be guessed. You may change this as needed. | */ 'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => 'password_resets', 'expire' => 60, ], /** 以下を追加 **/ 'admins' => [ 'provider' => 'admins', 'table' => 'password_resets', 'expire' => 60, ], /** ここまで **/ ], ]; adminsテーブルを作成するためマイグレーションファイルを作成する php artisan make:migration create_admins_table 内容は、ユーザーテーブルのマイグレーションファイルを参考にする。 <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateAdminsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('admins', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('admins'); } } Adminモデルを作成する app配下にModelsディレクトリを作成する。 add/Models/Adminを作成する。 内容はUserモデルを参考にする。 <?php namespace App\Models; use Illuminate\Notifications\Notifiable; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\Model; class Admin extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; } 認証エラー時の設定 app/Exceptions/Handler.phpに未ログイン時の対応(unauthenticated)を設定。 Illuminate\Auth\AuthenticationExceptionを継承しているのでunauthenticatedメソッドをオーバーライド。 app/Exceptions/Handler.php app/Exceptions/Handler.php <?php namespace App\Exceptions; use Exception; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; /** 以下追加 **/ use Request; use Response; use Illuminate\Auth\AuthenticationException; /** ここまで **/ class Handler extends ExceptionHandler { protected $dontReport = [ // ]; protected $dontFlash = [ 'password', 'password_confirmation', ]; public function report(Exception $exception) { parent::report($exception); } public function render($request, Exception $exception) { return parent::render($request, $exception); } /** 以下追加 **/ protected function unauthenticated($request, AuthenticationException $exception) { // JSONの対応 if($request->expectsJson()) { return response()->json(['error' => 'Unauthenticated.'], 401); } // Adminの対応 if(in_array('admin', $exception->guards())){ return redirect()->guest('admin/login'); } // Userの対応 return redirect()->guest(route('login')); } /** ここまで **/ } ルーティングの設定 route/web.php web.php // 'middleware'=>'auth:admin'を追加 Route::group(['prefix' => 'admin', 'middleware'=>'auth:admin'], function(){ //管理者ログインしたユーザーのみアクセス可能にしたいルーティングを記述 Route::get('home', 'Admin\HomeController@index')->name('admin.home'); }); //ログインやログアウト後のページに関しては、非ログイン時にアクセスするので'middleware'=>'auth:admin'の外に記述する Route::get('admin/login', 'Admin\LoginController@showLoginForm')->name('admin_login'); Route::post('admin/login', 'Admin\LoginController@login')->name('admin_login'); Route::post('admin/logout', 'Admin\LoginController@logout')->name('admin_logout'); Route::get('admin/register', 'Admin\RegisterController@showRegisterForm')->name('admin.register'); Route::post('admin/register', 'Admin\RegisterController@register')->name('admin.register'); 管理者ユーザーなので登録などは画面からさせない場合は、登録に関するregisterの部分は必要ありません。 コントローラーの設定 app ┗ Http ┗ Controllers ┠ Admin ┃ ┠ HomeController.php ┃ ┠ RegisterController.php ┃ ┗ LoginController.php ┠ Auth ┃ ┠ ForgotPasswordController.php ┃ ┠ LoginController.php // 変更する ┃ ┠ RegisterController.php ┃ ┠ ResetPasswordController.php ┃ ┗ VerificationController.php ┠ Controller.php ┗ HomeController.php Admin/HomeController.php <?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; // 追記 class HomeController extends Controller { public function __construct() { $this->middleware('auth:admin'); // 変更 } public function index() { return view('admin.home'); // 変更 } } Admin/LoginController.php <?php namespace App\Http\Controllers\Admin; // 変更 use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; // 追加※ class LoginController extends Controller { use AuthenticatesUsers; protected $redirectTo = '/admin/home'; // 変更 public function __construct() { $this->middleware('guest:admin')->except('logout'); // 変更 } /** 以下追記 **/ public function showLoginForm() { return view('admin.login'); } protected function guard() { return \Auth::guard('admin'); } public function logout(Request $request) { \Auth::guard('admin')->logout(); return redirect('/admin/login'); } /** ここまで **/ } Admin/RegisterController.php <?php namespace App\Http\Controllers\Admin; use App\Models\Admin; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RegistersUsers; class RegisterController extends Controller { /* |-------------------------------------------------------------------------- | Register Controller |-------------------------------------------------------------------------- | | This controller handles the registration of new users as well as their | validation and creation. By default this controller uses a trait to | provide this functionality without requiring any additional code. | */ use RegistersUsers; /** * Where to redirect users after registration. * * @var string */ protected $redirectTo = '/admin/home'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest:admin'); } public function showRegisterForm() { return view('admin.register'); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return \App\Models\Admin */ protected function create(array $data) { return Admin::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } protected function guard() { return \Auth::guard('admin'); } } 【Auth/LoginController.php修正】 ※Userだけログアウト app/Http/Controllers/Auth/LoginController.php Auth/LoginController.php use Illuminate\Http\Request; // 追記 class LoginController extends Controller { ・・・ public function __construct() { //$this->middleware('guest')->except('logout'); $this->middleware('guest:user')->except('logout'); // 変更 } /** 以下追記 **/ public function showLoginForm() { return view('auth.login'); } protected function guard() { return \Auth::guard('user'); } public function logout(Request $request) { \Auth::guard('user')->logout(); return redirect('/login'); } /** ここまで **/ 【User側HomeController.php修正】 ※Userだけログアウト app/Http/Controllers/HomeController.php HomeController.php public function __construct() { // $this->middleware('auth'); $this->middleware('auth:user'); } ビューの作成 以下の構成にする。 ┗ Views ┠ admin ┃ ┠ home.blade.php ┃ ┠ register.blade.php ┃ ┗ login.blade.php ┠ auth ┃ ┠ passwords ┃ ┠ register.blade.php ┃ ┠ login.blade.php ┃ ┗ verify.blade.php ┠ layout ┃ ┠ admin.blade.php ┃ ┠ app.blade.php ┠ home.blade.php ┗ welcom.blade.php resources/views/layouts/admin.blade.php route変更 route(‘login’) -> route(‘admin.login’) route(‘logout’) -> route(‘admin.logout’) resources/views/layouts/admin.blade.php <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Scripts --> <script src="{{ asset('js/app.js') }}" defer></script> <!-- Fonts --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm"> <div class="container"> <a class="navbar-brand" href="{{ url('/') }}"> {{ config('app.name', 'Laravel') }} </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- Left Side Of Navbar --> <ul class="navbar-nav mr-auto"> </ul> <!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest <li class="nav-item"> <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a> </li> @if (Route::has('register')) <li class="nav-item"> <a class="nav-link" href="{{ route('admin.register') }}">{{ __('Register') }}</a> </li> @endif @else <li class="nav-item dropdown"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{ Auth::user()->name }} <span class="caret"></span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{{ route('admin.logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> {{ __('Logout') }} </a> <form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;"> @csrf </form> </div> </li> @endguest </ul> </div> </div> </nav> <main class="py-4"> @yield('content') </main> </div> </body> </html> resources/views/admin/home.blade.php 継承元変更 @extends(‘layouts.app’) -> @extends(‘layouts.admin’) resources/views/admin/home.blade.php @extends('layouts.admin') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Dashboard</div> <div class="card-body"> @if (session('status')) <div class="alert alert-success" role="alert"> {{ session('status') }} </div> @endif You are logged in! </div> </div> </div> </div> </div> @endsection resources/views/auth/login.blade.php 継承元変更 @extends(‘layouts.app’) -> @extends(‘layouts.admin’) route変更 route(‘login’) -> route(‘admin.login’) resources/views/auth/login.blade.php @extends('layouts.admin') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Login') }}</div> <div class="card-body"> <form method="POST" action="{{ route('admin.login') }}"> @csrf <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <div class="col-md-6 offset-md-4"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}> <label class="form-check-label" for="remember"> {{ __('Remember Me') }} </label> </div> </div> </div> <div class="form-group row mb-0"> <div class="col-md-8 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Login') }} </button> @if (Route::has('password.request')) <a class="btn btn-link" href="{{ route('password.request') }}"> {{ __('Forgot Your Password?') }} </a> @endif </div> </div> </form> </div> </div> </div> </div> </div> @endsection resources/views/auth/register.blade.php 継承元変更 @extends(‘layouts.app’) -> @extends(‘layouts.admin’) route変更 route(‘login’) -> route(‘admin.login’) resources/views/auth/register.blade.php @extends('layouts.admin') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Register') }}</div> <div class="card-body"> <form method="POST" action="{{ route('admin.register') }}"> @csrf <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus> @error('name') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email"> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password"> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Register') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection こちらでマルチユーザー認証を実装することができました。 ちなみにこれでエラーが出る場合は、namespaseのパスが間違っていたりなどが多いと思いますので、エラーが出る場合は見直してみるようにして下さい。 私も、パスが間違っていて表示されなかったりで苦戦しました。。笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Laravel8 + Jetstream + Inertia」Vuetifyのインストールに手を焼いた・・・

概要 Laravel8系にvuetifyのインストールを試みたところ、vueのバージョンとvuetifyの依存関係によってインストールすることができず、かなり苦労しました。 今回は、その解決方法を備忘録として残します。 環境(version) Ucanさんの「最強のLaravel開発環境をDockerを使って構築する」をお借りします Docker環境の構築について、わからない方は上記の記事を参考にしていただけると有り難く思います。 docker-laravel/backend/package.json "@inertiajs/inertia": "^0.8.4", "@inertiajs/inertia-vue3": "^0.3.5", "@inertiajs/progress": "^0.2.4", "@tailwindcss/forms": "^0.2.1", "@tailwindcss/typography": "^0.3.0", "@vue/compiler-sfc": "^3.0.5", "axios": "^0.21", "laravel-mix": "^6.0.6", "lodash": "^4.17.19", "postcss": "^8.1.14", "postcss-import": "^12.0.1", "tailwindcss": "^2.0.1", "vue": "^3.0.5", "vue-loader": "^16.1.2" docker-laravel/backend/composer.json "php": "^7.3|^8.0", "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", "inertiajs/inertia-laravel": "^0.3.5", "laravel/framework": "^8.40", "laravel/jetstream": "^2.3", "laravel/sanctum": "^2.11", "laravel/tinker": "^2.5", "tightenco/ziggy": "^1.0" "facade/ignition": "^2.5", "fakerphp/faker": "^1.9.1", "laravel/sail": "^1.0.1", "mockery/mockery": "^1.4.2", "nunomaduro/collision": "^5.0", "phpunit/phpunit": "^9.3.3" vuetifyのインストールを試みる エラーが発生 $ npm i vuetify npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: undefined@undefined npm ERR! Found: vue@3.1.1 npm ERR! node_modules/vue npm ERR! dev vue@"^3.0.5" from the root project npm ERR! npm ERR! Could not resolve dependency: npm ERR! peer vue@"^2.6.4" from vuetify@2.5.3 npm ERR! node_modules/vuetify npm ERR! vuetify@"*" from the root project npm ERR! npm ERR! Fix the upstream dependency conflict, or retry npm ERR! this command with --force, or --legacy-peer-deps npm ERR! to accept an incorrect (and potentially broken) dependency resolution. npm ERR! npm ERR! See /Users/nakada/.npm/eresolve-report.txt for a full report. npm ERR! A complete log of this run can be found in: npm ERR! /Users/nakada/.npm/_logs/2021-06-12T05_00_16_070Z-debug.log 原因 jetsotream2.3バージョンではVue 3(2021月6月13日時点)を利用しており、 Vue 3はvuetifyに対応していません。 細く言えば、現在のVue 2とVue 3はView部分の(template)の実装が異なり、ビルドする方法が異なります。 Vue 2がvue-template-cmplilerを利用するのに対して、Vue3 は@vue.compiler-sfcを利用しています。 解決方法 今回は、 削除するファイルや不要なファイルがプロジェクトに残らないようにするためLaravelの導入から始めます。 Laravelのプロジェクトを作成 Ucanさんのdocker環境ではmakeコマンドを使用することでプロジェクトを作成することができます $ make create-project Jetstreamのバージョンを2.1に下げる Vue 2を利用するため、Jetstreamのバージョンを指定し再度インストール $ composer require laravel/jetstream:2.1 inertiaをインストールする $ php artisan jetstream:install inertia npmインストール & ビルド $ npm install && npm run dev Vuetifyのインストール $ npm i vuetify Vuetifyがインストールされていることを確認 $ npm list vue backend@ /Users/ユーザー名/workspace/docker-laravel/backend ├─┬ @inertiajs/inertia-vue@0.5.12 │ └── vue@2.6.14 deduped ├─┬ portal-vue@2.1.7 │ └── vue@2.6.14 deduped ├── vue@2.6.14 └─┬ vuetify@2.5.3 └── vue@2.6.14 その他 以上で、vuetifyをインストールすることができます! この記事が役に立った方は、LGTMのクリックをお願い致します。 最後までご覧いただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ページネーションのリンクが表示できなかった。(備忘録)

ページネーションを実装中にエラーになったので、備忘録として残しておきます。 エラー内容 BadMethodCallException Method Illuminate\Database\Eloquent\Collection::links does not exist. (View: /var/www/html/resources/views/articles/index.blade.php) 該当するソースコード ArticleController.php public function index(Request $request) { //検索機能 $search = $request->input('search'); $query = Article::query(); //タイトルと本文の曖昧検索を実施 if (!empty($search)) { $query->where('title', 'LIKE', "%{$search}%") ->orWhere('body', 'LIKE', "%{$search}%"); } $articles = $query->paginate(10)->sortByDesc('created_at'); return view('articles.index', ['articles' => $articles]); } index.blade.php @extends('app') @section('title', '一覧ページ') @section('content') @include('nav') <div class="container"> <div class="row"> <div class="col-md-12"> @guest <img src="/images/Construction-pana_r1.png" width="100%"> @endguest <form method="GET" action="{{ route('articles.index') }}" class="d-flex"> <input class="form-control me-2 mt-3" name="search" type="search" placeholder="検索" aria-label="Search"> <button class="btn btn-outline-success mt-3 mb-0 ml-0 py-0" type="submit"><i class="fas fa-search"></i></button> </form> @foreach($articles as $article) @include('articles.card') @endforeach </div> {{ $articles->links() }} </div> </div> @endsection sortByDescをorderByDescにすることで解決 $articles = $query->paginate(10)->sortByDesc('created_at');だと「10件取得した結果から並べ替える」になっています。 $articles = $query->orderByDesc('created_at')->paginate(10);に修正することで、「並べ替えた結果から10件取得」ことになります。 また、sortByDesc()はコレクション(配列を拡張した(機能を加えた)もの)で返していたのが原因だったと考えています。 クエリビルダの機能であるorderByDesc()を使用することで、指定したカラムでクエリ結果をソートし表示することができたと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLSTATE[HY000]: General error: 1364 Field 'body' doesn't have a default value

前提・環境 ・記事投稿処理をした際に遭遇したエラーになります。 ・Docker ・Docker-compose ・MySQL8 ・Laravel6.0 ・PHP7.4 エラー問題 記事投稿ボタンをクリックすると、、下記のエラーに遭遇しました。。 SQLSTATE[HY000]: General error: 1364 Field 'body' doesn't have a default value 解決方法 日本語訳に訳してみるとこのような訳になります!! SQLSTATE [HY000]:一般エラー:1364フィールド「body」にデフォルト値がありません しっかりArticlController.phpを確認すると、 $article->body = $request->body;が抜けていました。。(すごく凡ミスですね。) ArticleController.php // 記事投稿処理の実装 public function store(ArticleRequest $request, Article $article) { $article->title = $request->title; $article->user_id = $request->user()->id; $article->save(); return redirect()->route('articles.index'); } 下記に記入後、解決です! ArticleController.php // 記事投稿処理の実装 public function store(ArticleRequest $request, Article $article) { $article->title = $request->title; $article->body = $request->body; $article->user_id = $request->user()->id; $article->save(); return redirect()->route('articles.index'); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Laravel]AWS S3に画像をアップロードする際に発生したエラー

ポートフォリオに画像投稿機能を実装しました。 画像ファイルをAWSのS3に保存する際に、いくつか躓いた点があったので、それをアウトプットしようと思います。 バージョン PHP 7.4.2 Laravel 6.20.26 Docker 20.10.6 docker-compose 1.29.1 OS Windows 編集前のコード 画像ファイルをS3に保存する前はpublicフォルダに保存していました。 「S3に保存する前に、とりあえず画像機能を実装したい」という方は、下記のコードをご参照ください。 画像投稿に該当する箇所のみ記載しています。 また、【Laravel 7.x】Laravelで画像投稿機能を実装を参考にしつつ、コーディングしています。 web.php //略 Route::get('/', 'ArticleController@index')->name('articles.index'); Route::resource('/articles', 'ArticleController')->except(['index', 'show'])->middleware('auth'); Route::resource('/articles', 'ArticleController')->only(['show']); //略 Article.php //略 class Article extends Model { protected $fillable = [ 'title', 'body', ]; //略 ArticleRequest.php //略 public function rules() { return [ 'image' => 'mimes:jpeg,jpg,png,gif|max:10240', 'title' => 'required|max:50', 'body' => 'required|max:500', 'tags' => 'json|regex:/^(?!.*\s).+$/u|regex:/^(?!.*\/).*$/u', ]; } //略 ArticleController.php //略 public function store(ArticleRequest $request, Article $article) { $article->fill($request->all()); if ($request->file('image')){ $filename = $request->file('image')->store('public'); // publicフォルダに保存 $article->image = str_replace('public/', '', $filename); // 保存するファイル名からpublicを除外 } $article->user_id = $request->user()->id; $article->save(); //略 return redirect()->route('articles.index'); } //略 〇〇_create_articles_table.php //略 public function up() { Schema::create('articles', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title'); $table->text('body'); $table->bigInteger('user_id')->unsigned(); $table->timestamps(); }); } //略 〇〇_add_image_to_articles_table.php(imageカラムを後から追加しています) //略 class AddImageToArticlesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('articles', function (Blueprint $table) { $table->string('image')->nullable()->after('id'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('articles', function (Blueprint $table) { $table->dropColumn('image'); }); } } index.blade.php //略 <img src="{{ asset('/storage/'.$article->image)}}" alt=""> //略 create.blade.php //略 <form method="POST" action="{{ route('articles.store') }}" enctype="multipart/form-data"> @csrf <div class="container"> <div class="row"> <div class="offset-md-2 col-md-8"> //略 <div class="form-group"> <input type="file" class="from-control-file" id="image" name="image"> </div> </div> </div> </div> <button type="submit" class="btn aqua-gradient btn-block"><i class="fas fa-pen mr-1"></i>投稿する</button> </form> //略 事前準備 S3に保存するためには、事前にAWSの方でIAMユーザーやS3バケットの設定が必要になります。 Rails, Laravel(画像アップロード)向けAWS(IAM:ユーザ, S3:バケット)の設定の手順で進めてください。(この記事とてもわかりやすかったです!) 編集箇所 LaravelでAWS S3へ画像をアップロードするを参考にしつつ、コードを修正しました。(この記事も非常にわかりやすかったです!) ArticleController.php //略 public function store(ArticleRequest $request, Article $article) { $article->fill($request->all()); if ($request->file('image')){ //==========ここから削除========== $filename = $request->file('image')->store('public'); // publicフォルダに保存 $article->image = str_replace('public/', '', $filename); // 保存するファイル名からpublicを除外 //==========ここまで削除========== //==========ここから追記========== // s3アップロード開始 $image = $request->file('image'); // バケットの`myprefix`フォルダへアップロード $path = Storage::disk('s3')->putFile('myprefix', $image, 'public'); // アップロードした画像のフルパスを取得 $article->image_path = Storage::disk('s3')->url($path); //==========ここまで追記========== } $article->user_id = $request->user()->id; $article->save(); //略 return redirect()->route('articles.index'); } //略 index.blade.php //略 <img src="{{ asset('/storage/'.$article->image)}}" alt=""> //この行を削除 <img src="{{ $article->image_path }}"> //この行を追記 //略 画像ファイルの保存先をAWS S3に変更した際のエラー Class 'App\Http\Controllers\Storage' not found 一番最初のエラーはこれでした。 Storageが見つからないと言われています。 ArticleController.phpにuse Illuminate\Support\Facades\Storage;を追記することで、storage facadeをインポートし、このエラーは解決しました。 use Storage;でも試してみましたが、問題なく動作しました。 Class 'League\Flysystem\AwsS3v3\AwsS3Adapter' not found 次のエラーはこれです。 単純にflysystem-aws-s3-v3のインストールを忘れていました。 ドキュメントにもちゃんと書いてありますね。(Laravel 6.x ファイルストレージ) $ composer require league/flysystem-aws-s3-v3でインストールできます。 ただし、バージョンの指定がないと2.0がインストールされてエラーになることもあるらしいので、$ composer require league/flysystem-aws-s3-v3 ~1.0にしておいた方が安心ですね。 私の場合はDockerのappコンテナの中で実行したかったので、$ docker-compose exec app composer require league/flysystem-aws-s3-v3 ~1.0でインストールしました。 SQLSTATE[42S22]: Column not found: 1054 Unknown column 'image_path' in 'field list' 最後のエラーはこれです。 「articlesテーブルにimage_pathカラムがない」と言われています。 mysqlでarticlesテーブルのカラムを確認すると、、、 mysql> show columns from articles; +------------+-----------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+-----------------+------+-----+---------+----------------+ | id | bigint unsigned | NO | PRI | NULL | auto_increment | | image | varchar(255) | YES | | NULL | | | title | varchar(255) | NO | | NULL | | | body | text | NO | | NULL | | | user_id | bigint unsigned | NO | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+-----------------+------+-----+---------+----------------+ 7 rows in set (0.01 sec) 確かにないですね。 image_pathカラムではなく、imageカラムが正解でした。 下記のように修正しました。 ArticleController.php //略 public function store(ArticleRequest $request, Article $article) { $article->fill($request->all()); if ($request->file('image')){ // s3アップロード開始 $image = $request->file('image'); // バケットの`myprefix`フォルダへアップロード $path = Storage::disk('s3')->putFile('myprefix', $image, 'public'); // アップロードした画像のフルパスを取得 $article->image = Storage::disk('s3')->url($path); //この行を修正 } $article->user_id = $request->user()->id; $article->save(); //略 return redirect()->route('articles.index'); } //略 index.blade.php //略 <img src="{{ $article->image }}"> //略 バケットを確認 これで無事にS3へ保存することができるようになりました。 念のため実際にアップロードして、バケットへ画像がアップロードできているか確認しました。 ちゃんと保存できていますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでエラーが発生しても必ずHTTPステータスが200になる現象

課題 Laravelでアプリを作っていて、いつからかエラーが発生しても必ずHTTPステータスが200で帰ってくるようになりました。 検証 XDebugでレスポンスオブジェクトを確認すると422となっていますが、ブラウザには200で帰ってきてしまいます。 類似課題 解決策 これはすごく苦労したのですが、最近追加したカスタムconfigファイルの1行目に空行が入っているのが原因でした。その行を削除すると正しいHTTPステータスが戻ってきます。 // ↑ここに空行がありました。 <?php return [ 'img_url' => env('IMG_URL'), ... 検証 LaravelだけではなくPHPの仕様なのかもと思い、プレーンなPHPで試したら再現しました。 PHPの仕様と思っていいのでしょうか。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【php】Xss対策 ー学習ノート

初めに Xssの対策について学習した内容のoutput用記事です。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 ※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。  Xss(cross site scripting)とは 攻撃対象者のWebサイトのユーザーが入力するフォームなどからjavascriptなどで不正な入力値を送り込み、悪質なサイトへの誘導や情報漏洩などの被害を与える攻撃手法。 htmlspecialchars() フォームなどの不正な入力値を安全な入力値に変換してくれる関数。第1引数に変換対象の文字列、第2,3引数にはoptionとしてフラグ、文字エンコードを指定します。 htmlspecialchars (変換対象の文字列, フラグ, エンコード); フラグにはENT_COMPAT、ENT_NOQUOTES、ENT_QUOTESなどがあり、一般的にシングルクォーテーションとダブルクォーテーションを変換の対象にするENT_QUOTESを指定する。変換する文字エンコードはutf-8などを指定します。 $str = 'hoge'; htmlspecialchars ($str, ENT_QUOTES, 'UTF-8'); htmlspecialchars()関数は長いので、sp_charsという関数を作成し呼び出すようにします。 function sp_chars($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } 入力画面,確認画面,表示画面を作成 入力フォームを用意します。画面の構成は入力/確認/表示にします。それぞれのファイルを用意するのではなく、一つのファイルでflagを作り、\$_POSTの値が入っているか否かによって表示するformの表示を切り替えます。 $page_flag = 0; if (!empty($_POST["btn_submit"])) { $page_flag = 1; //確認画面 } if (!empty($_POST["btn_confirm"])) { $page_flag = 2; //完了画面 } if文で条件分岐させます。\$page_flagが0は入力画面、1は確認、2は送信完了にします。 <?php if ($page_flag === 0) : ?> 入力画面 <?php endif; ?> <?php if ($page_flag === 1) : ?> 確認画面 <?php endif; ?> <?php if ($page_flag === 2) : ?> 送信完了。 <?php endif; ?> formを用意します。確認画面には入力された値を表示します。表示される内容は作成したsp_chars関数で消毒します。 <?php if ($page_flag === 0) : ?> 入力画面 <form method="POST" action="xss.php"> 名前 <input type="text" name="input_name"> <br> <input type="submit" name="btn_submit" value="submit"> </form> <?php endif; ?> <?php if ($page_flag === 1) : ?> 確認画面 <form method="POST" action="xss.php"> 名前 <?php echo sp_chars($_POST["input_name"]); ?> <br> <input type="submit" name="btn_confirm" value="confirm"> <input type="hidden" name="input_name" value="<?php echo sp_chars($_POST['your_name']); ?>"> </form> <?php endif; ?> <?php if ($page_flag === 2) : ?> 送信完了。 <?php endif; ?> 不正な入力でテスト 入力値にを入力します。 確認画面にが表示され、javascriptの構文は実行されません。 参考サイト https://www.kagoya.jp/howto/network/xss/ https://www.php.net/manual/ja/function.htmlspecialchars.php https://www.flatflag.nir87.com/htmlspecialchars-555
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Php】Xss対策 ー学習ノート

初めに Xssの対策について学習した内容のoutput用記事です。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 ※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。 Xss(cross site scripting)とは 攻撃対象者のWebサイトのユーザーが入力するフォームなどからjavascriptなどで不正な入力値を送り込み、悪質なサイトへの誘導や情報漏洩などの被害を与える攻撃手法。 htmlspecialchars() フォームなどの不正な入力値を安全な入力値に変換してくれる関数。第1引数に変換対象の文字列、第2,3引数にはoptionとしてフラグ、文字エンコードを指定します。 htmlspecialchars (変換対象の文字列, フラグ, エンコード); フラグにはENT_COMPAT、ENT_NOQUOTES、ENT_QUOTESなどがあり、一般的にシングルクォーテーションとダブルクォーテーションを変換の対象にするENT_QUOTESを指定する。変換する文字エンコードはutf-8などを指定します。 $str = 'hoge'; htmlspecialchars ($str, ENT_QUOTES, 'UTF-8'); htmlspecialchars()関数は長いので、sp_charsという関数を作成し呼び出すようにします。 function sp_chars($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } 入力画面,確認画面,表示画面を作成 入力フォームを用意します。画面の構成は入力/確認/表示にします。それぞれのファイルを用意するのではなく、一つのファイルでflagを作り、\$_POSTの値が入っているか否かによって表示するformの表示を切り替えます。 $page_flag = 0; if (!empty($_POST["btn_submit"])) { $page_flag = 1; //確認画面 } if (!empty($_POST["btn_confirm"])) { $page_flag = 2; //完了画面 } if文で条件分岐させます。\$page_flagが0は入力画面、1は確認、2は送信完了にします。 <?php if ($page_flag === 0) : ?> 入力画面 <?php endif; ?> <?php if ($page_flag === 1) : ?> 確認画面 <?php endif; ?> <?php if ($page_flag === 2) : ?> 送信完了。 <?php endif; ?> formを用意します。確認画面には入力された値を表示します。表示される内容は作成したsp_chars関数で消毒します。 <?php if ($page_flag === 0) : ?> 入力画面 <form method="POST" action="xss.php"> 名前 <input type="text" name="input_name"> <br> <input type="submit" name="btn_submit" value="submit"> </form> <?php endif; ?> <?php if ($page_flag === 1) : ?> 確認画面 <form method="POST" action="xss.php"> 名前 <?php echo sp_chars($_POST["input_name"]); ?> <br> <input type="submit" name="btn_confirm" value="confirm"> <input type="hidden" name="input_name" value="<?php echo sp_chars($_POST['your_name']); ?>"> </form> <?php endif; ?> <?php if ($page_flag === 2) : ?> 送信完了。 <?php endif; ?> 不正な入力でテスト 入力値にを入力します。 確認画面にが表示され、javascriptの構文は実行されません。 参考サイト https://www.kagoya.jp/howto/network/xss/ https://www.php.net/manual/ja/function.htmlspecialchars.php https://www.flatflag.nir87.com/htmlspecialchars-555
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者がlaravel 8.x で画像投稿機能を実装してみた

本記事は初学者が初学者向けのCRUD機能つきブログを作成している時に 画像も載せたいと思いたったところから画像投稿機能を実装してみた時のメモです。 ※画像投稿の部分だけを内容としています。 環境 OS: Windows 10 home CPU: AMD Ryzen 2700X GPU: NVIDIA GTX 1060 RAM: 16GB 2666Mhz PHP: ver 8.0.3 Laravel: ver 8.45.1 MySQL(MariaDB): ver 15.1 下準備 web.phpにルーティングを記述する際、laravel 8.xでは Route::get('/user', [UserController::class, 'index']); といった記述になります。 Route::get('/user', UserController@index)>name('index'); 個人的に上記の書き方が現時点では分かりやすいのですが、 Laravel 8.xでこの記述を行った場合には、 Target class [UserController] does not exist. というエラーが発生してしまいます。 そこで「app」>「providers」内にある「RouteServiceProvider.php」の //protected $namespace = 'App\Http\Controllers'; という記述を探します。コメントアウトされているので//を消してあげることで Route::get('/user', UserController@index)>name('index'); 上記のような記述でもエラーが発生することなくルーティングが可能となります。 ※大して変わらないので公式ドキュメントに従ったほうが良いと思います。 1.テーブルの作成 はじめに、テーブル設計。 カラムはid,title,image,timestampといたってシンプル。 viewで表示されるのはあくまでもtitleとimageカラムに保存された文字列と同じ画像のみです。 timestampはカラムがあるものの、表示に使用はされていません。 無効にしておくと良いかもしれません。(timestampの記述を消すだけだとエラーが出るの注意) ※imageカラムにはxxxx.jpgのように保存されますがあくまで「文字列」 であることに留意してください。画像そのものは別のフォルダに保管されることになります。 テーブル設計は以下のようになります。 ※マイグレーションファイルの作り方は省略します。 ※ファイル名は任意なので好きなように xxx_xxxx_table.php public function up() { Schema::create('テーブル名(複数形)', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string("image"); $table->timestamps(); }); } public function down() { Schema::dropIfExists('image_upload'); } 後はマイグレーションすることでenvファイルに設定されたデータベースにテーブルが作成されます。 2.モデルの作成 モデルを作成します。 php artisan make:model XXXXX(単数形) XXXXX.php(モデル) class XXXXX extends Model { protected $table = "YYYYY"; protected $fillable = [ 'title', 'image', ]; } YYYYYにはマイグレーションファイルで指定したテーブル名を入れます。 protected table = テーブル名とすることでモデルがどのテーブルと関連するか明示します。 protected fillable = [属性名]とすることで複数代入可能な属性を指定しておきます。 ※筆者はfillableとguardedの必要性や意味がまだまだ理解出来ていないので間違っている可能性大です。 ちなみに$fillableの部分は無くても動きます。 3.コントローラーの作成 php artisan make:controller XXYYZZController XXYYZZController.php namespace App\Http\Controllers; use App\Models\image_upload; use Illuminate\Http\Request;←使いません use App\Http\Requests\XXYYRequest;←フォームリクエストバリデーションに使います class XXYYZZController extends Controller { public function index() {     $images = モデルクラス::all(); return view('index',['images' => $images ]); } public function store(XXYYRequest $request) { $newImage = new モデルクラス(); $newImage->title =$request->input('title'); if($request->hasfile('image')){ $file = $request->file('image'); $originalName = $file->getClientOriginalName(); $filename = time().'-'.$originalName; $file->storeAs('public/images',$filename); $newImage->image = $filename; } $newImage->save(); \Session::flash('err_msg','画像を登録しました'); return redirect(route('index')); } public function delete($id) { $imageDelete = モデルクラス::find($id); $imageDelete -> delete(); } } storeメソッドのif()の部分が画像の保存処理になります。 \$fileはimageの取得、\$originalNameは拡張子を含めたファイル名取得、 \$filenameで保存する画像のファイル名を定義しています。 storeAsメソッドでは、storage/appを指定しているので 第一引数にpublic/imagesとすることでstorage/app/public/imagesが 保存場所になります。 第二引数に\$filenameを入れることで$filenameで定義したファイル名で画像が保存されます。 次にフォームリクエストバリデーションのためにRequestファイルを作ります。 作ったRequestファイルが使用できるようコントローラー内にuse App\Http\Requests\XXYYRequest というふうに記述しておきます。 php artisan make:request YYYYRequest Requestファイルはapp/httpディレクトリ内に生成されるRequestsフォルダに配置されます。 XXYYRequest.php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class XXYYRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'image' => 'required|mimes:jpg,jpeg,png|max:10240', 'title' => 'required|min:2|max:100', ]; } } public function authorize() { return true; } authorizeメソッドは初期状態でfalseになっていますのでtrueにしておきます。 ここをtrueにしておかないと403 THIS ACTION IS UNAUTHORIZED.というエラーが返ってきます。 public function rules() { return [ 'image' => 'required|mimes:jpg,jpeg,png|max:10240', 'title' => 'required|min:2|max:100', ]; } ruleメソッドにバリデーションを記述します。 requiredで値の入力が必須となります。 mimesは拡張子を指定しており、maxはKBでサイズを指定しています。 titleのminmaxは文字数を指定しています。 設定したバリデーションに反する値はエラーとして返されます。 4.ビューの作成 以下は共通の表示になります。 layout.blade.php <!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>title</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8/normalize.css"> <body style="max-width: 1000px; margin:0 auto;"> <header> <nav class="navbar navbar-expand " style="background-color: #41505a; , color:white;"> <h2 style="color: rgba(201, 224, 247, 0.705)" class="mx-4">uploader</h2> <div class="navbar-nav "> <a class="btn btn-primary mx-3" href="{{ route('index') }}">list </a> <a class="btn btn-primary mx-3" href="{{ route('create') }}">upload</a> </div> </nav> </header> <br> <div class="container"> @yield('content') </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"> </script> </body> </html> 上の画像で表示されているは投稿した画像一覧になり、index.blade.phpが対応します。 コードが汚らしいのはご勘弁を index.blade.php @extends('layout') @section('title','uploader') @section('content') <div class="row" style="margin: 0 100px;"> <h2 style="text-align:center;"> ---List--- </h2> <div style="text-align: center"> @if (session('err_msg')) <p class="text-danger"> {{ session('err_msg') }} </p> @endif <table class="table table-bordered table-hover my-3"> <tr> <th>Title</th> <th>Image</th> <th></th> </tr> @foreach($images as $image) <tr> <td style="vertical-align: middle; font-size:2rem;"> {{ $image->title }} </td> <td class="p-4"> <img src="{{ asset('/storage/images/'.$image->image)}}" alt="image" style="width:200px;"  > </td> <td class="px-2" style="vertical-align: middle;;"> <form action="{{ route('delete',$image->id) }}" method="POST"> @csrf <button type="submit" class="btn btn-primary my-1"> Delete </button> </form> </td> </tr> @endforeach </table> </div> </div> @endsection 画像の表示部分にはasset関数を用いています。 XXYYZZControllerのstoreメソッドでは画像の保存先がstoreAsでstorage/app/public/imagesと指定されていました。 しかし、asset関数が参照するのはpublicディレクトリになります。 この状態では、画像を表示することでが出来ないのでシンボリックリンクを張る必要があります。 php artisan storage:link シンボリックリンクが張れました。 これでpublic/storageからstorage/app/public/imagesの画像にブラウザからアクセス出来るようになります。 第二引数は画像名となります。 続いて画像投稿画面に対応するcreate_form.blade.phpです。 @extends('layout') @section('title','投稿画面') @section('content') <div class="row"> <div style="margin: 0 auto;"> <h2>Upload form</h2> <form action="{{ route('store') }}" method="POST" onsubmit="return checkSubmit()" enctype="multipart/form-data"> @csrf <div class="form-group"> <label for="title"> Title </label> <input type="text" name="title" class="form-control" value="{{ old('title')}}"> @if($errors->first('title')) <div class="text-danger"> {{ $errors->first('title') }} </div> @endif </div> <br> <div class="form-group"> <input type="file" name="image" class="form-control"> </div> <p>※画像は必須です。拡張子はjpg・jpeg・pngのいずれか限定です。</p> @if ($errors->first('image')) <div class="text-danger"> {{ $errors->first('image') }} </div> @endif <div class="mt-5"> <a class="btn btn-secondary" href="{{ route('index') }}"> Cancel </a> <button type="submit" class="btn btn-primary"> Upload </button> </div> </form> </div> </div> <script> function checkSubmit() { return confirm("投稿しても良いですか?"); } </script> @endsection <form>の中でenctype="multipart/form-data"を指定しています。 これがないと上手くいきませんので要注意 今回は入力フォームがタイトルと画像の2つの要素しか無いので無くてもいいのですが、 ブログのように長文を書いて投稿したいという場合にエラーが発生するとそれまでの 入力内容が消えてします。 そこでold関数を使うことによって値が保持されるようにしています。 onsubmit="return checkSubmit()で投稿に対して再度確認を行うようにしていますが 別に無くても良いやつなのでオマケだと思ってください。 5.ルーティング web.php Route::get('/', 'XXYYZZController@index')->name('index'); //一覧表示画面 Route::get('/create', 'XXYYZZController@create')->name('create'); //画像投稿画面 Route::post('/create', 'XXYYZZController@store')->name('store'); //投稿処理 Route::post('/delete/{id}','XXYYZZController@delete')->name('delete'); //削除処理 このルーティングの記述については「下準備」で触れています。 laravel8.xにおけるデフォルトの記述ではありませんのでご注意ください。 以上で初学者の画像投稿機能実装は終わりとなります。 CRUD機能を実装出来たぐらいの方にお役に立てば幸いです。 間違えている点、不足しているとうあればご教示いただけますと幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サーバ初心者王がLaravel環境を構築するまでの道のり

はじめに 普段、phpを書いてサーバサイドエンジニアを自称しているのですが、実はサーバについてなにもわかっていないことに気づきました。 NginxとかApacheを使っているってのはなんとなく知ってるけど、実際自分が書いたphpのコードがどのような技術の上であのWebページを表示しているのかってのが全くわかっていない。 おそらく、こういった「サーバ初心者王」は他にも一定数いらっしゃるんじゃないかと思います。 今回は、そんな「サーバ初心者王」がLravel環境の構築に至るまでの道中での出来事です。 使用したOSについて Raspberry Pi 3が実家の押し入れの中で眠っていたので、今回はこいつを叩き起こして使うことにしました。 $ cat /etc/os-release PRETTY_NAME="Raspbian GNU/Linux 10 (buster)" NAME="Raspbian GNU/Linux" VERSION_ID="10" VERSION="10 (buster)" VERSION_CODENAME=buster ID=raspbian ID_LIKE=debian HOME_URL="http://www.raspbian.org/" SUPPORT_URL="http://www.raspbian.org/RaspbianForums" BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs" どうやらDebian系のようなので、「Raspberry Pi」で調べても出てこないことはUbuntuの記事を参考にしたりしました。 今回構築した環境 Nginx php7.3 MariaDB Laravel8 WebサーバをNginxにするかApacheにするかの選択は、以下の記事を参考にさせていただきました。 ApacheとNginxについて比較 nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する Nginxのインストール はじめに、WebサーバソフトウェアであるNginxをインストールしました。 $ sudo apt install nginx ブラウザで、自身の(ラズパイの)IPアドレス(http://{$privateIP})にアクセスすると「Welcome to nginx!」と問題なく表示されました。 nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) sudo nginx、sudo nginx -s stop、sudo systemctl start nginxや、sudo systemctl stop nginxとコマンドの違いを気にせず、起動と停止をして遊んでいると、上記エラーが出ました。 上記エラーの確認は下記コマンドで行いました。 $ systemctl status nginx ● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled) Active: failed (Result: exit-code) since Thu 2021-03-18 01:06:35 JST; 1min 2s ago Docs: man:nginx(8) Process: 11265 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS) Process: 11266 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=1/FAILURE) 3月 18 01:06:34 {$hostname} nginx[11266]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use) # 以下略 nginxを起動したら、起動に失敗しました。によると、 テストでsystemctl経由では無く、単独でnginxを動かしてしまっているなどが考えられます(同じnginxであっても、80番ポートをバインドできるのは、たった一つのプロセスのみです) ということなので、80番ポートを確認してみました。 その後、該当プロセスをkillすると上記エラーが解決して、正常に起動・停止ができるようになりました。 $ sudo netstat -lntp 稼働中のインターネット接続 (サーバのみ) Proto 受信-Q 送信-Q 内部アドレス 外部アドレス 状態 PID/Program name tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 4612/nginx: master tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 473/sshd tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 8446/cupsd tcp6 0 0 :::80 :::* LISTEN 4612/nginx: master tcp6 0 0 :::22 :::* LISTEN 473/sshd tcp6 0 0 ::1:631 :::* LISTEN 8446/cupsd $ sudo kill 4612 # Nginx(80番ポートで稼働中のプロセス)のPID 調べた感じsystemctl経由の方が良さそうだったので、今後の起動・停止はsystemctl経由で行うことに統一しました。 php-fpmのインストール そもそも「php-fpmってなんなら?」と思っていたのですが、現在の私の理解では「動的にコンテンツを生成できないWebサーバプログラム(Nginx)が、動的にコンテンツを生成する外部プログラム(php)と連携するために必要な仕組み = サーバがクライアントに動的コンテンツを提供できる」と捉えています。 つまり、Nginx上でphpを動作させるために必要なモノと理解しました。 php-fpmのインストールについて調べると、「phpとphp-fpm」、[php-fpmのみ」などインストール対象が異なる記事がたくさん出てきたので、それぞれどんなパッケージが追加されるか比べてみました。 # それぞれ必要箇所のみ抜粋 $ sudo apt install php7.3-fpm 以下のパッケージが新たにインストールされます: php-common php7.3-cli php7.3-common php7.3-fpm php7.3-json php7.3-opcache php7.3-readline $ sudo apt install php7.3 以下のパッケージが新たにインストールされます: apache2 apache2-bin apache2-data apache2-utils libapache2-mod-php7.3 libapr1 libaprutil1 libaprutil1-dbd-sqlite3 libaprutil1-ldap php-common php7.3 php7.3-cli php7.3-common php7.3-json php7.3-opcache php7.3-readline どうやら「php」をインストールすると、apacheなどの今回は必要ないパッケージも追加されるようだったので、$ sudo apt install php7.3-fpmのみ実行しました。 以下のコマンドで、phpとphp-fpmがインストールされていることを確認しました。 $ php -v $ systemctl status php7.3-fpm.service php.iniのファイルパス phpの設定ファイルであるphp.iniが「/etc/php/7.3/fpm/php.ini」と「/etc/php/7.3/cli/php.ini」と2つあって、中身も微妙に違ったので、最初は混乱しました。 $ php -i | grep php.ini Configuration File (php.ini) Path => /etc/php/7.3/cli Loaded Configuration File => /etc/php/7.3/cli/php.ini 上記コマンドを実行すると、どうも「/etc/php/7.3/cli/php.ini」が使われてそうだが、もう一方は「fpm/」って入っているしな、、と。 結論から言うと、後述の設定を経てphpinfo();の出力結果をみると、「/etc/php/7.3/fpm/php.ini」が参照されていることがわかりました。 当たり前といえば当たり前なのですが、上記コマンドはCLI(コマンドラインインターフェイス)上で実行したので、「cli/」である「/etc/php/7.3/cli/php.ini」が参照されていました。 少し「サーバ初心者王」すぎる感はありますが、スッキリして良かったです。 Nginxとphp-fpmをつなぐ php-fpmでは、FastCGIリクエストを受け付ける(Nginxと通信する)方法として、TCPソケットとUNIXドメインソケットがあるみたいです。 今回は、下記の記事を参考にして、UNIXドメインソケットを使用することにしました。 調べなきゃ寝れない!と調べたら余計に寝れなくなったソケットの話 php-fpm側の設定 上記のUNIXドメインソケットを用いた通信を可能にするために、php-fpm.conf のグローバル設定項目の「プール一覧の項目」を設定していきました。 まず、php-fpmの設定ディレクトリの中身を確認しました。 $ ls /etc/php/7.3/fpm/ conf.d php-fpm.conf php.ini pool.d php-fpm.confの中身を見ると、プロセスプールの設定はpool.d/配下の.confファイルから読み込んでいることがわかりました。 つまり、「etc/php/7.3/fpm/pool.d/www.conf」の内容がNginxとの通信設定に直結しているようでした。 そこで、ファイルの中身を確認してみました。 $ sudo nano /etc/php/7.3/fpm/pool.d/www.conf # 一部抜粋 listen = /run/php/php7.3-fpm.sock # ソケットファイルのパス listen.owner = www-data # アクセスを受け付けるユーザ = Nginxの実行ユーザ listen.group = www-data # アクセスを受け付けるユーザ = Nginxの実行ユーザ 次に、Nginxのワーカープロセスの実行ユーザが「www-data」であるかを確認しました。 $ ps aux | grep nginx # 一部省略 root 497 0.0 0.2 50232 2312 ? Ss 3月18 0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; www-data 498 0.0 0.2 50376 2768 ? S 3月18 0:00 nginx: worker process www-data 499 0.0 0.2 50376 2772 ? S 3月18 0:00 nginx: worker process www-data 501 0.0 0.2 50376 2772 ? S 3月18 0:00 nginx: worker process www-data 502 0.0 0.2 50376 2744 ? S 3月18 0:00 nginx: worker process 「www-data」であることがわかったので、そのままスルーしました。 結局、php-fpmの設定ファイルはなにも変更しませんでした。 Nginx側の設定 Nginxの設定ファイルの記述方法が全くわからなかったので、下記の記事を参考にさせていただきました。 Nginx設定のまとめ nginx連載3回目: nginxの設定、その1 まず、Nginxの設定ディレクトリの中身を確認しました。 ls /etc/nginx/ conf.d fastcgi_params koi-win modules-available nginx.conf scgi_params sites-enabled uwsgi_params fastcgi.conf koi-utf mime.types modules-enabled proxy_params sites-available snippets win-utf 上記ファイル(ディレクトリ)の中で、設定ファイルであるnginx.confに読み込まれているものを抜粋しました。 $ nano /etc/nginx/nginx.conf # include文のみ、抜粋 # coreモジュールの設定 include /etc/nginx/modules-enabled/*.conf; http { # MIMEタイプと拡張子の関連付け include /etc/nginx/mime.types; # バーチャルホストの設定 include /etc/nginx/conf.d/*.conf; # 中身無し include /etc/nginx/sites-enabled/*; # defaultファイルがある ←これをイジる } 次に、php-fpmと通信を行うために、「/etc/nginx/sites-enabled/default」ファイルを編集しました。(「/etc/nginx/sites-available/default」と内容が同じだったので、バックアップを取らずにそのまま書き換えました) $ sudo nano /etc/nginx/sites-enabled/default server { listen 80 default_server; listen [::]:80 default_server; # SSL configuration # # listen 443 ssl default_server; # listen [::]:443 ssl default_server; # # Note: You should disable gzip for SSL traffic. # See: https://bugs.debian.org/773332 # # Read up on ssl_ciphers to ensure a secure configuration. # See: https://bugs.debian.org/765782 # # Self signed certs generated by the ssl-cert package # Don't use them in a production server! # # include snippets/snakeoil.conf; root /var/www; index index.php index.html index.htm index.nginx-debian.html; server_name _; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; # With php-fpm (or other unix sockets): fastcgi_pass unix:/run/php/php7.3-fpm.sock; # /etc/php/7.3/fpm/pool.d/www.confのlisstenに指定してあるソケットを指定 } } ちょっと気になったので、読み込まれているsnippets/fastcgi-php.confの中身を確認してみると、さらにその中でfastcgi.confが読み込まれていることがわかりました。 $ cat /etc/nginx/fastcgi.conf fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REQUEST_SCHEME $scheme; fastcgi_param HTTPS $https if_not_empty; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; # PHP only, required if PHP was built with --enable-force-cgi-redirect fastcgi_param REDIRECT_STATUS 200; どうやら、このfastcgi_paramの設定値が、phpの$_SERVERの値になるみたいです。これは「サーバ初心者王」としては、大きな発見でした。 最後に、access_logのフォーマットの指定と読み込むバーチャルホストを限定するためにnginx.confの編集を行いました。 $ sudo nano /etc/nginx/nginx.conf log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; # 中略 # include /etc/nginx/conf.d/*.conf; # 必要ないのでコメントアウト 変更した設定を反映させるために、php-fpmとnginxを再起動しました。 $ sudo systemctl restart php7.3-fpm $ sudo systemctl restart nginx そして、公開ディレクトリ配下にindex.phpを作成しました。 $ sudo nano /var/www/index.php <?php phpinfo(); ?> http://{$privateIP}にアクセスすると、phpinfo();の内容が表示されることが確認できました。 MySQL(MariaDB)のインストール 次にデータベースの準備をしました。 データベースの管理システムには、MySQLを採用することにしました。 $ sudo apt install default-mysql-server # 一部抜粋 以下のパッケージが新たにインストールされます: default-mysql-server galera-3 gawk libcgi-fast-perl libcgi-pm-perl libconfig-inifiles-perl libdbd-mysql-perl libdbi-perl libencode-locale-perl libfcgi-perl libhtml-parser-perl libhtml-tagset-perl libhtml-template-perl libhttp-date-perl libhttp-message-perl libio-html-perl liblwp-mediatypes-perl libreadline5 libsigsegv2 libterm-readkey-perl libtimedate-perl liburi-perl mariadb-client-10.3 mariadb-client-core-10.3 mariadb-server-10.3 mariadb-server-core-10.3 socat どうやら、debian(buster)の標準データベースはMySQLではなくMariaDB(MySQL互換) になっているみたいなので、上記コマンドをそのまま実行しました。($ sudo apt install mariadb-serverと同じパッケージがインストールされる) # インストールされたことの確認 $ mysql -V mysql Ver 15.1 Distrib 10.3.27-MariaDB, for debian-linux-gnueabihf (armv8l) using readline 5.2 $ mysqld -V mysqld Ver 10.3.27-MariaDB-0+deb10u1 for debian-linux-gnueabihf on armv8l (Raspbian 10) 初期設定 DBMSの初期設定として、rootのパスワードなどを設定するスクリプトを実行しました。 $ sudo mysql_secure_installation # 全部[y]で回答 しかし、パスワードを設定したにも関わらず、まだパスワード無しで入れてしまいました。 また、再度$ sudo mysql_secure_installation実行したときに聞かれる現在のパスワードを空で入力しても、何もエラーが出ませんでした。 再起動なども試したが、いっこうに反映されませんでした。 おかしく思い、いろいろ調べると下記の記事を見つけました。 技術メモの壁 Ubuntu 18.04 + MariaDB 上記動作の原因は、初期状態の DB root ユーザ認証に unix_socket 認証プラグインが使われていることである。この状態では、DB root として接続するために OS の root 権限にならなければならない (sudo または su コマンドを使う)。OS root になっていれば、以下のようにパスワードなしで DB root として接続可能である。 そこでデータベースにログインして、unix_socket 認証プラグインが使われているかの確認を行いました。 # sudo mysql -u root でも sudo mysql -u root -p でもなんでも入れる $ sudo mysql MariaDB [(none)]> SELECT user,host,plugin from mysql.user; +------+-----------+-------------+ | user | host | plugin | +------+-----------+-------------+ | root | localhost | unix_socket | # 確かに、unix_socket 認証プラグインが使われている +------+-----------+-------------+ 1 row in set (0.001 sec) パスワード認証を採用するためにrootの認証方法を変える作戦で行こうかと思いましたが、下記の記事が見つかりました。 Ubuntu 20.04にMariaDBをインストールする方法 Ubuntuでは、MariaDBのrootアカウントが自動システムメンテナンスと密接に結び付けられているため、そのアカウント用に設定された認証方法を変更してはいけません。変更したりすると、パッケージアップデートにより、管理アカウントへのアクセスが削除されることになり、データベースシステムが破損される可能性があります。 ということで、パスワード認証を採用する管理ユーザー(admin)を新たに作成することにしました。 # root権限を引き継いだadminユーザの作成 MariaDB [(none)]> GRANT ALL ON *.* TO 'admin'@'localhost' IDENTIFIED BY 'Your Password' WITH GRANT OPTION; Query OK, 0 rows affected (0.001 sec) # 権限の反映 MariaDB [(none)]> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.001 sec) MariaDB [(none)]> SELECT user,host,plugin from mysql.user; +-------+-----------+-------------+ | user | host | plugin | +-------+-----------+-------------+ | root | localhost | unix_socket | | admin | localhost | | # unix_socket 認証プラグインが使われていないユーザができた +-------+-----------+-------------+ 2 rows in set (0.001 sec) 作成したadminユーザでパスワード認証ログインができることを確認しました。 $ mysql -u admin -p Enter password: MariaDB [(none)]> SELECT USER(); +-----------------+ | USER() | +-----------------+ | admin@localhost | +-----------------+ 1 row in set (0.001 sec) laravelプロジェクト用のDB作成 パスワード認証ログインにこだわっていた理由は、LaravelからMariaDBに接続するときにパスワード認証ログインを使用するからです。 初期設定を終えると、laravelプロジェクト用のDBを作成しました。 # データベース一覧の表示 MariaDB [(none)]> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | +--------------------+ 3 rows in set (0.004 sec) # 「laravel」というデータベースの作成 MariaDB [(none)]> CREATE DATABASE laravel; Query OK, 1 row affected (0.001 sec) # 作成されたことを確認 MariaDB [(none)]> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | laravel | | mysql | | performance_schema | +--------------------+ 4 rows in set (0.002 sec) Laravelのインストール ようやく、Laravelのインストールまで来ました。 ながいながい「サーバ初心者王」の旅もこれで最終章です。 composerのインストール まず、Laravelをインストールするために必要なPHPのパッケージ管理システムであるcomposerをインストールしました。 今まで通りaptコマンドでインストールすることもできそうでしたが、公式に倣ってphpコマンドでインストールすることにしました。 # インストーラをダウンロードする(phpコマンド以外にもcurlやwgetなどのコマンドでダウンロードもできるみたい) $ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" # ダウンロードしたインストーラのハッシュをチェックし、正しければ'Installer verified'と表示する $ php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" Installer verified # インストーラを実行する $ php composer-setup.php All settings correct for using Composer Downloading... Composer (version 2.0.12) successfully installed to: {$Your Current Directory}/composer.phar Use it: php composer.phar # インストーラの削除 $ php -r "unlink('composer-setup.php');" # composer.pharがインストールされていることの確認 $ ls Desktop composer.phar ダウンロード テンプレート ドキュメント ビデオ 音楽 画像 公開 このままだと、パスが通ってないのでグローバルにcomposerコマンドが実行できませんでした。(参考:公式) # インストールされたpharファイルをパスが通る場所にリネームして移動する $ sudo mv composer.phar /usr/local/bin/composer # composerコマンドがグローバルに使えることを確認 $ composer -V Composer version 2.0.12 2021-04-01 10:14:59 Laravelのインストール composerでグローバルにLaravelのインストーラーをダウンロードしてから、$ laravel new プロジェクト名でプロジェクトを作成する方法もあるみたいですが、あまり汚したくないので下記コマンドでプロジェクト作成を行いました。 # homeディレクトリ配下にsrcディレクトリを作成 $ mkdir src $ cd src # 「laravel」というディレクトリを作成し、laravelプロジェクトを作成する $ composer create-project laravel/laravel laravel # 一部抜粋 # エラー発生 Problem 1 - laravel/framework[v8.12.0, ..., 8.x-dev] require ext-mbstring * -> it is missing from your system. Install or enable PHP's mbstring extension. - Root composer.json requires laravel/framework ^8.12 -> satisfiable by laravel/framework[v8.12.0, ..., 8.x-dev]. it is missing from your system. Install or enable PHP's mbstring extension. 上記エラーが出て、laravelプロジェクトの作成に失敗しました。 どうやら、mbstringがないことが理由みたいなので、インストールしました。 $ sudo apt install php7.3-mbstring # インストールされていることの確認 $ php --ini # 一部抜粋 /etc/php/7.3/cli/conf.d/20-mbstring.ini, # 再び、laravelプロジェクトを作成する $ composer create-project laravel/laravel laravel # 別のエラーが発生 - phpunit/phpunit[9.3.3, ..., 9.5.x-dev] require ext-dom * -> it is missing from your system. Install or enable PHP's dom extension 今度はdomがないって怒られたので、同様にインストールしました。 # domを入れる → xmlを入れればdomも入ってくるみたい $ sudo apt install php7.3-dom # 一部抜粋 注意、'php7.3-dom' の代わりに 'php7.3-xml' を選択します 以下のパッケージが新たにインストールされます: php7.3-xml # できた!! $ composer create-project laravel/laravel laravel # 一部抜粋(かなり長い) Package manifest generated successfully. 74 packages you are using are looking for funding. Use the `composer fund` command to find out more! > @php artisan key:generate --ansi Application key set successfully. Nginxの再設定 アクセスしたときにLaravelの画面が表示されるように、Nginxのドキュメントルートを作成したLaravelプロジェクトに向けました。 $ sudo nano /etc/nginx/sites-enabled/default # 変更箇所のみ抜粋 root {$home_dir}/src/laravel/public; index index.php; # nginxを再起動して設定を反映 $ sudo systemctl restart nginx The stream or file "{$home_dir}/src/laravel/storage/logs/laravel.log" could not be opened in append mode: failed to open stream: Permission denied 確認のため、http://{$privateIP}にアクセスすると、上記の権限エラーが出ました。 エラーの内容はNginxの実行ユーザにlaravel/storage/配下へのアクセス権限がないということなので、下記の権限設定を行いました。 # /etc/nginx/nginx.confのuser(www-data)にディレクトリの所有権を与える $ sudo chown -R www-data:www-data ~/src/laravel/storage # 権限を確認すると、755だったのでそのままにしておく $ ls -l ~/src/laravel/storage 合計 12 drwxr-xr-x 3 www-data www-data 4096 5月 12 05:49 app drwxr-xr-x 6 www-data www-data 4096 5月 12 05:49 framework drwxr-xr-x 2 www-data www-data 4096 5月 12 05:49 logs 再び、http://{$privateIP}にアクセスすると、正常にLaravelの初期画面が表示されました。 DBの接続設定 先ほど作成した「laravel」データベースと接続するために、laravelプロジェクトの.envファイルを編集しました。 $ nano ~/src/laravel/.env # 一部抜粋 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=admin DB_PASSWORD={$YourPassword} DBの接続確認を行うために、laravelの対話シェルである「tinker」を起動して、簡単なSQL文を実行してみました。 # laravelプロジェクトのディレクトリに移動 $ cd ~/src/laravel/ $ php artisan tinker >>> DB::select('select 1'); # テスト用のSQL文 Illuminate\Database\QueryException with message 'could not find driver (SQL: select 1)' Illuminate\Database\QueryException with message 'could not find driver (SQL: select 1)' SQL文を実行してみると上記エラーが出たので、まだDBに接続できていないことがわかりました。 エラーの内容はドライバがないということなので、MySQLドライバをインストールしました。 $ sudo apt install php7.3-mysql # 設定を反映させるためにNginxを再起動 $ sudo systemctl restart nginx # 再びテストするとSQL文が通り、DBに接続できたことがわかった $ php artisan tinker >>> DB::select('select 1'); => [ {#3392 +"1": 1, }, ] これで、ようやくLaravel環境の構築完了です。 結局必要だったパッケージ 時系列で書いていて、わかりにくいと思うので、最後に今回aptコマンドでインストールしたパッケージをまとめておきます。 $ sudo apt install nginx php7.3-fpm php7.3-mbstring php7.3-xml php7.3-mysql default-mysql-server # 別途、composer, laravelのインストールが必要です さいごに 最後まで読んでくださった方、ありがとうございます。 いかがでしたか?「サーバ初心者王」の旅路は。 自分で一度手を動かしてみるというのは、やっぱり良いですね。今回の旅で「サーバ初心者王」から脱却できた気がします。 ただ、コード中の日時を見ていただくとわかるかと思いますが、途中疲れたりしてかなり日が空いています。やはり慣れない作業は難しいです。 これくらいの作業、半日あれば余裕で終わるっていうレベルにまで早く持って行って、早いこと「サーバ御家人」に昇格したいです。 最後に、なにか誤った解釈などございましたら、優しくご指摘お願いします。なにぶん、まだまだ未熟な「サーバ初心者キング」なもんで。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー備忘録】LaravelでGoogle認証のコールバックを受け取る際「localhostで接続が拒否されました」のエラー

状況・エラー内容 Laravelのアプリケーションに、OAuthとSocialiteを使って、Googleアカウントでログインする機能を実装しているとき。 Googleアカウントを選択する画面までは表示できているが、アカウントを選択してログインしようとすると「このサイトにアクセスできません。localhostで接続が拒否されました。」となる。 原因 Googleから承認済みのリダイレクトを受け取るURLをGoogleに設定しているが、そのURLにlocalhostのポート番号を指定していなかった。 ここではローカル開発環境はMAMPを使っている為、 「http://localhost:8888/login/google/callback」 と設定しなおす。 すると今度は別のエラー画面が表示される。 これはLaravelで設定しているAPP_URLが原因だったので、以下のようにenvファイルを修正する。 env APP_URL=http://localhost:8888 これで無事、コールバックを受け取り次の画面へ進むことができました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー備忘録】Laravelのログ出力時のエラー Object of class Laravel\Socialite\Two\User could not be converted to string 

状況 Laravelで作ったアプリケーションに、OAithとLaravelのsocialiteを使ってGoogleアカウントでのログイン機能を実装していたときのこと。 Google認証後にコールバックを受け取るところで、画面にエラーが表示されました。 エラー内容 エラー文はこちら ブラウザ Object of class Laravel\Socialite\Two\User could not be converted to string  「オブジェクト形式であるUserクラスを、文字列に変換できませんでした」という内容のようです。 原因の考察 エラーの原因となっているコードは、この部分でした。 LoginController public function handleGoogleCallback() { // Google認証後の処理 Log::debug('1'); $googleUser = Socialite::driver('google')->stateless()->user(); Log::debug($googleUser);    //この行がエラーの原因 Log::debug('2'); handleGoogleCallbackメソッドが実行された際、Log::debug('1');はログ出力出来ていますが、$googleUserと'2'はログ出力できていませんでした。 Log::debugは文字列をログ出力するものですが、$googleUserの中身はオブジェクト形式。よって、先述の内容のエラーが出ていたようです。 解決法 ログ出力する際、print_r()を使って$googleUserを文字列に変換します。 ※print_r()は、第二引数にtrueをとると、結果を文字列として関数の戻り値にすることができる。 LoginController public function handleGoogleCallback() { // Google認証後の処理 Log::debug('2'); $googleUser = Socialite::driver('google')->stateless()->user(); Log::debug(print_r($googleUser, true)); //書き換える Log::debug('3'); これでエラーはなくなり、また$googleUserの中身をログ出力することもできました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel env関数の環境変数が取得できない

勉強用です APP_NAME=Laravel .envファイルで定義した環境変数をPHPファイルで呼び出したい。 で、env関数を使います。 env('APP_NAME') でもこのまま実行してもエラーが出ちゃいました。 tinker使って調べると・・・ $ php artisan tinker Psy Shell v0.9.12 (PHP 7.3.24-(to be removed in future macOS) — cli) by Justin Hileman >>> env('APP_NAME') => null >>> 中身何も入っていなくnullが返されてしまいます。 調べてみると、キャッシュを消せば手っ取り早いということで、このコマンドを実行 php artisan config:clear 自分は変更が反映されるようになりました! そしてもう一つこのコマンドを使ってのやり方もあるんですが、configファイルを使わなきゃいけなかったりと注意しなくてはならない点があるため、理解したら追記しようかなと思います。 php artisan config:cache
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む