- 投稿日:2021-02-20T23:56:56+09:00
【Laravel】バージョン8.27から利用可能なafterメソッドでカラムを追加する
はじめに
Laravel8.27より
after
メソッドで既存のカラムのあとに複数のカラムを**追加できるようになりました。この記事では、afterメソッドでカラムを追加する方法についてまとめました。前提
追加するテーブル構造
今回Userテーブルのカラム「name」のあとに新しくカラム「birth_year、birth_month、birth_day」を追加していきます。
+-------------------+-----------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+-----------------+------+-----+---------+----------------+ | id | bigint unsigned | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | email_verified_at | timestamp | YES | | NULL | | | password | varchar(255) | NO | | NULL | | | remember_token | varchar(100) | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +-------------------+-----------------+------+-----+---------+----------------migrationファイルを追加する
$ php artisan make:migration add_birth_to_users_table --table=usersafterメソッドを使用してカラム追加
public function up() { Schema::table('users', function (Blueprint $table) { $table->after('name', function ($table) { $table->integer('birth_year'); $table->integer('birth_month'); $table->integer('birth_day'); }); }); }修正が終わったら、マイグレーション実行する。
$ php artisan migrate Migrating: 2021_02_20_142718_add_birth_to_users_table Migrated: 2021_02_20_142718_add_birth_to_users_table (342.99ms)きちんとカラム追加されているか確認。
mysql> desc users; +-------------------+-----------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+-----------------+------+-----+---------+----------------+ | id | bigint unsigned | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | birth_year | int | NO | | NULL | | | birth_month | int | NO | | NULL | | | birth_day | int | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | email_verified_at | timestamp | YES | | NULL | | | password | varchar(255) | NO | | NULL | | | remember_token | varchar(100) | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +-------------------+-----------------+------+-----+---------+---------------- 11 rows in set (0.01 sec)上手く追加されていました!
おまけ: afterメソッド使用しないでカラム追加(従来のやり方)
public function up() { Schema::table('users', function (Blueprint $table) { $table->integer('birth_year')->after('name'); $table->integer('birth_month')->after('birth_year'); $table->integer('birth_day')->after('birth_month'); }); }参考
- 投稿日:2021-02-20T23:09:53+09:00
Laravel5.8 画像アップロード機能を仕組みから理解する
1.はじめに
ECサイトを開発中に画像アップロード機能について最高に詰まったのでこの記事にまとめます。
誰かのお役に立てたら幸いです。
なお、今回実装したのは
画像をストレージに保存してDBにその「パス」を保存する方法です。対象読者
・画像アップロードに躓いている方
・Laravel初学者注)
本記事に記載してあるコードは要点部分のみです。
本記事は画像アップロードの仕組みを理解するためのものとお考えください。環境
・Laravel Framework 5.8.38
・PHP 7.2.34
・mysql Ver 14.14 Distrib 5.7.32
・phpmyadmin2.実現したい内容
今回実装したかったのはECサイトにおける商品登録の段階で、
①フォームに画像データを添付して送信したい
②送ったデータをDBに保存したい
③保存された画像データをView(商品一覧)に表示させたい
という内容を実装したい。つまりは
このようなFormを作って、画像添付して登録ボタンを押すとこの一連の流れを習得できるようにします。
3.体系的に理解する
まず前提知識を列挙していきます。
3.1.今回の画像アップロード機能概要
第一に、画像アップロード機能の構造的解釈で躓いたので記載しておきます。
「2.実現したい内容」で述べた手順(構成)がそもそも間違っていました。実現したい内容を要約すると
①商品登録をする際に、商品情報を記載するフォーム画面に画像を添付して送信する
②その画像をDBに保存する
③商品一覧画面にその画像を表示させるとなりますが、正しくは下記の通りです
①商品登録をする際に、商品情報を記載するフォーム画面に画像を添付して送信する
②画像データ自体はサーバ(Laravel)に保存される
③DBには②で保存した位置情報(ファイルパス)のみを保存する
④商品一覧画面にその画像を表示させる僕はこの赤文字部分を理解するのに時間がかかりました...
順に解説しますと
「①」はそのままです。「②、③」に関して、「①」で送信したリクエストに画像データとファイルパスの二つの情報を持たせます。そのうち画像データはサーバー(Laravel)内に保存します。ファイルパスのみをDBに保存します。
「④」は表示のさせ方が少しややこしいですが、すぐ理解できると思います。(後ほど記載します)
次に画像データはどこに保存されるか?について記載していきます
3.2.publicフォルダについて
前項で、画像データはサーバー(Laravel)内に保存すると書きましたが
その保存先が、publicディレクトリです。...!?
publicが二つある..!?ここも理解に苦しみました。。解説します
Laravelにはpublicディレクトリが二つあります。
・public(appと同じ階層)(便宜上、「上のpublic」と呼ばせていただきます)
・storage/app/public (便宜上、「下のpublic」と呼ばせていただきます)3.2.1.上のpublicディレクトリ
アプリケーションに送られる、全てのリクエストのエントリーポイント(最初に実行される)となるindex.php ファイルがあるところ。
ここには他に画像、JS、CSSといったものを置いたりする。
https://qiita.com/shosho/items/93cbff79376c41c3a30b (参考サイト)
このpublicはブラウザからサーバーにアクセスした時には公開ディレクトリとなっていることが特徴(セキュリティ的に弱いディレクトリである)
つまりは公開ディレクトリなので、あまり情報はおきたくないところです。3.2.2.下のpublicディレクトリ
まずstorageディレクトリについて
Bladeテンプレートをコンパイルしたものやセッションのファイル、キャッシュファイル、その他フレームワークが作り出したファイルなんかが置かれる。storage以下の階層
ディレクトリは app framework logs の3つに分かれている。
このうちの「app」のなかに「public」は存在する。storage/app/publicについて
storage/app/public についてはユーザーのプロフィールアバターといったいろんなとこで使いたくなるようなファイルを置く場所で、非公開ディレクトリ。
https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/12 (参考サイト)Laravel 5.3からアップロードファイルの格納が簡単にできるようになり、その置き場所となっているそう。
つまりは、あまり外部に漏らしたくないアップロードされた画像を格納しておくところぐらいの解釈で良いかなと思ってます。3.2.3.publicまとめ
上のpublic = 公開ディレクトリ
下のpublic = 非公開ディレクトリ
いったんこれだけ抑えておけば事足りるかと思います結論をいうと、
フォームから送信された画像データは「下のpublic」(非公開ディレクトリ)に格納します。
この二つの「public」のせいでかなり混乱しましたが、それぞれ存在意義があります。
というのも、僕らがブラウザでショッピングサイトを開いても、公開されているのは上のpublicであって、画像データがあるのは下のpublicです。
ではどうやって下のpublicから画像を引っ張ってきているのか..?
答えは次項のシンボリックリンクで3.3.シンボリックリンクについて
最初この言葉を見た時、意味不明だったので簡単に記載します。
3.3.1シンボリックリンクとは??
ショートカットのようなものです。今回の例で言うと、laravelアプリの、上のpublicが、ウェブ上に公開されるディレクトリとなります。ブラウザからサーバー上のファイルにアクセスするときは、画像は /storage/app/public(下のpublic)の中にあるため、表示することができません。そこで、上のpublic/storage(後に、上のpublicのなかにstorageを作成します) と、下のstorage/app/public にリンクを持たせることにより、public/storageにアクセスする=storage/app/publicにアクセスする、ということを実現できます。(間違っていたらすみません)
https://qiita.com/si-ma/items/16565d925b0558cbba58 (参考サイト)つまりは、シンボリックリンクとやらを行うと、公開されてる上のpublicから非公開である下のpublicにアクセスできるようになるということ(多分)
感覚的には上のpublicと下のpublicは全く別物ではなく「表と裏の存在」のようなイメージ。
3.3.2シンボリックリンクを張る
というわけでシンボリックリンクを張っていきます。
ターミナル上にて(artisanファイルがあるディレクトリで)
php artisan storage:link上のpublicのなかにstorageファイルができました。
そしてその右側に「↩️」こんなマークができました。(リンクが張られているマークだと思います)
これで、下のpublicにアクセスが可能となったはずです。3.4.画像データの保存先を理解する
ここまでで何度か記載しましたが、重要なので大項目で記載しておきます。
画像データの保存先は下のpublicとなります3.5.DBへの保存について
3.5.1.保存する内容
前項でも書きましたがDBに保存するのは
下のpublicに保存した画像データのファイルパス
となります。「下のpublic」 と 「DB」 に保存するデータ内容抑えておくと考えがまとまりやすいです。
※DBに画像データを保存するやり方もあります。ですが推奨はファイルパスのみをDBに保存する方法だそうです。
ーー番外編ーー
アップロード画像をDBに保存しないほうがいい理由
・レコードのデータ量が多くなり、クエリに時間がかかる
・WebとDBを分割しようと思った時に弊害がある
・DBのストレージ容量を圧迫する
・ネットワークを圧迫する
・メンテナンス性が低下する
・キャッシュしにくくなる
どこかのサイトで見ましたが、URLがわかりませんでした。。
ーーーーーーー3.5.2.DBに保存するために
僕の場合画像アップロード機能をつけることは当初の設計に入っていなかったので、migrationファイルからいじる必要がありました。
つまりは、新しくファイルパスをいれるカラムが必要となったので作成しておかないといけません。
既にmigrationファイルを作成してしまっている人でも、すぐに追加することができます。画像のファイルパスを入れたいmigrationファイルに以下のコードを追加してください
$table->string('product_image'); //カラム名は好きなものでOK完成した僕のmigrationファイルです↓↓
//略 public function up() { Schema::create('m_products', function (Blueprint $table) { $table->increments('id'); $table->string('product_name', 64); $table->integer('category_id')->unsigned(); $table->integer('price')->unsigned(); $table->integer('sale_status_id')->unsigned(); $table->integer('product_status_id')->unsigned(); $table->string('description', 256); $table->string('product_image'); //←ここに入れました $table->timestamp('resist_date'); $table->integer('user_id')->unsigned(); $table->char('delete_flag', 1); $table->foreign('sale_status_id')->references('id')->on('m_sales_statuses')->onDelete('cascade'); $table->foreign('product_status_id')->references('id')->on('m_products_statuses')->onDelete('cascade'); $table->timestamps(); }); } //略これで、DBでのファイルパスの受け皿も完成しました。
では実装していきます。4.実装
4.1.Form部分
{!! Form::open(['route' => 'back_product_store', 'enctype'=>'multipart/form-data']) !!} //略 <div class="form-group-sm"> {!! Form::label('image', '商品画像', ['class' => 'd-block mt-2 mb-0']) !!} <input type="file" name="product_image" value="" class="ml-3 mr-2 d-inline"> </div> //略 {!! Form::close() !!}※bootstrapとLaravelCollectiveを用いているので、少し書き方が独特な部分があります。
ポイント
①'enctype'=>'multipart/form-data'
ファイルのアップロードを行う場合は、enctype=”multipart/form-data”は忘れずにform要素に設定をしてください。②type="file"
取り扱うデータはfileなのでtype属性はfileに設定します③name="product_image"
送信するデータの名前です。なんでもいいです。
僕の場合は商品画像なのでproduct_imageとしました。④'route' => 'back_product_store'
このフォームを送信するとback_product_storeという名前のルーティングにいきます。4.2.Routing
今回の場合は、、
Route::post('product/store', 'BackProductController@store')->name('back_product_store');ルーティング部分に関しては特に注意事項はないです。
このルーティングで
BackProductController の storeアクションにいきます。4.3.Controller
public function store(CreateProductRequest $request) { //バリデーションの記載 $this->validate($request, CreateProductRequest::rules()); $productImage = $request->product_image; if ($productImage) { //一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成 //ここでstore()メソッドを使っているが、これは画像データをstorageに保存している $productImagePath = $productImage->store('public/uploads'); } else { $productImagePath = ""; } $user = Auth::user(); if ($user->id) { $userId = $user->id; } //userIdとproductImageが存在すれば以下の項目をMProductテーブルに保存 if ($userId && $productImage) { $data = [ 'product_name' => $request->productName, 'category_id' => $request->categoryId, 'price' => $request->price, 'sale_status_id' => $request->saleStatusId, 'product_status_id' => $request->productStatusId, 'description' => $request->description, 'user_id' => $userId, 'resist_date' => date('Y-m-d H:i:s'), //DBにはファイルパスを保存!!!!!! 'product_image' => $productImagePath, 'delete_flag' => '', ]; //$dataをクリエイトする MProduct::create($data); } }ポイント
①バリデーションについて$this->validate($request, CreateProductRequest::rules());今回はクラスを使ってるので少しややこしい書き方です、、
調べれば、これとは別に基本的な書き方がすぐ出てくると思うので割愛させていただきます。②ファイルパスの作成及び画像データの保存を1行で記載 ※超重要※
$productImagePath = $productImage->store('public/uploads');この一文、超重要です。
全てがここに集約されていると言っても過言ではないくらい重要です。解説)
・前提として、$productImageは、送信されてきた画像データです。
・まず$productImagePathという変数を作ります。
・右側にて、$productImageをstore()関数で保存します。
保存場所は引数にある'public/uploads'です。
つまりpublicディレクトリのなかのuploadsに保存します。
・「uploadsディレクトリ」は準備していませんでしたが、保存の際に自動的に作成されます。
・ここでのpublicはもちろん「下のpublicディレクトリ」です。
・この時保存されるファイル名はstore()関数によって乱数的に作成されます。
(これによりプロフィール画像など、ファイル名が被って欲しくない時には有効です。ファイル名を固定したければstoreAs()関数を使います。)そして
・作った変数$productImagePathの値として何が入るかというと、
この変数名からお察しかと思いますが
"保存された場所へのパス"がこの変数の値となります。(ここで感動しました)コードのコメントアウトにあるように、このコード1行のみで
//一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成
//ここでstore()メソッドを使っているが、これは画像データをstorageに保存している
これだけのことを行っています。③DBにファイルパスを保存
//DBにはファイルパスを保存!!!!!! 'product_image' => $productImagePath,少しカラム数が多くてわかりにくいですが、やってることで重要なのはこの部分です。
product_imageというカラムに $productImagePath (ファイルパス)を保存します
dd()で取り出してみるとわかりやすいので見てみます。以下のコメントアウト部分でデバッグをかけてみます
BackProductControllerpublic function store(CreateProductRequest $request) { //バリデーションの記載 $this->validate($request, CreateProductRequest::rules()); $productImage = $request->product_image; if ($productImage) { //一意のファイル名を自動生成しつつ保存し、かつファイルパス($productImagePath)を生成 //ここでstore()メソッドを使っているが、これは画像データをstorageに保存している $productImagePath = $productImage->store('public/uploads'); dd($productImagePath); //ここでデバッグをかけてみる } else { $productImagePath = ""; }結果がこちら
このようなデータが取れました。
これはファイルパスなので画像データの位置情報です。public/uploads/にある7qaJNxnb4cKml1aOwUWoOVUoHdaAUos1U55SkYU2.jpgという画像
という意味です(そのままですみません)
というわけでしっかりとファイルパスも設定できているのでこのままDBに保存してしまえば、この後viewで表示したい時にそのパスを頼りに、下のpublic(の中のuploads)から画像データを引っ張り出せるということになります。
4.4.View(画像を表示)
最後にサーバー(Laravel)に保存した画像データを、データベースに保存したファイルパスを使ってviewに表示させます。
表示に関しては冒頭で「少しややこしい」と言いましたが、すぐ理解できるはずです。
結論から書くとこうです。
<img class="product_image" src="{{ Storage::url($product->product_image) }}" alt="" width="150px" height="100px">class=""とか、alt=""に関してはつけたい方は適当につけてください。
width=""やheight=""は割愛します。大事な部分はここです。
src="{{ Storage::url($product->product_image) }}"分けて考えます。
まずは
Storage::url() の部分ですが、
指定したファイルのURLを取得するには、urlメソッドを使います。
Storage::をつけてurl()メソッドを使用することで、その引数のファイルパスを取得することができます。(言い回しがあってるかわかりませんが)詳しくはリファレンスに記載があります。
https://readouble.com/laravel/5.5/ja/filesystem.html次に
$product->product_image の部分ですが$productは僕が設定した商品データの変数です。
なので
$product->product_image と記載することで
その商品のファイルパスを表しています。つまり、最初のコードの src は「ある商品のファイルパス」を取得していたことになるので、その商品にあった場所に適した画像を表示できるわけです。
というわけで、実際のView画像である
こちらにたどり着きます。
画像アップロードの解説は以上となります。
5.まとめ
何がどこに保存されているかわからなくて、大いに躓いたので記事にしました。
データの動きをみることがすごく大切だと痛感しました。。僕なりの解釈でこの記事を書きましたが、不備などありましたらご指摘頂けると幸いです。
以下参考にさせていただいたサイトです。参考サイト一覧
https://qiita.com/ryo-program/items/35bbe8fc3c5da1993366
https://note.com/akina7/n/ne9af79fea62e
https://reffect.co.jp/laravel/how_to_upload_file_in_laravel#i-13
https://qiita.com/koru1893/items/1d2f522e20744b03e3ad#%E5%BE%8C%E3%81%AF%E7%A7%BB%E5%8B%95%E3%81%95%E3%81%9B%E3%81%A6db%E3%81%B8%E4%BF%9D%E5%AD%98
https://laraweb.net/tutorial/2707/
https://note.com/laravelstudy/n/n038bd68f53a7#nRJwi
https://qiita.com/u-dai/items/8a904cc7fd2795c0e70d
https://biz.addisteria.com/image-upload/
https://qiita.com/shosho/items/93cbff79376c41c3a30b
https://qiita.com/si-ma/items/16565d925b0558cbba58
https://reffect.co.jp/laravel/laravel-storage-manipulation-master#visibility最後まで読んでいただき、ありがとうございました。
- 投稿日:2021-02-20T21:12:53+09:00
クライアントがアプリの一部機能にドメインを割り当てる際に自動でSSL化する方法
概要
アプリケーションの一部機能、カート機能や予約機能をクライアントのページで作成する時に、ドメインを指定したいという要望に対してHTTPS接続なので、ACM証明書をこちらで追加する必要があります。今回は、その部分を自動化を行います。
使用技術
・AWS ACM
https://aws.amazon.com/jp/certificate-manager/
・AWS SDK for PHP
https://docs.aws.amazon.com/aws-sdk-php/v3/api/index.html作業手順
- 特定の機能のURLにクライアント固有のidなど含めます。
- クライアントが、【使用したいドメイン】をフォームに入力を行う。(AWS SDKを使用し入力されたドメインをACMに追加 レスポンスに含まれる「エントリー名」「値」を表示)
- クライアントが、DNSサービスプロバイダで作成した【使用したいドメイン】にCNAMEで「1.のURL」 「エントリー名」 「値」を登録
上記の3ステップでクライアント側の操作のみで、SSL化されたドメインを割り当てれるようになります。
AWS SDK for PHPを使用しACM登録処理,「エントリー名」 「値」を確認する
参考コード(for_laravel)
use App\Certification; use Aws\Acm\AcmClient; public function createACM(Request $request) { $client = new AcmClient([ 'version' => 'latest', 'region' => 'ap-northeast-2', 'credentials' => [ 'key' => config('aws.key'), 'secret' => config('aws.secret_key'), ], ]); //ACM登録 $result = $client->requestCertificate([ 'DomainName' => $request->url;, 'Tags' => [ [ 'Key' => '<string>', // REQUIRED 'Value' => '<string>', ], ], 'ValidationMethod' => 'DNS', ]); //こちらで、ACMの情報を呼び出します。 $resource_name = $result['CertificateArn'], $results = $client->describeCertificate([ 'CertificateArn' => $resource_name ]); $エントリー名 = $results['Certificate']['DomainValidationOptions'][0]["ResourceRecord"]['Name']; $値 = $results['Certificate']['DomainValidationOptions'][0]["ResourceRecord"]['Value']; }参考URL
https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-acm-2015-12-08.html#requestcertificateあとがき
半年ほど前に実装した機能で、上手く説明出来なかったのですが記事にして理解できました。
クラウドフロントのキャッシュ削除なんかも下記サイトをみて実装しました。めっちゃ便利です!
・AWS SDK for PHP
https://docs.aws.amazon.com/aws-sdk-php/v3/api/index.html
- 投稿日:2021-02-20T20:29:03+09:00
Laravel初期設定について【個人用メモ】
1.言語や時刻の設定
言語や時刻の設定は config/app.php で設定いたします。
config/app.php'timezone' => 'Asia/Tokyo', 'locale' => 'ja', 'fallback_locale' => 'ja', 'faker_locale' => 'ja_JP',2.DBの設定
①デフォルトでは以下の設定になっています。
.envDB_CONNECTION=mysql // どのRDBMSを利用するか? DB_HOST=127.0.0.1 // HOST名 DB_PORT=3306 //PORT番号 DB_DATABASE=homestead // DB名 DB_USERNAME=homestead // DBのユーザー名 DB_PASSWORD=secret // DBのパスワード設定を変えにDBを作成するなら
DB名:作成したDB名 アクセスユーザー名:homesteadなど アクセスユーザー名のパスワード:secretなどこの設定でDBとユーザーアカウントを作成します。
②冒頭にある'default'の第二引数を使用したいデータベース名に設定すればOK。sqliteならsqlite、MySQLならmysql。
config/database.php'default' => env('DB_CONNECTION', データベース名),DB文字コード変更を下記の通り変更する。
config/database.php// 変更前 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', // 変更後 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci',3.エラーメッセージの日本語化
メッセージの日本語ファイルはデフォルトでは提供されていないので、
手順としては以下の通りです。①laravel/resources/lang/en ディレクトリを丸ごとコピーする
②コピーしたディレクトリ名を ja へ変更する。
③jaディレクトリ内のvalidation.phpに記述されている英文を日本語に置き換える
※エラーメッセージを格納するファイルは validation.php になります。最終的には以下のようなディレクトリ構成になります。
laravel ├── resources │ ├── lang │ │ ├── en │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ │ └── ja │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ └── validation.php日本語化参考URL
https://readouble.com/laravel/5.8/ja/validation-php.html?header=%25E8%25AA%25AC%25E6%2598%258E①アプリケーションのロケール設定
'locale' => 'ja',アプリケーションロケールによって、翻訳サービスプロバイダによって使用されるデフォルトのロケールが決定されます。 この値は、アプリケーションがサポートするロケールのいずれかに自由に設定できます。
②アプリケーションフォールバックロケール
'fallback_locale' => 'en',フォールバックロケールは、現在のロケールが使用できない場合に使用するロケールを決定します。 アプリケーションによって提供される任意の言語フォルダに対応するように値を変更することができます。
エラーメッセージ内の「:attribute」と記載されている部分は、検証フォームのname設定に置き換えられます。
name設定部分も日本語に置き換えたい場合、上記日本語リソースファイル(validation.php)の106行目にあるattributes部分に追加をおこないます。
例えば、name設定が「title」と設定されており、その部分を「タイトル」と日本語化したい場合は、以下のように記述をおこないます。
'date' => ':attributeを有効な日付形式にしてください。', 'min' => [ 'string' => ':attributeを:min文字以上にしてください。', ], 'required' => ':attributeを入力してください。', // ... /* |-------------------------------------------------------------------------- | Custom Validation Attributes |-------------------------------------------------------------------------- */ 'attributes' => [ 'title' => 'タイトル', 'body' => '本文', 'published_at' => '公開日', ], ];4.head内の設定【BootStrapなど】
BootStrap(CDNでの方法)
①レスポンシブ用メタタグの記述。
②CSS を < head >タグ 内側に他のスタイルシートよりも先に入れてください。
③いくつかのコンポーネントで jQuery, Popper.js などの JavaScript プラグインが必要です。< script > を </ body> の直前に順番通りに入れてください。resources/views/layout.blade.php<!doctype html> <html lang="ja"> <head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> .... <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> .... </head> <body> .... <!-- Optional JavaScript --> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </body> </html>公式リファレンス
https://getbootstrap.jp/docs/4.5/getting-started/introduction/#%E3%81%AF%E3%81%98%E3%82%81%E3%82%8BページネーションをBootstrap 4 スタイルにしたい。
bootstrap/app.php の末尾の return の手前あたりに以下を追記する。bootstrap/app.phpIlluminate\Pagination\AbstractPaginator::defaultView("pagination::bootstrap-4");jQuery サイトの CDN
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>Font AwesomeのCDN
<link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">Google font
<link rel = "preconnect" href = "https://fonts.gstatic.com"> <link href = "https://fonts.googleapis.com/css2?family = Noto + Sans + JP& display = swap" rel = "stylesheet">Googleフォント
https://fonts.google.com/?subset=japanese5.キャッシュファイルの作成
①binフォルダの作成
②clear-laravel.shファイルの作成
③clear-laravel.shファイル内の記述laravel
├── bin
│ ├── clear-laravel.sh#!/bin/bash php artisan view:clear php artisan cache:clear php artisan config:clear php artisan route:clear php artisan clear-compiled php artisan config:cache composer dump-autoload php artisan ide-helper:generate php artisan ide-helper:models -N php artisan ide-helper:meta find . -name '._.DS_Store' -type f -ls -delete設定が完了したら、下記を叩く。
$ bin/clear-laravel.sh
- 投稿日:2021-02-20T20:12:29+09:00
バリデーションをControllerではなくFormRequestに記述
概要
今までバリデーションは
Controller
に記述していたのですが、分離させた方が読みやすくなると思って調べてみたら、FormRequest
というものが使えるらしいので、実装してみました。実装
公式サイトを参考に実装してみました。
公式サイト改善前
AccountController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests\AccountRequest; use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rule; class AccountController extends Controller { public function update(Request $request) { $request->validate([ 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($request->id)], 'password' => ['required', 'string', 'min:8'], ]); DB::beginTransaction(); try{ $user = User::find($request->id); $user->email = $request->email; $user->password = Hash::make($request->password); $user->save(); DB::commit(); }catch(\Exception $e){ DB::rollBack(); } } }改善後
php artisan make:request AccountRequestAccountController.php<?php namespace App\Http\Controllers; use App\Http\Requests\AccountRequest; use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; class AccountController extends Controller { public function index($login_id){ $user = User::find($login_id); return $user; } public function update(AccountRequest $request) //←ここ変更 { DB::beginTransaction(); try{ $user = User::find($request->id); $user->email = $request->email; $user->password = Hash::make($request->password); $user->save(); DB::commit(); }catch(\Exception $e){ DB::rollBack(); } } }AccountRequest.php<?php namespace App\Http\Requests; use Illuminate\Http\Request; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class AccountRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules(Request $request) { return [ 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')->ignore($request->id)], 'password' => ['required', 'string', 'min:8'], ]; } public function attributes() { return [ 'email' => 'メールアドレス', 'password' => 'パスワード' ]; } }まとめ
分けた方が管理しやすいし便利ですね。
- 投稿日:2021-02-20T15:42:05+09:00
LaravelでFullcalendarに登録したイベントを削除する方法
はじめに
こちらは以下の記事の続きになります。
LaravelでFullcalendarを実装する方
LaravelでFullcalendarに登録した内容を更新する方法
ディレクトリ名等も同じものを使用しているので、ご了承くださいませ。前回までにFullcalendarにイベントの登録と更新はできるようになったので、今回は削除方法について見ていきたいと思います。
-各バージョン
-Laravel 6.x
-PHP 7.4.9
-MySQL 5.7.30
-Fullcalendar v5削除ボタンを作成する
前回までに更新用のモーダルを開くと、登録されている情報が確認できているようにしました。
今回は更新用のモーダルに削除ボタンを追加し、このボタンを押すとデータが削除されるようにします。modal.blade.php<div class="modal micromodal-slide" id="modal-1" aria-hidden="true"> <div class="modal__overlay" tabindex="-1" data-micromodal-close> <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title"> <header class="modal__header"> <h2>Editing my task list</h2> <button class="modal__close" aria-label="Close modal" data-micromodal-close></button> </header> <main> <form method="POST" action="{{ route('editEvent') }}"> @csrf <input type="hidden" id="id" value="" name="id"> <input type="text" id="edit_title" name="title" value=""> <input type="date" id="edit_start" name="start" value=""> <input type="color" id="edit_color" name="textColor" value=""> <button class="modal__btn modal__btn-primary" type="submit">変更する</button> </form> // ここから追加 <form id="delete-event-btn" method="POST" action="{{ route('deleteEvent') }}"> @csrf <input type="hidden" name="id"> <a href="#" id="delete-event" class="cancel-btn">削除する</a> </form> // ここまで </main> </div> </div> </div>前回と同様、カレンダーのイベントを表示させるアクションであるindexアクションではAjaxで通信時のみidを渡すようになっているので(LaravelでFullcalendarを実装する方法)、idはJSで取得するようにします。
更新時にvalueの値を取得するコードを書いているので、こちらに追加していきます。
event.blade.php<script> $(document).ready(function () { $('#calendar').fullCalendar({ // はじめりの曜日を月曜日に変更 デフォルトは日曜日になっており、日=0,月=1になる firstDay: 1, headerToolbar: { right: 'prev,next' }, events: '/index', eventClick: function(info){ document.getElementById("id").value = info.id; document.getElementById("edit_title").value = info.title document.getElementById("edit_start").value = info.start._i document.getElementById("edit_color").value = info.textColor MicroModal.show('modal-1'); } }); // ここから追加 $("#delete-event").on("click", function() { var form = document.getElementById("delete-event-btn"); var eventId = document.getElementById("id").value; // 削除対象のidを前回作成した更新フォームから取得 form.elements['id'].value = eventId; // 削除対象のidを削除フォームにもセット form.submit(); // 削除フォームを送信させる }); }); </script>コントローラー、ビューを作成する
先ほど削除ボタンのフォームで指定したactionを指定していきます。
web.phpRoute::post('/deleteEvent', 'EventController@deleteEvent')->name('deleteEvent');最後にコントローラーです。
EventControllerpublic function deleteTask(Request $request) { // 送信されてきたidをEventテーブルに登録されているデータと紐付ける $task = DoneTask::find($request->input('id')); $task->delete(); return redirect('/event'); }実際に削除ボタンを押してみると、カレンダーとDBどちらからもデータが削除されてるかと思います。
これで完成です!
さいごに
今回でFullcalendarにデータの登録、更新、削除まで行うことができました!
Fullcalendarはまだまだアレンジがたくさんできるので、これからも公式マニュアルを見ながらアップデートしていきたいと思います。
最後まで読んでいただいてありがとうございました!
- 投稿日:2021-02-20T11:49:32+09:00
FormRequestクラスをテストする
動機
PHP/Laravelを触り始めて間もないが、HTTPリクエストのバリデーションでFormRequestが便利ということで使っていた。手軽だったのでHTTPテストでテストを書いていたが、非効率な箇所もあったのでFormRequest単体でのテストを書きたかった。
前提
- PHPUnit 9.5.2
- Laravel Framework 8.27.0
内容
テスト対象のクラスを定義する
$ php artisan make:request ExampleRequest
authorize() で真を返すようにし、 rules() で適当なバリデーションルールを追加する。
app/Http/Requests/ExampleRequest.php<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class ExampleRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'id' => 'required', 'name' => 'required', 'email' => 'required' ]; } }テストを書く
テストファイルを作成する。(UnitかFeatureかの議論はここでは取り扱わない)
$ php artisan make:test ExampleTest
方法は以下の通り。 Table Driven Test 風に書きたかったので、データプロバイダを定義している。
- ExampleTestインスタンスを作成する
- Validatorインスタンスを作成する。テストデータをテスト関数の引数から、ルールとメッセージはExampleTestインスタンスから渡す
- バリデータを実行し、その結果を評価する
tests/Feature/ExampleTest.php<?php namespace Tests\Feature; use Illuminate\Support\Facades\Validator; use Tests\TestCase; use App\Http\Requests\ExampleRequest; class ExampleTest extends TestCase { /** * @dataProvider validationProvider * @return void */ public function testValidation($inData, $outFail, $outMessage) { $request = new ExampleRequest(); $rules = $request->rules(); $messages = $request->messages(); $validator = Validator::make($inData, $rules, $messages); $result = $validator->fails(); $this->assertEquals($outFail, $result); $messages = $validator->errors()->getMessages(); $this->assertEquals($outMessage, $messages); } public function validationProvider() { return [ 'success' => [ [ 'id' => 1, 'name' => 'aaa', 'email' => 'aaaa@example.com', ], false, [], ], 'empty all fields' => [ [], true, [ 'id' => ['The id field is required.'], 'name' => ['The name field is required.'], 'email' => ['The email field is required.'], ], ], ]; } }注意点
HTTPテストをやめて書く場合、当然ながらそのテストで確認していた点はテストできないので注意(例えば、FormRequestが返すメッセージ構造とHTTPレスポンスの構造は異なるなど。)
参考
- 投稿日:2021-02-20T11:10:28+09:00
Laravel 学習メモ
関数など
{{ route('ルートの名前') }}
web.phpを参照して、該当のルートに繋げる。
{{ asset('cssやイメージ') }}
publicを参照して、そこをドキュメントルートとして該当のファイルを読み込む。
例えば、public/css/layout.cssというファイルを読み込む際には、
{{ asset('css/layout.css') }}
という記載にする。store('public/image')
画像を保存する際に用いられることが多い。
storage/app
を参照して、引数のディレクトリにファイルが保存される。
なお、public/image
はこの関数を実行することで自動で作成される。with('key', 'value')
別のリンクへリダイレクトを行う際に、
return redirect('/')->with('flash_message', '投稿が完了しました');と記載するとリダイレクトされるとともに、セッションデータを作成してくれる。
第一引数がセッションのキーで、第二引数がそのキーの値となる。各設定
バリデーションを日本語に変更する
confing/app.phpの
local
がen
になっていて、これは
resources/lang/en
のバリデーション使用しているという意味。
resources/lang/ja
のディレクトリを作成して、その中に日本語に変更しているファイルを作成して、local
をja
に変更することで日本語のバリデーションに変更される。
- 投稿日:2021-02-20T11:10:28+09:00
Laravel ヘルパ関数 メモ
- 投稿日:2021-02-20T10:16:41+09:00
【Docker/Laravel/MySQL5.7】docker-compose upした際にMySQL5.7コンテナがすぐにExitedしてしまう
Hello everyone !
問題
docker-compose up -d
でコンテナを作成&起動しようとしたところStarting app-mysql ... done Starting app-redis ... done Starting app-mailcatcher ... done Starting app-app ... done Starting app-nginx ... done正常に実行できたかと思われましたが
docker ps
でステータスを確認するとCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 88f28d9cab3a app_nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp app-nginx 068d2c2a95ca app_app "docker-php-entrypoi…" About a minute ago Up About a minute 0.0.0.0:9000->9000/tcp app-app 46819a975074 mysql:5.7 "docker-entrypoint.s…" About a minute ago Exited (1) About a minute ago app-mysql b48d4aa43dc9 redis:5.0.5-alpine "docker-entrypoint.s…" 5 weeks ago Up About a minute 0.0.0.0:6379->6379/tcp app-redis 79f839320c2a schickling/mailcatcher "mailcatcher --no-qu…" 5 weeks ago Up About a minute 1025/tcp, 0.0.0.0:1080->1080/tcp app-mailcatchermysql:5.7コンテナだけ起動後にすぐ終了していることがわかりました。
ログを確認する
docker-compose logs上記のコマンドでログを確認したところ以下のエラー文を確認しました。
// 一部抜粋 app-mysql | 2021-02-20T00:29:51.663885Z 0 [ERROR] --initialize specified but the data directory has files in it. Aborting.解決策
エラー文を直訳すると
--initialize(初期化)が指定されていますが、データディレクトリにファイルがあるため失敗
ということでしたのでデータディレクトリのファイルを削除します。
app/docker-compose.ymlでディレクトリを設定していたので確認。volumes: - "./storage/mysql:/var/lib/mysql"ファイルやディレクトリを削除する
rm
Linuxコマンドにオプションの
r(指定ディレクトリ内のファイルやディレクトリを全て削除)
と
f(警告メッセージを表示しない)
をつけて実行します。$ rm -rf storage/mysql以上でデータディレクトリにファイルの削除ができたのでコンテナを再起動させます。
$ docker-compose restartmysql:5.7コンテナが起動後に落ちていないことを確認できました。
Thank you for always reading my posts.
Have a good day !
- 投稿日:2021-02-20T08:18:41+09:00
laravel ファクトリとSedderでデータベースをテスト
はじめに
避けては通れないテスト作業。laravelでは簡単にテストができますので備忘録として投稿します。
モデルはModels
というフォルダにて格納しています。
スクショはモデルの設計図になります。
投稿者の環境
MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)
macOS Big Sur バージョン11.0.1
PHP 7.3.23
Laravel Framework 6.20.7下準備
config\app.php
にある'faker_locale'を下記の様に修正して下さい。ダミーデータが日本語化されます。app.php'faker_locale' => 'ja_JP',ファクトリの生成
ファクトリを作成することによりダミーデータを作成ができます。また、
--model=Postオプション
により、ファクトリが生成するモデルの名前を指定できます。このオプションは、生成するファクトリファイルへ指定モデル名を事前に設定します。モデルを作成していたら設定をして下さい。ターミナル php artisan make:factory PostFactory --model=PostPost.php//階層 Models/Post.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Post extends Model { public function comments() { // 投稿は複数のコメントを持つ return $this->hasMany('App\Models\Comment'); } }フェイカーの作成
マイグレーションファイルと照らし合わせながら作成します。カラムによりプロパティが変わりますのでこちらのチートシートを参考にして下さい。
sample_faker.php$data[] = [ 'name' => $faker->name, // 名前 'zip' => $faker->postcode, // 郵便番号 'pref' => $faker->prefecture, // 都道府県 'city' => $faker->city, // 市 'address' => $faker->streetAddress, // 住所 'phone' => $faker->unique()->phoneNumber, // 電話番号 'email' => $faker->unique()->safeEmail, // メール 'birthday' => $faker->dateTimeBetween('-80 years', '-20years') ->format('Y-m-d'), // 生年月日 (20〜80年前の日付) 'score' => $faker->numberBetween(1, 100), // 1〜100の数字 'text' => $faker->realText(20), // ダミーテキスト 20文字 ];PostFactory.php<?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Models\Post; //Postモデルを参照してテストデータを作成。Modelsというフォルダに格納しているため use Faker\Generator as Faker; // ↓ Post::classのPostはModelsフォルダのPostを参照 $factory->define(Post::class, function (Faker $faker) { return [ 'name' => $faker->name, // 氏名 'subject' => $faker->realText(16), // realTextは日本語の投稿場合に必ず使用。 'message' => $faker->realText(200), // 200文字のテキスト 'created_at' => $faker->date('Y-m-d H:i:s', 'now'), 'updated_at' => $faker->date('Y-m-d H:i:s', 'now'), ]; });create_posts_table.php//Postsマイグレーションファイル <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->bigIncrements('id'); $table->timestamps(); $table->string('name'); $table->string('subject'); $table->text('message'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('posts'); } }Seederファイルの作成
Seederを作成することによって前述したファクトリのダミーデータを呼び出すことができる様になります。
SeederというのはDBのテスト以外でもく初期値やダミーデータ投入のための仕組みになります。
下記のコマンドを実行するとdatabase/seeds/以下にファイルが生成されます。また、PostsTableSeeder.phpを下記の様に編集して下さい。ターミナル php artisan make:seeder PostsTableSeederPostsTableSeeder.php<?php use Illuminate\Database\Seeder; class PostsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { factory(App\Models\Post::class, 50) //Postモデルを参照して尚且つ50個のダミーデータを作成 ->create(); } }外部キーによるファクトリとSeederの設定
CommentにはPost_idという外部キーが割り当てられています。このままテストデータを作成しても
SQLSTATE[HY000]: General error: 1364 Field カラム名 doesn't have a default valueというエラーが発生します。そこで、リレーションを指定することにより外部キーにダミーデータを作成することができます。ファクトリを下記の様に編集します。公式HPの
リレーションと属性クロージャ
を参考にしました。CommentFactory.php<?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Models\Comment; use Faker\Generator as Faker; $factory->define(Comment::class, function (Faker $faker) { return [ 'name' => $faker->name, 'comment' => $faker->realText(200), 'post_id' => factory(App\Models\Post::class),//外部キーを参照 'created_at' => $faker->date('Y-m-d H:i:s', 'now'), 'updated_at' => $faker->date('Y-m-d H:i:s', 'now'), ]; });CommentsTableSeeder.php<?php use Illuminate\Database\Seeder; class CommentsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { factory(App\Models\Comment::class, 50) ->create(); } }最後に
database/seeds/DatabaseSeeder.phpを開いて、runメソッドに PostsTableSeeder を追加します。
DatabaseSeeder.php<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { // $this->call(UsersTableSeeder::class); $this->call(PostsTableSeeder::class); $this->call(CommentsTableSeeder::class); } }ターミナル $ composer dump-autoload $ php artisan db:seed ダミーデータを削除して再度作成したい場合 $ composer dump-autoload $ php artisan migrate:refresh --seed成功すると下記のスクショの様になりphpMyAdminにダミーデータが保存されます。
参考URL
- 投稿日:2021-02-20T03:25:39+09:00
【PHP】Laravel6で同時ログイン制御(先勝ち)をする方法を考えてみた
この記事について
この間に投稿した記事でLaravelでは認証機能(ログイン機能)を設定するのは簡単だと説明しました。
【PHP】Laravel6で遊ぶ(認証機能のセットアップ)認証機能で考えてみたいのが、『同時ログイン制御』です。
少し調べたところ、あまり同時ログイン制御に関する記事やサイトがないような感じです。
本記事では先勝ちで同時ログインを禁止する方法を検討してみます。環境
OS:Windows10 Home
PHP:7.4.15(XAMPP)
Laravel:6.20.16実現方法
昨日の記事でSessionの管理方法について触れてみました。
【PHP】Laravelのセッション管理を勉強するこの記事では、Session情報をDBで管理する方法を紹介し、
Sessions
テーブルに以下の項目が管理できています。
カラム名 内容 id セッションID user_id ログインしているユーザーのID(NULLの場合は未ログイン) ip_address IPアドレス user_agent ユーザーエージェント payload いろいろなデータ(適当) last_activity 最終行動時間(UNIX時間) 実現方法としては、ログイン処理後の遷移前にログインしようとしているユーザーのIDが
Sessions
テーブルに存在するかどうかを調べます。
存在しない場合はログインさせ、存在する場合はログイン画面に戻す処理で考えてみます。修正するファイルについて
実現方法のイメージはできたので、どのファイルをいじればいいのかを考えてみます。
ログイン画面のHTMLを確認すると、FormのPOSTメソッドでactionが(ドメイン)/login
になっています。
その場合にどのような処理が走るかは以下のコマンドで確認できます。commandphp artisan route:listresult+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/user | | Closure | api,auth:api | | | GET|HEAD | home | home | App\Http\Controllers\HomeController@index | web,auth | | | GET|HEAD | login | login | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest | | | POST | login | | App\Http\Controllers\Auth\LoginController@login | web,guest | | | POST | logout | logout | App\Http\Controllers\Auth\LoginController@logout | web | | | GET|HEAD | password/confirm | password.confirm | App\Http\Controllers\Auth\ConfirmPasswordController@showConfirmForm | web,auth | | | POST | password/confirm | | App\Http\Controllers\Auth\ConfirmPasswordController@confirm | web,auth | | | POST | password/email | password.email | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web | | | GET|HEAD | password/reset | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web | | | POST | password/reset | password.update | App\Http\Controllers\Auth\ResetPasswordController@reset | web | | | GET|HEAD | password/reset/{token} | password.reset | App\Http\Controllers\Auth\ResetPasswordController@showResetForm | web | | | GET|HEAD | register | register | App\Http\Controllers\Auth\RegisterController@showRegistrationForm | web,guest | | | POST | register | | App\Http\Controllers\Auth\RegisterController@register | web,guest | +--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+上記のようなRouteの情報は
route/web.php
ファイルで確認することもできます。route/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');ただ、認証機能をセットアップすると
Auth::routes();
で認証関係のRouteがまとめられているので、コマンドで調べました。コマンドの結果に戻ります。
ログイン処理時には/login
にPOSTメソッドで遷移します。
コマンド結果を見てみると・・・+--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+------------------------+------------------+------------------------------------------------------------------------+--------------+ | | POST | login | | App\Http\Controllers\Auth\LoginController@loginこれですね。
アクションとしては、LoginController
のlogin
メソッドに処理を渡していることが分かります。
なので、App\Http\Controllers\Auth\LoginController.php
を修正すればよさそうですね。loginメソッドの確認
早速、
LoginController.php
を確認してみます。LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = RouteServiceProvider::HOME; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); } }あれ?loginメソッドは?ってなりました。
実際は上記ファイルにlogin
メソッドの詳細は記載されていません。
AuthenticatesUsers
というのがポイントで、Illuminate/Foundation/Auth/AuthenticatesUsers
に記載されています。
ちなみに、Illuminate
は/vendor/laravel/framework/src/Illuminate
を指しています。/vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesUsers.php<?php namespace Illuminate\Foundation\Auth; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; trait AuthenticatesUsers { use RedirectsUsers, ThrottlesLogins; /** * Show the application's login form. * * @return \Illuminate\Http\Response */ public function showLoginForm() { return view('auth.login'); } /** * Handle a login request to the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse * * @throws \Illuminate\Validation\ValidationException */ public function login(Request $request) { $this->validateLogin($request); // If the class is using the ThrottlesLogins trait, we can automatically throttle // the login attempts for this application. We'll key this by the username and // the IP address of the client making these requests into this application. if (method_exists($this, 'hasTooManyLoginAttempts') && $this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); } // If the login attempt was unsuccessful we will increment the number of attempts // to login and redirect the user back to the login form. Of course, when this // user surpasses their maximum number of attempts they will get locked out. $this->incrementLoginAttempts($request); return $this->sendFailedLoginResponse($request); } /** * Validate the user login request. * * @param \Illuminate\Http\Request $request * @return void * * @throws \Illuminate\Validation\ValidationException */ protected function validateLogin(Request $request) { $request->validate([ $this->username() => 'required|string', 'password' => 'required|string', ]); } /** * Attempt to log the user into the application. * * @param \Illuminate\Http\Request $request * @return bool */ protected function attemptLogin(Request $request) { return $this->guard()->attempt( $this->credentials($request), $request->filled('remember') ); } /** * Get the needed authorization credentials from the request. * * @param \Illuminate\Http\Request $request * @return array */ protected function credentials(Request $request) { return $request->only($this->username(), 'password'); } /** * Send the response after the user was authenticated. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ protected function sendLoginResponse(Request $request) { $request->session()->regenerate(); $this->clearLoginAttempts($request); return $this->authenticated($request, $this->guard()->user()) ?: redirect()->intended($this->redirectPath()); } /** * The user has been authenticated. * * @param \Illuminate\Http\Request $request * @param mixed $user * @return mixed */ protected function authenticated(Request $request, $user) { // } /** * Get the failed login response instance. * * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response * * @throws \Illuminate\Validation\ValidationException */ protected function sendFailedLoginResponse(Request $request) { throw ValidationException::withMessages([ $this->username() => [trans('auth.failed')], ]); } /** * Get the login username to be used by the controller. * * @return string */ public function username() { return 'email'; } /** * Log the user out of the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function logout(Request $request) { $this->guard()->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return $this->loggedOut($request) ?: redirect('/'); } /** * The user has logged out of the application. * * @param \Illuminate\Http\Request $request * @return mixed */ protected function loggedOut(Request $request) { // } /** * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\StatefulGuard */ protected function guard() { return Auth::guard(); } }長いですが、loginメソッドが記載されていましたね。
内容までは解説しませんが・・・ただ、
login
メソッドが上記ファイルに記載されているとは言っても、上記ファイルを編集することはおススメしません。
vendor
フォルダにはComposerでインストールしたライブラリ等が含まれるので、影響範囲が大きいです。
処理の制御はControllerで行うのが基本になりますので、LoginController
の方を修正することにします。LoginControllerの修正
以下のように修正してみました。
LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; //追加 use Illuminate\Support\Facades\Auth; //追加 use Illuminate\Support\Facades\DB; //追加 class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ //以下を修正 use AuthenticatesUsers{ login as _login; } /** * Where to redirect users after login. * * @var string */ protected $redirectTo = RouteServiceProvider::HOME; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); } //以下を追記 public function login(Request $request){ $response = $this->_login($request); $user_id = Auth::id(); $count = DB::table("sessions") ->where("user_id", $user_id) ->count(); if ($count === 0){ return $response; } else{ Auth::logout(); \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。"); return view("auth.login"); } } }解説
use AuthenticatesUsers{ login as _login; }これによって、後述する自作のloginメソッドに向くようにしています。
public function login(Request $request){ $response = $this->_login($request); $user_id = Auth::id(); $count = DB::table("sessions") ->where("user_id", $user_id) ->count(); if ($count === 0){ return $response; } else{ Auth::logout(); \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。"); return view("auth.login"); } }こちらが追記した
login
メソッドです。
リネームした_login
メソッドを実行してreturn
の前に分岐を入れています。ここでは詳しい説明はしませんが、Authファザードを使ってログインしたユーザーのIDを取得して、DBファザードを使ってSQLの実行をしています。
既にログインされている場合は、自信をログアウトさせてログイン画面に遷移するようにしています。
\Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。");ちなみに、フラッシュと言って、遷移先に1回だけメッセージを表示することができます。
ということで、メッセージを表示する
login.blade.php
にメッセージ表示するよう追記します。/resources/views/auth/login.blade.php@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <!--追記--> @if (session('message')) <div class="alert alert-warning"> {{ session('message') }} </div> @endif <!--ここまで--> <div class="card"> <div class="card-header">{{ __('Login') }}</div> <div class="card-body"> <form method="POST" action="{{ route('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検証結果
Chromeでログインしてみる(1人目)
1人目としてなので、ログインできてOKです!Firefoxでログインしてみる(2人目)
2人目なのでログインできずに、未ログインの状態でログイン画面に戻っていますね!
メッセージも表示されていていい感じです!完成?
上記内容でうまく同時ログイン制御ができています。
ただ、一つ考慮しないといけないことがあります。
この内容でうまく処理ができるのは「ログアウト処理をちゃんとしている」場合です。よくあると思いますが、皆さんログアウト処理をせずにブラウザのタブを閉じたり、ブラウザ自体を閉じたりして作業を終了させること多いと思います。
その場合、sessions
テーブルのuser_is
``に値が入ったままで残り続けます・・・
そうなると、一生ログインできなくなってしまいます・・・もう1つの条件
ここで使うのが
last_activity
カラムです。
このカラムには最終行動したUNIX時間が格納されます。なので、例えば20分操作しなかった場合にログアウトしたとみなすものとします。
それならば、ログインできる条件を「自アカウントでログインしている、かつ、現在時刻と最終行動時間の差が1200秒以内であるユーザーが0人」とすれば良いと考えます。なので、以下のように
LoginController
のlogin
メソッドに条件を追加します。public function login(Request $request){ $response = $this->_login($request); $user_id = Auth::id(); $time = time(); //現在時刻のUNIX時間 $count = DB::table("sessions") ->where("user_id", $user_id) ->where("last_activity", ">", $time-1200) //条件(AND)追加 ->count(); if ($count === 0){ return $response; } else{ Auth::logout(); \Session::flash("message", "他のユーザーがログインしています。時間を置いて再ログインしてください。"); return view("auth.login"); } }こんな感じでしょうか。
ちなみに「20分操作しなかった場合にログアウトしたとみなす」とする場合はLaravelの.env
および、config/session.php
のSESSION_LIFETIME
も20分にして合わせておくのがベストです。
逆にそうしないと2人ログインできている状態にもできるので、同時ログイン制御の目的は達成できないですね。最後に
後勝ちの同時ログイン制御は
logoutOtherDevices
を使えるので比較的楽に実装できそうですが、
先勝ちの場合はどうしようかと思って考えついた方法です。
もっと楽な方法、リスクが少ない方法があれば教えて欲しいです^^参考記事
- 投稿日:2021-02-20T00:47:21+09:00
AWSのEC2へのデプロイ②(必要なパッケージのインストール)
AWSのEC2へのデプロイをしたいと思い備忘録のために手順を記録します。
初学者の為、ご指摘等ありましたらご連絡いただければ幸いです。前回の記事はこちら
AWSのEC2へのデプロイ①(EC2インスタンス作成からログインまで)前回の内容は言語関係ないのですが今回から必要なパッケージが言語によって異なるのでご注意ください。
はじめに
私自身この手順で無事にデプロイができましたが、途中でエラーが出たことによりいろいろな記事を参考にさせて頂きました。
この場を借りて御礼申し上げます。そのため、私に知識がないばかりに私の環境に必要のない操作もあるかもしれません。
初学者のためご承知おきください。
お気づきの点ございましたらご連絡いただければとても喜びます。環境
Version PHP 7.4.14 Laravel 8.24.0 mysql 8.0.23 docker 20.10.2 docker-compose 1.27.4 EC2では
Amazon Linux 2
を利用しています。前提
- AWSのアカウントを作成済みである
- EC2のインスタンス作成済みでsshにログインできる
EC2インスタンスに接続
1.AWSのEC2の画面で下記画像のように
インスタンス
から接続
を押下します。2.下記画像のように
SSHクライアント
を押下後、一番下の文字列をコピーします。3.コピーした
ssh -i ~
をターミナルで入力するとターミナルAre you sure you want to continue connecting (yes/no/[fingerprint])?と出るので
yes
と入力します。4.下記のような内容が出力されていればOKです。
ターミナル[ec2-user@ip-×××-××-××-××× ~]$gitのインストール
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドでyumをupdateします。
ターミナル$ sudo yum update -y2.下記コマンドでgitをインストールします。
ターミナル$ sudo yum install git3.下記コマンドご自身のユーザー名とパスワードを設定します。
ターミナル$ git config --global user.name [ユーザー名] $ git config --global user.email [メールアドレス]4.下記コマンドで確認します。
ターミナル$ git config --list | grep user5.下記のように出ていればOKです。
ターミナルuser.name=[ユーザー名] user.email=[メールアドレス]Dockerのインストール
コマンドは全てEC2インスタンス内で行います。
Dockerのインストールは公式のAmazonECSにおけるDockerの基本で確認しました。1.下記コマンドで最新の
Docker Engineパッケージ
をインストールします(Amazon Linux 2用のコマンドです。)ターミナルsudo amazon-linux-extras install docker2.下記コマンドでDockerサービスを開始します。
ターミナルsudo service docker start3.下記コマンドで
ec2-user
をdockerグループ
に追加するとsudoコマンド
を使用せずにDockerコマンド
を実行できます。ターミナルsudo usermod -a -G docker ec2-user4.一旦EC2インスタンスからログアウト(exit)して再ログインします。
※これをしなくて私はエラーになりましたので大事です。5.下記コマンドで
ec2-user
がsudoコマンド
を使用せずにDockerコマンド
を実行できることを確認します。ターミナルdocker info下記のようなメッセージが出ればOKです。
ターミナルClient: Debug Mode: false Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 //省略docker-composeのインストール
参考記事:M1 Mac + Laravel + Docker + AWS でポートフォリオを作るまで[初心者][未経験]
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドで
su
に入ります。ターミナルsudo -i2.下記コマンドでcomposeをインストールします。
ターミナルcurl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose3.下記コマンドで実行権限を付与します。
ターミナルchmod +x /usr/local/bin/docker-compose4.exitをして抜けます。
ターミナルexit
5.下記コマンドでdocker-composeがインストールされているか確認します。
ターミナルdocker-compose --version下記のようにバージョンが出てくればOKです。
ターミナルdocker-compose version 1.27.4, build 40524192参考にした先ほどの記事で
docker-compose.yml
のバージョンを書き換えました。docker-compose.ymlversion: "3.3" ←変更 services: app: build: ./infra/php volumes: - ./backend:/work //省略EC2インスタンス内でDockerを起動
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドでgitからcloneします。
ターミナルgit clone [githubのURL]2.下記コマンドでフォルダがあるか確認します。
ターミナルls
3.下記コマンドでディレクトリを移動します。
ターミナルcd [ディレクトリ名]4.下記コマンドでdockerコンテナを立ち上げます。
少し長いメッセージが出ます。ターミナルdocker-compose up -d下記のようなメッセージが出ればOKです。
ターミナルCreating ×××_app_1 ... done Creating ×××_web_1 ... done Creating ×××_db_1 ... done Creating phpmyadmin ... done5.下記コマンドでステータスを確認します。
ターミナルdocker-compose ps
State
が全てup
になっていればOKです。PHPのインストール
参考記事:AWS EC2 AmazonLinux2 PHPをインストールする
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドでインストールリポジトリを追加します。
ターミナルsudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpmターミナルsudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm2.下記コマンドで既存のPHPパッケージを削除します。(削除するものがなくても問題ないです)
ターミナルsudo yum remove php3.下記コマンドでPHPのパッケージをインストールします。
ターミナルsudo yum install --enablerepo=remi,remi-php74 php php-devel php-mbstring php-pdo php-gd php-xml php-mcrypt4.下記コマンドでバージョンを指定してインストールします。
途中で[y/d/N]
とインストールしてもいいかの選択が出るのでy
を押しEnterで進めます。ターミナルsudo amazon-linux-extras install php7.45.下記コマンドでその他の必要なパッケージをインストールします。
こちらも途中で[y/d/N]
とインストールしてもいいかの選択が出るのでy
を押しEnterで進めます。ターミナルsudo yum install --enablerepo=remi,amzn2extra-php7.4 php-xml php-mbstring6.下記コマンドでyumにてインストールされたphpと名前がつくパッケージの一覧を表示し確認します。
ターミナルyum list installed | grep php下記のように表示されればOKです。
ターミナルphp-cli.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-common.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-fpm.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-json.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-mbstring.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-mysqlnd.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-pdo.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.4 php-xml.x86_64 7.4.7-1.amzn2 @amzn2extra-php7.47.無事に成功すれば下記コマンドでバージョンが表示されます。
ターミナルphp --versionMariaDBのインストール
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドでMariaDBのインストールをします。
ターミナルsudo yum -y install mysql56-server mysql56-devel mysql56 mariadb-server mysql-devel2.下記コマンドでデータベースを起動します。
ターミナルsudo systemctl start mariadb下記コマンドで起動できたかを確認します。
ターミナルsudo systemctl status mariadb緑の文字で
active (running)
と出ていればOKです。3.データベースのrootパスワードを設定します。
まずは下記のコマンドを入力します。
ターミナルsudo /usr/bin/mysql_secure_installation次からどんどん質問が出てくるので答えていきます。
Enter current password for root (enter for none):
→Enterキーを押す。
Set root password? [Y/n]
→「Y」を入力してEnterキーを押す。
New password:
→ご自身で決めたパスワードを入力する。(画面には表示されないけど入力できています)
Re-enter new password:
→もう1度ご自身で決めたパスワードを入力する。
Remove anonymous users? [Y/n]
→「Y」を入力してEnterキーを押す。
Disallow root login remotely? [Y/n]
→「Y」を入力してEnterキーを押す。
Remove test database and access to it? [Y/n]
→「Y」を入力してEnterキーを押す。
Reload privilege tables now? [Y/n]
→「Y」を入力してEnterキーを押す。4.データベースに接続を確認します。
下記コマンドでデータベースに接続できるか確認します。
ターミナルmysql -u root -pパスワードを求められるので入力しEnterキーを押します。
ターミナルMariaDB [(none)]>上記のように表示されれば接続できています。
抜け出したいときはexit
で抜け出せます。Laravel環境設定
参考記事:AWSにEC2上にdockerを使用したlaravelをデプロイ④(gitクローン〜デプロイ、マイグレーション)
コマンドは全てEC2インスタンス内で行います。
1.下記コマンドでコンテナ内に入ります。
ターミナルdocker-compose exec app bash2.下記コマンドで.envファイルを作成します。
.envファイルはgitリポジトリにpushされないためです。ターミナルcp .env.example .env3.下記コマンドでcomposerをインストールします。
ターミナルcomposer install4.下記コマンドでAPP_KEYを発行します。
ターミナルphp artisan key:generate5.下記コマンドで権限を変更します。
ターミナルchmod 777 storage/logs vendor chmod 777 storage/framework/views chmod 777 storage/framework/sessions6.画像投稿がある場合は下記コマンドも必要みたいです。
ターミナルphp artisan storage:link chown -R www-data:root .7.下記コマンドでマイグレーションを実行します。
ターミナルphp artisan migrate php aritsan db:seed以上で無事にデプロイ ができました。
ただ、現在S3への画像の保存ができない状態なので解決出来次第修正致します。