学習備忘録

よく忘れてしまうのをここにメモしておく

マイグレーションファイルに外部キー制約の付け方

<?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