Carbonのformatをyyyy-mm-dd hh:mm:ss表示にしたい
created_atに登録するときは基本carbonでformatするがいつも、formatの仕方を忘れるのでここにメモしておく。
<?php $now = Carbon::now()->format('Y-m-d H:i:s');
トランザクションの貼り方
<?php use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; try { DB::beginTransaction(); $item->bulkInsertOrUpdate($this->items); DB::commit(); } catch (Exception $e) { DB::rollback(); Log::error($e->getMessage()); }
マイグレーションファイルに外部キー制約の付け方
<?php public function up() { Schema::create('sales', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('item_id'); $table->bigInteger('quantity'); $table->bigInteger('amount'); $table->foreign('user_id') ->references('id') ->on('users') ->onDelete('cascade'); $table->foreign('item_id') ->references('id') ->on('items') ->onDelete('cascade'); $table->timestamps(); }); }
バルクインサートをする方法
課題
- DBにデータを登録する際に1件ごとにやっていると時間がかかるため、バルクインサートをしたい
- データがない場合にはcreate、すでに存在している場合はupdateするようにしたい。
対処
- 任意の場所に
BulkInsertBuilder.php
ファイルを以下のように作成する。
<?php namespace App; use Illuminate\Database\Query\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use InvalidArgumentException; class BulkInsertBuilder extends Builder { const DUPLICATE = ' ON DUPLICATE KEY UPDATE '; const UPDATED_AT = 'updated_at'; const EQUALS = ' = '; const NOW = 'NOW()'; public function __construct(Builder $builder) { $this->from = $builder->from; parent::__construct($builder->getConnection()); } /** * インスタンス生成 * * @param Builder $builder * @return self */ public static function build(Builder $builder): self { return app()->makeWith(self::class, ['builder' => $builder]); } /** * バルクインサート処理 * key制約発生時はupdateする * * @param array|Collection $values * @param array $update_values * @return void */ public function execute($values, array $update_columns): void { $values = $this->assembleValues($values); $update_columns = $this->generateUpdateColumns($update_columns); $query = $this->assembleBulkInsertQuery($values, $update_columns); // Execute the created SQL $this->connection->insert($query, $this->cleanBindings(Arr::flatten($values, 1))); } /** * もしバルクインサートメソッドに渡す引数が配列かコレクション以外になっていた場合、異常終了させる * * @param mixed $values * @throws InvalidArgumentException * @return void */ public function validateValues($values): void { if ($this->isCollectionOrModel($values) || is_array($values)) { return; } else { throw new InvalidArgumentException( 'Invalid argument given. BulkInsertBuilder expects only types of Array or Collection.' ); } } /** * 引数がモデルまたはコレクションかどうか判定 * * @param mixed $v * @return bool */ private function isCollectionOrModel($v): bool { return $v instanceof Collection || get_parent_class($v) === AbstractModelBase::class || get_parent_class($v) === Model::class; } /** * valuesをクエリ生成用に加工する * * @param array|Collection $v * @return array */ private function assembleValues($v): array { $v = $this->convertValuesIntoArray($v); return $this->sortValuesOrCastSingleValueToArray($v); } /** * もしコレクションかモデルインスタンスが引数に渡されていたら、インスタンスを配列にキャストする * また、2次元配列内にstdClassが格納されていた場合、全て配列にキャストする * * @param array|Collection|Model $v * @return array */ private function convertValuesIntoArray($v): array { $v = $this->isCollectionOrModel($v) ? $v->toArray() : $v; return $this->castStdClassToArrayIfGiven($v); } /** * 2次元配列内に格納されている値がstdClassだった場合、配列にキャストする * * @param array $values * @return array */ private function castStdClassToArrayIfGiven(array $values): array { foreach ($values as $key => $value) { if (is_object($value)) { $values[$key] = (array) $value; } } return $values; } /** * Here, we will sort the insert keys for every record so that each insert is * in the same order for the record. We need to make sure this is the case * so there are not any errors or problems when inserting these records. * * @param array $values * @return array */ private function sortValuesOrCastSingleValueToArray(array $values): array { if (!is_array(reset($values))) { $values = [$values]; } else { foreach ($values as $key => $value) { ksort($value); $values[$key] = $value; } } return $values; } /** * updateするカラムを保持する配列を生成する * * @param array $columns * @return Collection */ private function generateUpdateColumns(array $columns): Collection { return collect($columns)->mapWithKeys(function ($column) { // updated_atのみ、更新された時刻を入れるようにする return $column === self::UPDATED_AT ? [$column => DB::raw(self::NOW)] : [$column => DB::raw("VALUES($column)")]; }); } /** * Generate an ordinal INSERT statement and following ON DUPLICATE... * clause then join two SQL parts together. * * @param array $values * @param Collection $update_columns * @return string */ private function assembleBulkInsertQuery(array $values, Collection $update_columns): string { $query = $this->grammar->compileInsert($this, $values); return $query .= self::DUPLICATE . collect($update_columns)->map(function ($value, $key) { return $this->grammar->wrap($key) . self::EQUALS . $this->grammar->parameter($value); })->implode(', '); } }
- また任意の場所に
BulkInsertOrUpdateTrait.php
を以下のように作成する。namespaceは合わせるように。
<?php namespace App; use App\BulkInsertBuilder; use Illuminate\Support\Collection; trait BulkInsertOrUpdateTrait { /** * バルクインサートビルダーのインスタンスを保持する * 1回目のインスタンス生成時にこのプロパティに格納される * * @var BulkInsertBuilder|null */ private $bulk_insert_builder = null; /** * key制約が発生したときの、updateするカラムのデフォルト指定 * * @return array * * ex: return ['foo', 'bar', 'updated_at']; */ abstract protected function getUpdateColumnsOnDuplicate(): array; /** * レコードをバルクインサートする * もし重複キー制約が発生した場合、指定したカラムの値のみアップデートする * * @param array|Collection $values * @param array|null $update_columns * @return void */ public function bulkInsertOrUpdate($values, ?array $update_columns = null): void { // インスタンス生成. すでにあれば使い回す $this->bulk_insert_builder = $this->bulk_insert_builder ?? BulkInsertBuilder::build($this->query()->getQuery()); // もし配列、モデル、コレクション以外のvaluesが渡されていたら、例外を投げて終了させる $this->bulk_insert_builder->validateValues($values); // カラムが明示的に渡された場合はそのカラムでupdateする $update_columns = $update_columns ?? $this->getUpdateColumnsOnDuplicate(); $this->bulk_insert_builder->execute($values, $update_columns); } }
- バルクインサートしたモデルに
BulkInsertBuilder.php
をuseする。以下の例ではItemモデル - また、ユニークキーが重複していた場に更新するカラムを
getUpdateColumnsOnDuplicate
メソッドの中で既定する。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Item extends Model { use BulkInsertOrUpdateTrait; protected function getUpdateColumnsOnDuplicate(): array { return [ 'amount', 'updated_at' ]; } public function cartItems() { return $this->hasMany('App\CartItem'); } }
- 最後に実際にバルクインサートすると気はこんな感じでやる。
bulkInsertOrUpdate
メソッドに配列を渡してあげる。
<?php try { DB::beginTransaction(); $item->bulkInsertOrUpdate($this->items); DB::commit(); } catch (Exception $e) { DB::rollback(); Log::error($e->getMessage()); }
ミドルウェアについて
Laravelのミドルウェアに関して、よく迷いそうな点をメモしておく
標準で組み込まれているミドルウェアを調べたいとき
App\Http\Kernel.php
に色々書かれてる。 例えばmiddleware('auth')のauthについて処理を追いたい時は
<?php protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, //ここを見れば分かる 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ];
ミドルウェアの読み方
- 基本
handle
メソッドの中身が実装される。 - 引数は$requestと$nextである。
- たまに
middleware('auth:user')
といった書き方があるが、その場合コロンの後のuserはhadnleメソッドの第三引数となる。 例えば以下の場合 userが$guardに渡される。
<?php public function handle($request, Closure $next, ...$guards) { $this->authenticate($request, $guards); return $next($request); }
バリデーション以外で$errors内容を表示する方法
課題
- requestのvalidationに引っかかった際は$errorsに勝手に値が入るが、それ以外でエラーになったときは独自でerrorsに値を入れてリダイレクトさせなきゃいけない。
対処
以下のコードを参考に。withErrors()
の引数が$errorsに入る。またwithInput()
で元々の入力値がsessionのoldに入る。
<?php public function index(){ try { // 略 } catch { $errors = $this->getAuthyErrors($verification->errors()); return redirect()->back()->withErrors(new MessageBag($errors))->withInput(); } } private function getAuthyErrors($authyErrors) { $errors = []; foreach ($authyErrors as $field => $message) { array_push($errors, $field . ': ' . $message); } return $errors; }
- blade側ではこのようはtempleを作成し、埋め込んで上げておくと良い.
<?php @if ($errors->any()) <div class="card-text text-left alert alert-danger"> <ul class="mb-0"> @foreach($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif