20220114のlaravelに関する記事は7件です。

Laravel Docker AWS EC2デプロイ⑥

今回は、AWSのサービスを使用してSSLを無料発行し、前回独自で用意したドメインをSSL化していきます! 前回 SSL証明書取得 下記の記事を参考に進めていきますが、現在のAWS画面とは少し異なるので、その部分に関してはこちらに画像を記載しますので参考にしてください。 ・Certificate Managerの開き方。 1.Certificate Managerで検索してサービスを選択。 2.証明書リクエストボタンから申請可能。 3.パブリック証明書リクエストを選択して次へ。 4.独自で用意したドメイン名を入力し、リクエスト。 5.証明書発行確認 リクエストをクリックすると、最初の画面に戻り、証明書が作成されています。 詳細に関しては、証明書IDを選択することで確認できます! 6.Route 53でレコード作成 証明書IDをクリックすると、詳細画面が開きます。 画面中央部のRoute 53でレコード作成をクリック。 7.レコード作成 内容を確認してレコード作成をクリック。 メッセージが表示されれば無事作成完了です。 一旦別画面に移動し、もう一度戻ってくると、下記のようにステータス欄が検証中→発行済みに変わっていることがわかります! これで証明書を取得できたことになりました! SSL証明書の設定 では、取得した証明書の設定をしていきます。 1.ロードバランサーの作成 EC2画面左側のメニューバーから、ロードバランサーを開き、作成をクリック。 2.タイプを選択 今回は、ALBを使用します! 3. 設定項目を入力 先ず、画面上部ではロードバランサー名を入力し、スキームやIPアドレスタイプはデフォルトのままでOKです。 下にスクロールします。 マッピングは、EC2インスタンスを設置したVPCと組み込みたいゾーンを二つ以上選択しましょう。 セキュリティグループはALB用に新規で作成します。 その際、インバウンドとアウトバウンドルールも設定します。 リスナーとルーティングはHTTPSを許可するように入力します。 セキュアリスナーでは、先ほど作成したSSL証明書を選択します。 リスナーのターゲットグループも新規で作成します。 ヘルスチェックパスにはドメインにアクセスした際のリダイレクト先を指定します。 こちらの仕組みに関しては下記記事がわかりやすく説明されているので参考にしてください。 最後に、何のロードバランサーか区別がつきやすくするようタグにNameを設定し、概要を確認して問題なければ作成ボタンを押して作成します。 無事作成できればロードバランサーの画面に表示されます! レコード設定 最後に、初めにRoute 53で作成したドメインのレコードを編集します。 EC2に関連付けさせたELPの箇所をALBのDNSに変更します。 エイリアスにチェック→ALBを選択→リージョン選択→DNS 動作確認 では、https通信でドメインからアクセス確認してみましょう! https://????.com 無事SSL化されたドメインでログイン画面が表示されました! 今回はここまでにします! 次は、Auto Scalingの設定を進めていきます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP(とちょっとLaravel)の細々とした覚書

foreachで値を直接いじくりたいとき foreach ($data_array as &$data) { $data['中身']['値'] = true; } 繰り返し処理のあとの$data_arrayを変更された状態にしたい場合は、値の変数に&をつける。 値から配列のキーを取りたいとき array_search(値, 配列); 定数クラス内の配列('定数が表すもの'=>定数)からマジックナンバーを取り出したい時によく使ってます。 array_search($const, CONST_CLASS::CONST_LIST); 配列から任意の値を削除して、別の配列を作りたいとき こちらを参照しました。PHPで配列から特定の要素を削除する array_values(array_diff(配列, 第一引数から削除する値の配列); 配列同士を比較するarray_diffの戻り値(値だけ消えてキーはそのまま)をarray_valuesで整える。 リクエストで変更するカラムの配列を受け取り、その中から変更レコードのものを除外した配列を作ってwhereInに渡す、みたいな処理で使ってます。 一定期間の中でランダムな日付を作りたい時 こちらを参照しました。特定の2つの日付の間から、ランダムに1日を取得 HogeFactory.php $start = Carbon::create('2021', '1', '1'); $end = Carbon::create('2021', '12', '31'); $min = strtotime($start); $max = strtotime($end); $date = rand($min, $max); return [ 'なんらかの日付' => date('Y-m-d', $date), ]; Factoryでテストデータの日付を適当に作る際、この方法で1年の内どこかしらの日付を取ってくるようにしました。 空のコレクションはifでfalseにはならない なぜなら配列ではなく、Collectionクラスのインスタンスだから… 中身があろうとなかろうと… 中身の有無で処理を変えたいときは($collection->isEmpty())で真偽値を返してもらうとよい。 定数は定数クラスを作るのが楽 こちらを参照しました。Laravelで定数を使うときにConfigを使う腰抜けはもう死んだ configに定数.phpを作って配列をリターンするやり方はどうも不便でなりません。 デメリットは参考記事にも書いてありますし、個人的にはVSCodeで表示したとき一面まっかっかになってしまうのが見づらくてしょうがないです。 クラスで管理するメリットとしては、個人的にはテストコードで簡単に使えるという点に尽きると思います。 前職では定数はconfigで定義し、テストコード内の定数はすべてマジックナンバーで書いてましたので、変更がある度に地獄でした。 「0」だの「301(なんらかの独自ステータスコード)」だの、検索かけて書き換えようにもテストがデグらないか不安で不安で…見てるうちになんの数字だかわかんなくなってくるし。 というわけで定数クラス推しです。 PHP8.1ではfinal public const修飾子をつけることで、クラス定数を上書きさせないことが可能になりました。安心ですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelの多対多リレーションを復習+a

久しぶりにLaravelで多対多リレーションをやったので、復習+ついでに学習したことをまとめます。 一般的なモデルの設定 2つのモデルと、関連付け用の中間モデル。普通の形です。 Library(図書館) id Book(書籍情報マスタ) id LibraryBook(蔵書) library_id book_id 各モデルはphp artisan make:modelで作成されています。 関連付け 図書館モデルからそこの蔵書リストを作りたい、という場合は、LibraryモデルにbelongsToManyメソッドを記述するだけで可能になります。 Library.php /** * 蔵書一覧を取得 * * @return void */ public function books() { return $this->belongsToMany(Book::class); } $library->books; // 全取得 $library->books()->where('title', $request->input('title'))->get(); // 絞り込み // 取得してから絞り込み $library->books->filter(function ($value, $key) { return $value->title === $request->input('title'); }); 特定の本が所蔵されている図書館のリストを作る場合でも全く同様です。 Book.php /** * 蔵書一覧を取得 * * @return void */ public function libraries() { return $this->belongsToMany(Library::class); } Pivotでもうちょい便利にする php artisan make:modelで生成されたモデルクラスは、必ずModelクラスを継承したものとなります。 多対多リレーションの中間モデルに、ModelではなくPivotを継承したPivotモデルクラスを使用すると、より簡単に応用的なリレーションを組めるようになります。 php artisan make:model LibraryBook --pivot Pivotモデルクラスにするとどう便利なのか 今回の例では「図書館」と「蔵書」の関係ですが、例えば中間モデルが「貸出中」という状態を持つとします。 LibraryBook library_id book_id checked_out (trueが入ると「貸出中」) 先程の蔵書一覧取得のような形で「貸出中の蔵書一覧」を取得したい、という場合は、このようなリレーションを組むことができます。 Library.php /** * 貸出中の蔵書一覧を取得 * * @return void */ public function lendingBooks() { return $this->belongsToMany(Book::class) ->wherePivot('checked_out', true); } whereメソッド同様、カラムと値を指定して絞り込むことができます。 処理内で中間テーブルの値を使いたい場合は、wherePivotと並べてwithPivotメソッドをつなげれば、引数で指定したカラムを一緒に取得できます。 $library->lendingBooks; // 貸出中の蔵書を取得 他にどんなメソッドが使えるかはReadouble.comから。 これをやるのに必要な作業 補足:ルートモデル結合で親子モデル両方を取得するとき ルートモデル結合でこのように書いたとき、 Route::get('/library/{library}/book/{book}/'.... 指定のlibraryの蔵書内にbook_idがあるかどうかを自動的に参照してもらえますが、このときテーブル名をlibrary_booksにしていたらエラーになりました。 ルートモデル結合の処理時には、自動的に2つのモデル名がアルファベット順に連結されたテーブル名を参照しようとするため、「book_librariesテーブルがないですよ」というエラーに。 任意のテーブル名を使うときは、belongsToManyメソッドの第二引数にテーブル名を入れるだけで参照先が上書きされます。 Library.php /** * 貸出中の蔵書一覧を取得 * * @return void */ public function lendingBooks() { return $this->belongsToMany(Book::class, 'library_books') ->wherePivot('lending', true); } リレーションについて理解しておくべきこと 中間テーブルの値を使ってリレーション先の状態を定義する場合、テストコード等でちょっとハマりました。ちょっと考えればわかったことかもしれませんが、まあ筆者はアホなんだなあと 遅延読み込み さて筆者は、貸出中の書籍だけを取り出すためのこんなリレーションを定義しました。 Library.php /** * 貸出中の蔵書を取得 * * @return void */ public function checkedOutBooks() { return $this->belongsToMany(Book::class, 'library_books') ->using(LibraryBook::class) ->withPivot('checked_out') ->wherePivot('checked_out', true); } この記述によって、こう書くことで貸出中の蔵書だけ取得できるようになったわけなので、 $library->checkedOutBooks; 返却処理のテストの中で、返却処理の前後で貸出中の蔵書を数えるアサーションをいれました。 $this->createCheckedOutBooks($request_books); // 3冊の貸出を行う $this->assertCount(3, $library->checkedOutBooks); // ~~~~~~返却処理を実行~~~~~~ $this->assertCount(0, $library->checkedOutBooks); 筆者は、こう書けばいい感じにテーブルの状態を比較できると思っちゃったわけです。 でも実際は、処理の前後で$library->checkedOutBooksの中身はなにも変わっていません。 一度取ってきたらもう取らない クエリログを出力するメソッドを使って実験したところ、こんな結果でした。 処理前に出力 \DB::enableQueryLog(); $library->checkedOutBooks; dump(\DB::getQueryLog()); // 出力されたクエリ * array:1 [ * 0 => array:3 [ * "query" => "select `books`.*, `library_books`.`library_id` as `pivot_library_id`, `library_books`.`book_id` as `pivot_book_id`, `library_books`.`checked_out` as `pivot_checked_out` from `books` inner join `library_books` on `books`.`id` = `library_books`.`book_id` where `library_books`.`library_id` = ? and `library_books`.`checked_out` = ?" * "bindings" => array:2 [ * 0 => 6 * 1 => true * ] * "time" => 1.3 * ] * ] $this->createCheckedOutBooks($request_books); // 3冊の貸出を行う $this->assertCount(3, $library->checkedOutBooks); // 貸出中を数えても0。通らない 処理後に出力 $this->createCheckedOutBooks($request_books); // 3冊の貸出を行う $this->assertCount(3, $library->checkedOutBooks); // 通る \DB::enableQueryLog(); $library->checkedOutBooks; dump(\DB::getQueryLog()); // 出力されたクエリ [] このような結果に。 つまり、初回はDBを参照して値を取得するが、その後は取得済みの値を参照する仕様になっているということのようです。 一度取得した値は、単にDBから取り出した情報がメモリ空間に配置されているに過ぎないので、DBの状態が変わっても取得済みの値が変わってるはずありません。当たり前ですね。 ちなみに、Readouble.comのリレーションのページには、 プロパティとしてEloquentリレーションへアクセスすると、関連するモデルは「遅延読み込み」されます。 と記述されています。 つまり、 $library = Library::find(1); が実行された時点では、Libraryのリレーション先は読み込まれていません。 そりゃそうですよね。リレーション先なんていくつ設定されるかわかりませんし。 $library = Library::find(1); $library->checkedOutBooks; 2行目まで来て、初めてリレーション先を読み込むクエリが発行されます。 つまりデフォルトでは、 $library = Library::find(1); // 読み込まない $library->checkedOutBooks; // 読み込む->保存される $library->checkedOutBooks; // 読み込まない こうなるように実装されており、なるべく余計なクエリを発行しないような作りになっていると。 ただReadouble.comで紹介されている通り、このままforeach ($library->books as $book)してしまうと、一冊ごとにクエリを発行して値を取ってきます。N+1問題発生。取得した値をただ返すだけでなく処理に用いる場合は、withでリレーション先を含めて取り出してから使うべきです。 筆者は$libraryの取得にはすべてルートモデル結合を使っているので、RouteServiceProvider内でwithメソッドを付けることでN+1を防止します。 RouteServiceProvider.php public function boot() { + Route::bind('library', function ($id) { + return Library::with('books')->findOrFail($id); + }); まとめ 夏以降はフロントエンドにかまけていたのでいろいろと思い出さないといけないことが多く、また知らなかった仕様などもあって、自分はまだまだ何も知らないんだなあと思わされました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(仮)Laravel Snappyにて、cssサイズが想定通りにする。(wkhtmltopdf側の設定をする。)

※私的備忘録のため、乱雑に記載しています。 【問題点】 Laravel Snappy にて、cssが想定通りに出力されない 【原因】 wkhtmltopdf にて、自動調整がdefaultでonになっている。 【解決方法】 snappyのsetOptionにて、以下のように設定すれば良い。 ->setOption('disable-smart-shrinking', true); ※wkhtmlでは、--disable-smart-shrinking true を追加してあげれば良い。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでCSVファイルを読み込み、DBに住所データを一括登録

はじめに 実務で表題の通り、郵便データをCSVファイルから読み取り、住所データをDBに一括登録する場面があったので、その方法をご紹介致します。 今回は下記リンクから飛べる、読み仮名データの促音・拗音を小書きで表記するものの中にあるken_all.zipという全国一括版のファイルをダウンロードしました 環境 Laravel 8.* MySQL5.7 データをダウンロードする 全国一括(KEN_ALL.CSV)というファイルをZIPファイルを展開する。 展開したファイルを laravelプロジェクトディレクトリ/storage/app/csv直下に配置し、Laravelからファイルを読み込むようにする。 住所データを保存するテーブルを作成 下記コマンドを実行して、住所データを格納する為のマイグレーションファイルと、モデルを同時に作成 php artisan make:model PostalCode -m database/migrations内にxxxx_xx_xx_xxxxxx_create_postal_codes_table.phpというファイルができているので、up()の中身を下記のように変更してphp aritsan migrateを実行する。 public function up() { Schema::create('postal_codes', function (Blueprint $table) { $table->increments('id');//オートインクリメントで連番 $table->unsignedInteger('first_code')->index();//郵便番号の始め(3桁)の部分 $table->unsignedInteger('last_code')->index();//郵便番号の後ろ(4桁)の部分 $table->string('prefecture');//都道府県名 $table->string('city');//市区町村 $table->string('address');//それ移行の住所 }); } 先程作成したモデルは下記のように変更 namespace App; use Illuminate\Database\Eloquent\Model; class PostalCode extends Model { public $timestamps = false;//マスタデータとして使用するので不要とのことだったので、timestampsをfalseに指定。 protected $guarded = ['id'];//データ追加時に思わぬエラーが起きないようidを指定 } ここまでの手順で、住所データをDBに登録する為の土台が出来上がりました。 (住所登録用のテーブルとモデルが完成) CSVファイルから郵便データを読み取り、DBへ一括登録 日本郵便のデータは頻繁に更新されているようなので、その都度更新が出来るよう郵便データ登録用のコマンドを作成します。 php artisan make:command ImportPostalCodeCommand 次に、app/Console/Commands/ImportPostalCodeCommand.phpを開いて、中身を下記のように変更します。 <?php namespace App\Console\Commands; use Illuminate\Console\Command; class ImportPostalCodeCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'import:postal-code';//コマンド実行名 /** * The console command description. * * @var string */ protected $description = 'Import postal-code';//コマンドの説明内容 /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { // 登録処理の前にテーブルを一旦、空にする \App\PostalCode::truncate(); // CSVファイルの文字コード変換 $csv_path = storage_path('app/csv/KEN_ALL.CSV');//先程ダウンロードしたCSVファイルのパスを書く $converted_csv_path = storage_path('app/csv/postal_code_utf8.csv');//CSVファイルの文字コードをSJIS-winからUTF-8に変換した後のファイル名を指定 file_put_contents( $converted_csv_path, mb_convert_encoding( file_get_contents($csv_path), 'UTF-8', 'SJIS-win' ) ); // 文字コードを変換したCSVファイルから郵便データを取得してDBへ保存 $file = new \SplFileObject($converted_csv_path); $file->setFlags(\SplFileObject::READ_CSV); foreach ($file as $row) { if (!is_null($row[0])) { //空行の場合、登録処理の際にエラーが発生することがあるので条件分岐させる \App\PostalCode::create([ 'first_code' => intval(substr($row[2], 0, 3)), 'last_code' => intval(substr($row[2], 3, 4)), 'prefecture' => $row[6], 'city' => $row[7], 'address' => (str_contains($row[8], '(')) ? current(explode('(', $row[8])) : $row[8] ]); } } } } php artisan import:postal-codeを実行して郵便データを移動させます。(全12万件と、データ量がとても大きいので、登録処理に少し時間がかかるのと、大規模のデータを登録するのでテーブルの削除処理から登録までにトランザクションを張った方が良いかと思います。(下記のような形で) 以上で、郵便データの登録処理が完了致しました。作業に取りかかるまでは難しそうなイメージでしたが、いざ終わってみると意外と簡単という感想です。 今回は実務として着手することはなかったのですが、個人で住所検索処理の実装などもやってみます。 ここまで読んで頂きありがとうございます。今後ともメモ代わりに書いていけたらと思っておりますので宜しくお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cropper.jsで加工した画像をAjaxでLaravelに送り保存する。

この記事では、Cropper.jsを利用し画像を加工したあとに、加工した画像をAjaxを通してLaravelに渡し、その画像を保存する方法を紹介します。 Cropper.jsは、ライブプレビューとカスタムアスペクト比をサポートしており、JavaScript / jQueryプラグインを画像のトリミングに利用するのに便利です。 事前準備 1. フォルダの作成 ファイルを保存する先のフォルダを作成します。 今回、コントローラで指定するフォルダは「storage/app/public/upload」です。 ファイルをアップロードして画像を切り取って保存を押すと、このフォルダに保存されます。 バックエンド側 2. ルーティング 次に下記のルーティングを設定してください。 routes/web.php Route::get('image-cropper','ImageCropperController@index'); Route::post('image-cropper/upload','ImageCropperController@upload'); 3. コントローラ 次に下記のようなコントローラを作成します。 app/Http/Controller/ImageCropperController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class ImageCropperController extends Controller { public function index() { return view('cropper'); } public function upload(Request $request) { $folderPath = storage_path('app/public/upload/'); // 保存先のパス $image_parts = explode(";base64,", $request->image); $image_type_aux = explode("image/", $image_parts[0]); // ファイルの型を取り出す(今回は使わない) $image_type = $image_type_aux[1]; // ファイルの型を取り出す(今回は使わない) $image_base64 = base64_decode($image_parts[1]); // 画像データとして取り出す $file = $folderPath . uniqid() . '.png'; // 保存に使うファイル名 file_put_contents($file, $image_base64); // ファイルを保存 return response()->json(['success'=>'success']); // JSONでレスポンスを返す } } フロントエンド側 3. ビュー 最後にお待ちかねのビューを用意します。 ここにCropper.jsの処理も記述します。 resources/views/cropper.blade.php <!DOCTYPE html> <html> <head> <title>Laravel Crop Image Before Upload using Cropper JS - LaravelCode</title> <meta name="_token" content="{{ csrf_token() }}"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha256-WqU1JavFxSAMcLP2WIOI+GB2zWmShMI82mTpLDcqFUg=" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.css" integrity="sha256-jKV9n9bkk/CTP8zbtEtnKaKf+ehRovOYeKoyfthwbC8=" crossorigin="anonymous" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.js" integrity="sha256-CgvH7sz3tHhkiVKh05kSUgG97YtzYNnWt6OXcmYzqHY=" crossorigin="anonymous"></script> </head> <style type="text/css"> img { display: block; max-width: 100%; } .preview { overflow: hidden; width: 160px; height: 160px; margin: 10px; border: 1px solid red; } .modal-lg{ max-width: 1000px !important; } </style> <body> <div class="container"> <h1>Cropper.JSで加工した画像をAjaxでLaravelに送信</h1> <input type="file" name="image" class="image"> </div> <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel" aria-hidden="true"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <div class="img-container"> <div class="row"> <div class="col-md-8"> <img id="image" src="https://avatars0.githubusercontent.com/u/3456749"> </div> <div class="col-md-4"> <div class="preview"></div> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button> <button type="button" class="btn btn-primary" id="crop">保存</button> </div> </div> </div> </div> </div> </div> <script> var $modal = $('#modal'); var image = document.getElementById('image'); var cropper; $("body").on("change", ".image", function(e){ var files = e.target.files; var done = function (url) { image.src = url; $modal.modal('show'); }; var reader; var file; var url; if (files && files.length > 0) { file = files[0]; if (URL) { done(URL.createObjectURL(file)); } else if (FileReader) { reader = new FileReader(); reader.onload = function (e) { done(reader.result); }; reader.readAsDataURL(file); } } }); $modal.on('shown.bs.modal', function () { cropper = new Cropper(image, { aspectRatio: 1, viewMode: 3, preview: '.preview' }); }).on('hidden.bs.modal', function () { cropper.destroy(); cropper = null; }); $("#crop").click(function(){ canvas = cropper.getCroppedCanvas({ width: 160, height: 160, }); canvas.toBlob(function(blob) { url = URL.createObjectURL(blob); var reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = function() { var base64data = reader.result; $.ajax({ type: "POST", dataType: "json", url: "image-cropper/upload", data: {'_token': $('meta[name="_token"]').attr('content'), 'image': base64data}, success: function(data){ $modal.modal('hide'); alert("success upload image"); } }); } }); }) </script> </body> </html> 最後に 以上の実装でAjaxを通してLaravelにCropper.jsで加工した画像を送れるはずです。 ぜひ試してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】Laravel-AdminLTEとlivewireを布教したい

はじめに 普段小規模な案件を主にやっていて、よく管理画面を作るんですが Laravel-AdminLTEとlivewireを採用した所かなり効率が良かったので布教したい。 特にLaravel-AdminLTEに付属してるコンポーネントが便利だったのでそれを使いながら チュートリアルがてらLaravel-AdminLTE+livewireで非同期掲示板を作っていきます。 参考記事:Laravel-AdminLTEの付属コンポーネントで楽にフォームを作ろうぜ 目次 環境構築 モデルとマイグレーション作成 シーダー作成 投稿機能作成 書き込んでみよう 編集機能作成 おわりに 参考文献 環境構築 Laravel8をインストール。 composer create-project --prefer-dist laravel/laravel sample "8.*" livewireをインストール、ついでにLaravel-AdminLTEも入れます。 cd sample sample> composer require livewire/livewire sample> composer require jeroennoten/laravel-adminlte sample> php artisan adminlte:install sample> php artisan adminlte:install --only=main_views config/adminlte.php内のlivewireを適応させます。 画面上部に@livewireStyleがテキストとして表示されてしまう場合は resources/views/vendor/adminlte/master.blade.php内の @livewireStyles ↓ @livewireStyles() @livewireScripts ↓ @livewireScripts()に書き換えます。 モデルとマイグレーション作成 model作ります、ついでにマイグレーションファイルも作成。 php artisan make:model Board -m php artisan make:model Tag -m php artisan make:model BoardTag -m 投稿に付けるタグにリレーションをセット app/Models/Board.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Board extends Model { use HasFactory; protected $fillable = [ 'name', 'contents', ]; public function tags(){ return $this->belongsToMany(Tag::class,'board_tags'); } } シーダー作成 php artisan make:seeder TagsTableSeeder タグ作るついでに投稿を1つ作ります database/seeders/TagsTableSeeder.php <?php namespace Database\Seeders; use App\Models\Board; use App\Models\Tag; use Illuminate\Database\Seeder; class TagsTableSeeder extends Seeder { public function run() { Tag::insert([ ['name' => '食べ物'], ['name' => '投げ物'], ['name' => '偽物'], ['name' => '煮物'], ]); $board = Board::create([ 'name' => '名無しさん', 'contents' => 'こんちゃ' ]); $board->tags()->attach([1,2]); } } database/seeders/DatabaseSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run() { $this->call(TagsTableSeeder::class); } } 投稿機能作成 掲示板を表示する親コンポーネントと、投稿用の子コンポーネントを作ります php artisan make:livewire Board/Index php artisan make:livewire Board/Create テンプレートを継承して投稿データを渡します app/Http/Livewire/Board/Index.php <?php namespace App\Http\Livewire\Board; use App\Models\Board; use Livewire\Component; class Index extends Component { //追記:他コンポーネントから'RefreshBoard'を発火されるとマジックメソッド'$refresh'が発動!!!再レンダリングだ! protected $listeners = ['RefreshBoard' => '$refresh']; public function render() { $boards = Board::all(); return view('livewire.board.index',compact('boards')) ->extends('adminlte::page') ->section('content'); } } bladeも書いてきます resources/views/livewire/board/index.blade.php <div> <div class="col-12 pt-5"> @foreach($boards as $board) <div class="card col-5 mx-auto"> <div class="card-body"> <h5 class="card-title">{{ $board->name }}</h5> <p class="card-text">{{ $board->contents }}</p> @foreach($board->tags as $tag) <span class="badge badge-info">{{ $tag->name }}</span> @endforeach </div> </div> @endforeach </div> </div> ルーティングも書きます routes/web.php <?php use Illuminate\Support\Facades\Route; Route::get('/',App\Http\Livewire\Board\Index::class); マイグレーション&シーダーコマンドを叩いて画面を見てみましょう php artisan migrate:fresh --seed はい1件だけ表示されました。 書き込み用のコンポーネントも書いていきます app/Http/Livewire/Board/Create.php <?php namespace App\Http\Livewire\Board; use App\Models\Board; use App\Models\Tag; use Livewire\Component; class Create extends Component { public $tags; public $select_tags = []; public $name; public $contents; protected $rules = [ 'name' => 'required', 'contents' => 'required', ]; protected $validationAttributes = [ 'name' => 'なまえ', 'contents' => 'ないよう' ]; //バリデーションの日本語ファイル用意するのが面倒だったので'required'だけ書き換え protected $messages = [ 'required' => ':attributeは必ず指定してください。', ]; //コンストラクタ的な役割、プロパティの初期化とかに使う public function mount() { $this->tags = Tag::all(); } //こいつがメイン、更新が入るとこいつが走る public function render() { return view('livewire.board.create'); } //モーダル展開用 public function openModal() { //jsのイベントを発火させる $this->dispatchBrowserEvent('show_create_modal'); } //書き込み処理 public function create() { //バリデーション発動、ひっかかったらここで止まります $this->validate(); //投稿情報作成 $board = Board::create([ 'name' => $this->name, 'contents' => $this->contents ]); //タグ紐づけ $board->tags()->attach($this->select_tags); //フラッシュメッセージ session()->flash('createMessage', 'かきこんだよ'); //全てのプロパティを初期化。mountで設定した値も消える $this->reset(['name','contents']); //親コンポーネントのrefreshイベント発火 $this->emitUp('RefreshBoard'); } } mountはページが開いた時に一度だけ動きます。 blade側をLaravel-AdminLTEのbladeコンポーネントで書いてきます。 resources/views/livewire/board/create.blade.php <div> <form wire:submit.prevent="create"> <x-adminlte-modal wire:ignore.self id="createModal" title="書きこむ" size="md" theme="teal" v-centered static-backdrop scrollable> <div class="card"> <div class="card-body"> @if (session()->has('createMessage')) <x-adminlte-alert theme="success" title="Success"> {{ session('createMessage') }} </x-adminlte-alert> @else <x-adminlte-input name="name" label="なまえ" wire:model.lazy="name"/> <x-adminlte-textarea name="contents" label="ないよう" rows=5 wire:model.lazy="contents"> </x-adminlte-textarea> @foreach($tags as $tag) <div class="custom-control custom-checkbox custom-control-inline"> <input type="checkbox" class="custom-control-input" id="{{ $tag->id }}" value="{{ $tag->id }}" wire:model.lazy="select_tags"> <label class="custom-control-label" for="{{ $tag->id }}">{{ $tag->name }}</label> </div> @endforeach @endif </div> </div> <x-slot name="footerSlot"> <div class="col"> <div class="row"> <div class="col-6"> @if (!session()->has('createMessage')) <x-adminlte-button type="submit" class="btn-lg btn-block" label="登録" theme="success"/> @endif </div> <div class="col-6"> <x-adminlte-button theme="secondary" class="btn-lg btn-block" label="閉じる" data-dismiss="modal"/> </div> </div> </div> </x-slot> </x-adminlte-modal> </form> <x-adminlte-button label="書きこむ" data-toggle="modal" class="bg-teal mb-3" wire:click="openModal()"/> <script> //モーダル展開用 window.addEventListener('show_create_modal', event => { $('#createModal').modal('show'); }); </script> </div> <form wire:submit.prevent="create"> ページをリロードさせない為にpreventを引っ付けています。 参考記事:【JavaScript】event.preventDefault()が何をするのか <x-adminlte-input name="name" label="なまえ" wire:model.lazy="name"/> wire:modelという記述でプロパティとバインドをさせています。 バインドするタイミングをある程度調整できるので、 1文字1文字のリアルタイムの検索やバリデーションをする時は修飾子を書かず、サブミット時にバインドさせたければ.deferを使う、など。 今回.lazyを使っていますが基本フォームの作成などは.deferで良いです。 修飾子 バインドするタイミング 無し 入力時 .lazy 入力エリア外にフォーカスされた時 .defer 次のネットワークリクエスト 参考記事:Livewire 2.x プロパティ Laravel-AdminLTEのbladfeコンポーネントがlivewireを想定していないせいか フォームのエラー表示がされないので少しコンポーネントを改造します。 29行目辺りのif文を書き換えます。 resources/views/vendor/adminlte/components/form/input-group-component.blade.php @if($isInvalid() && ! isset($disableFeedback)) <span class="invalid-feedback d-block" role="alert"> <strong>{{ $errors->first($errorKey) }}</strong> </span> @endif これを resources/views/vendor/adminlte/components/form/input-group-component.blade.php @if($errors->has($errorKey) && ! isset($disableFeedback)) <span class="invalid-feedback d-block" role="alert"> <strong>{{ $errors->first($errorKey) }}</strong> </span> @endif こう。 書き込み用の子コンポーネントを親コンポーネントに埋め込みます resources/views/livewire/board/index.blade.php <div> <div class="col-12 pt-5"> <!-- 追記 --> <div class="col-5 mx-auto"> <livewire:board.create/> </div> <!-- 追記ここまで --> @foreach($boards as $board) <div class="card col-5 mx-auto"> <div class="card-body"> <h5 class="card-title">{{ $board->name }}</h5> <p class="card-text">{{ $board->contents }}</p> @foreach($board->tags as $tag) <span class="badge badge-info">{{ $tag->name }}</span> @endforeach </div> </div> @endforeach </div> </div> モーダル用のボタンが表示されました。 書き込んでみよう モーダルを開いて何も選択せずに登録ボタンを押してみましょう あぁ~...エラー表示処理書かなくてもエラー出してくれるの良いわぁ...便利やわぁ 適当に書き込んでみましょう。 書き込んだら「閉じる」をクリック。 書き込まれてますね、良きかな。 編集機能作成 編集用のコンポーネントを作ります php artisan make:livewire Board/Edit app/Http/Livewire/Board/Edit.php <?php namespace App\Http\Livewire\Board; use App\Models\Tag; use Livewire\Component; class Edit extends Component { public $board; public $tags; public $select_tags = []; protected $rules = [ 'board.name' => 'required', 'board.contents' => 'required', ]; protected $validationAttributes = [ 'board.name' => 'なまえ', 'board.contents' => 'ないよう' ]; public function mount() { $this->tags = Tag::all(); //紐づいてるタグのIDを配列として取得 $this->select_tags = array_map('strval', $this->board->tags()->get()->pluck('id')->toArray()); } public function render() { return view('livewire.board.edit'); } public function openModal() { $this->dispatchBrowserEvent('show_edit_modal_'.$this->board->id); } public function update() { $this->validate(); $this->board->save(); $this->board->tags()->detach(); $this->board->tags()->attach($this->select_tags); session()->flash('updateMessage', 'こうしんしたよ'); $this->emitUp('RefreshBoard'); } } mountメソッド内にある $this->select_tags = array_map('strval',$this->board->tags()->get()->pluck('id')->toArray()); どうして配列の中身をstring型にしてるかと言うと、入力フォームからバインドされる値が文字列なので、 この状態から食べ物タグをクリックすると 食べ物のIDだけstringになっています。タグを追加するだけなら問題無いのですが、タグを解除しようとすると 偽物タグをクリックしても消えてくれません。再度偽物タグをクリックすると? はいstring型のIDが追加されました。もうグチャグチャです。 めちゃくちゃ気持ち悪い挙動になるので気になった人は配列の中身をstring型にせずに弄ってみてください。 resources/views/livewire/board/edit.blade.php <div> <form wire:submit.prevent="update"> <x-adminlte-modal wire:ignore.self id="updateModal_{{ $board->id }}" title="書きこむ" size="md" theme="primary" v-centered static-backdrop scrollable> <div class="card"> <div class="card-body"> @if (session()->has('updateMessage')) <x-adminlte-alert theme="success" title="Success"> {{ session('updateMessage') }} </x-adminlte-alert> @else <x-adminlte-input name="board.name" label="なまえ" wire:model.lazy="board.name"/> <x-adminlte-textarea name="board.contents" label="ないよう" rows=5 wire:model.lazy="board.contents"> </x-adminlte-textarea> @foreach($tags as $tag) <div class="custom-control custom-checkbox custom-control-inline"> <input type="checkbox" class="custom-control-input" id="{{ $tag->id }}_{{ $board->id }}" value="{{ $tag->id }}" wire:model.lazy="select_tags"> <label class="custom-control-label" for="{{ $tag->id }}_{{ $board->id }}">{{ $tag->name }}</label> </div> @endforeach @endif </div> </div> <x-slot name="footerSlot"> <div class="col"> <div class="row"> <div class="col-6"> @if (!session()->has('updateMessage')) <x-adminlte-button type="submit" class="btn-lg btn-block" label="更新" theme="success"/> @endif </div> <div class="col-6"> <x-adminlte-button theme="secondary" class="btn-lg btn-block" label="閉じる" data-dismiss="modal"/> </div> </div> </div> </x-slot> </x-adminlte-modal> </form> <x-adminlte-button label="編集" data-toggle="modal" class="bg-primary mb-3" wire:click="openModal()"/> <script> //モーダル展開用 window.addEventListener('show_edit_modal_{{ $board->id }}', event => { $('#updateModal_{{ $board->id }}').modal('show'); }); </script> </div> board.nameのようにname用のプロパティを作らなくてもネストして書けます。便利ですな。 編集用のコンポーネントを埋め込みます <livewire:board.edit :board="$board" :wire:key="$board->id"/> app/Http/Livewire/Board/Edit.phpのboardプロパティに$boardを渡して、 livewireがどの投稿がどのコンポーネントかを分かるようにするためにコンポーネントのキーをセット。 resources/views/livewire/board/index.blade.php <div> <div class="col-12 pt-5"> <div class="col-5 mx-auto"> <livewire:board.create/> </div> @foreach($boards as $board) <div class="card col-5 mx-auto"> <div class="card-body"> <!-- 追記 --> <livewire:board.edit :board="$board" :wire:key="$board->id"/> <!-- 追記ここまで --> <h5 class="card-title">{{ $board->name }}</h5> <p class="card-text">{{ $board->contents }}</p> @foreach($board->tags as $tag) <span class="badge badge-info">{{ $tag->name }}</span> @endforeach </div> </div> @endforeach </div> </div> 適当な位置に編集ボタンを置きましたがまぁ良いでしょう。編集ボタンを押して動かしてみます。 はい、更新されました。 おわりに かなり雑に走り書きをしましたが、便利さが2ミリ程でも伝われば幸いです。 Laravel-AdminLTE、livewire両方使うとコード量も減らせて入力漏れのバグも減らせてルーティングも減らせたりして うまみがたくさんです。 livewireの日本語記事や参考文献が少なくて手を付けにくいかもしれませんが、割と直ぐに慣れるので是非使ってみてください。 参考文献 livewireドキュメント(マジックアクション) 【JavaScript】event.preventDefault()が何をするのか Laravel-AdminLTE コンポーネントwiki
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む