- 投稿日:2019-11-01T23:05:41+09:00
Laravel6.0 で バリデーションをサーバ側とクライアント側(bootstrap4 + jquery validate)で行ってみたメモ
概要
前回は一覧と作成ができることを確認した。
今回は、bootstrap4とjquery validationを使ったフロントのバリデーションも行った。Form用ライブラリのインストール
bladeでFormを扱うのが楽になるライブラリを導入した。
composer require laravelcollective/htmlLaravelでのバリデーション
migration用ファイルの作成
今回作成するマスタのテーブルを作成するモデルを作成。
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateCouponsTable extends Migration { public function up() { Schema::create('coupons', function (Blueprint $table) { $table->string('id'); $table->integer('type')->default(config('const.Coupons.TYPE_GET', 1))->comment('1:取得, 2:使用'); $table->string('name'); $table->integer('point')->default(0); $table->boolean('is_display')->default(true); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->primary('id'); }); } public function down() { Schema::dropIfExists('coupons'); } }モデルの作成
以下の主キーに関する設定を行わないと、string型のidをHTMLで表示するときに0になる。
app/Models/Coupon.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Coupon extends Model { protected $keyType = 'string'; public $incrementing = false; }ルーティングの設定
routes/web.phpRoute::put('/tag/{tag}','Admin\TagController@update'); + Route::get('/coupon','Admin\CouponController@index'); + Route::post('/coupon','Admin\CouponController@store'); + Route::put('/coupon/{coupon}','Admin\CouponController@update'); + Route::delete('/coupon/{coupon}','Admin\CouponController@destroy');コントローラの設定
indexではページネーションの設定を行っている。
storeではサーバサイドのバリデーションを作成時に行っている。
idがキーなので、一意であることを確認している。
updateでは上記のバリデーションは不要。
チェックボックスのデータはチェックがないときはnullになるので、
チェックがあるときの値と比較を行ってbool値をDBに格納するようにした。app/Http/Controllers/Admin/CouponController.php<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Coupon; use Illuminate\Http\Request; class CouponController extends Controller { public function index() { $coupons = Coupon::orderBy('updated_at', 'DESC')->paginate(config('const.Paginator.PER_PAGE')); return view('admin/coupons', [ 'coupons' => $coupons, 'types' => [ config('const.Coupons.TYPE_GET', 1) => '取得', config('const.Coupons.TYPE_USE', 2) => '使用'] ]); } public function store(Request $request) { // bail:最初のバリデーションに失敗したら、残りのバリデーションルールの判定を停止 $validatedData = $request->validate([ 'id' => 'bail|required|unique:coupons|max:255', 'name' => 'bail|required|max:255', 'point' => 'required|integer', 'type' => 'required|integer' ]); $coupon = new Coupon(); $coupon->id = $request->id; $coupon->name = $request->name; $coupon->point = $request->point; $coupon->type = $request->type; $coupon->is_display = $request->is_display === '1'; $coupon->save(); return redirect('/coupon'); } public function update(Request $request, Coupon $coupon) { $validatedData = $request->validate([ 'name' => 'bail|required|max:255', 'point' => 'required|integer', 'type' => 'required|integer' ]); $coupon->name = $request->name; $coupon->point = $request->point; $coupon->type = $request->type; $coupon->is_display = $request->is_display === '1'; $coupon->save(); return redirect('/coupon'); } public function destroy(Coupon $coupon) { $coupon->delete(); return redirect('/coupon'); } }View
長いので非表示
resources/views/admin/coupon.blade.php@extends('layouts.app') @section('title') クーポン管理 @endsection @section('admin') <li class="nav-item"><a class="nav-link" href="{{ url('/admin') }}">管理者ダッシュボード</span></a></li> @endsection @section('content') <div id="editModal" class="modal fade" tabindex="-1" role="dialog"> <form id="edit-form" action="dummy{{--jsで置き換え--}}" method="POST"> @csrf @method('PUT') <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">編集</h5> <button type="button" class="close" data-dismiss="modal" aria-label="閉じる"> <span aria-hidden="true">×</span> </button> </div>{{-- /.modal-header --}} <div class="modal-body"> {{-- クーポン名 --}} <div class="form-group row"> <label for="edit-coupon-name" class="col-sm-3 col-form-label">クーポン名</label> <div class="col-sm-6"> <input type="text" name="name" id="edit-coupon-name" class="form-control" value="{{ old('coupon') }}" required> <div class="valid-feedback"> 入力済み! </div> </div> </div> {{-- クーポン種別 --}} <div class="form-group row"> <label for="edit-coupon-type" class="col-sm-3 col-form-label">クーポン種別</label> <div class="col-sm-6"> {{Form::select('type', $types, old('coupon')) }} </div> </div> {{-- クーポン値 --}} <div class="form-group row"> <label for="edit-coupon-point" class="col-sm-3 col-form-label">値</label> <div class="col-sm-6"> <input type="number" name="point" id="edit-coupon-point" class="form-control" value="{{ old('coupon', 0) }}" required> </div> </div> {{-- 表示フラグ --}} <div class="form-group row form-check"> <div class="col-sm-6"> {{Form::checkbox('is_display', true, true, ['class' => 'form-check-input', 'id'=>'edit-coupon-is_display'])}} <label for="edit-coupon-is_display" class="form-check-label">表示</label> </div> </div> </div>{{-- /.modal-body --}} <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button> <button type="submit" class="btn btn-primary">変更を保存</button> </div>{{-- /.modal-footer --}} </div>{{-- /.modal-content --}} </div>{{-- /.modal-dialog --}} </form> </div>{{-- /.modal --}} {{-- 削除モーダル--}} <div id="deleteModal" class="modal fade" tabindex="-1" role="dialog"> <form id="delete-form" action="dummy" method="POST"> @csrf @method('DELETE') <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">削除</h5> <button type="button" class="close" data-dismiss="modal" aria-label="閉じる"> <span aria-hidden="true">×</span> </button> </div>{{-- /.modal-header --}} <div class="modal-body"> <mark id="delete-item-name">{{-- 削除アイテム名 --}}</mark>を削除します。よろしいですか? </div>{{-- /.modal-body --}} <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button> <button type="submit" class="btn btn-danger">削除</button> </div>{{-- /.modal-footer --}} </div>{{-- /.modal-content --}} </div>{{-- /.modal-dialog --}} </form> </div>{{-- /.modal --}} {{-- ここからメイン --}} <main class="container"> <div class="col-sm-offset-2 col-sm-8"> <div class="card"> <div class="card-header">新しいクーポン</div> <div class="card-body"> {{-- バリデーションエラーの表示 --}} @include('common.errors') {{-- 新クーポンフォーム --}} <form id="create-form" action="{{ url('coupon')}}" method="POST"> @csrf {{-- クーポンID --}} <div class="form-group row"> <label for="coupon-id" class="col-sm-3 col-form-label" >クーポンID</label> <div class="col-sm-6"> <input type="text" name="id" id="coupon-id" class="form-control" aria-describedby="idHelpBlock" value="{{ old('coupon') }}" required> <small id="idHelpBlock" class="form-text text-muted">IDは半角英数字と-_で、重複しないものを入力してください</small> </div> </div> {{-- クーポン名 --}} <div class="form-group row"> <label for="coupon-name" class="col-sm-3 col-form-label">クーポン名</label> <div class="col-sm-6"> <input type="text" name="name" id="coupon-name" class="form-control" value="{{ old('coupon') }}" required> <div class="valid-feedback"> 入力済み! </div> </div> </div> {{-- クーポン種別 --}} <div class="form-group row"> <label for="coupon-type" class="col-sm-3 col-form-label">クーポン種別</label> <div class="col-sm-6"> {{Form::select('type', $types, old('coupon')) }} </div> </div> {{-- クーポン値 --}} <div class="form-group row"> <label for="coupon-point" class="col-sm-3 col-form-label">値</label> <div class="col-sm-6"> <input type="number" name="point" id="coupon-point" class="form-control" value="{{ old('coupon', 0) }}" required> </div> </div> {{-- 表示フラグ --}} <div class="form-group row form-check"> <div class="col-sm-6"> {{Form::checkbox('is_display', true, true, ['class' => 'form-check-input', 'id'=>'coupon-is_display'])}} <label for="coupon-is_display" class="form-check-label">表示</label> </div> </div> {{-- クーポン追加ボタン --}} <div class="form-group row"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-primary"> <i class="fa fa-btn fa-plus"></i> クーポン追加 </button> </div> </div> </form> </div> </div> @if (count($coupons) > 0) <div class="card"> <div class="card-header">クーポン一覧</div> <div class="card-body"> <table class="table table-striped coupon-table"> {{-- テーブルヘッダ --}} <thead> <tr> <th>ID</th> <th>クーポン</th> <th>値</th> <th>種別</th> <th>表示</th> <th>更新日時</th> <th>{{-- 更新 --}} </th> <th>{{-- 削除 --}} </th> </tr> </thead> {{-- テーブル本体 --}} <tbody> @foreach ($coupons as $coupon) <tr> <td class="table-text"> <div>{{ $coupon->id }}</div> </td> <td class="table-text"> <div>{{ $coupon->name }}</div> </td> <td class="table-text"> <div>{{ $coupon->point }}</div> </td> <td class="table-text"> <div>{{ $types[$coupon->type] }}</div> </td> <td class="table-text"> <div>{{ $coupon->is_display ? '表示' : '隠す' }}</div> </td> <td class="table-text"> <div>{{ $coupon->updated_at->format('Y/m/d H:i:s') }}</div> </td> <td> <button type="button" class="btn" data-toggle="modal" data-target="#editModal" data-action="{{ url('coupon/' . $coupon->id) }}" data-name="{{$coupon->name}}" data-point="{{$coupon->point}}" data-is_display="{{$coupon->is_display}}" data-type="{{$coupon->type}}" > <i class="fa fa-btn fa-edit"></i> </button> </td> <td> <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal" data-action="{{ url('coupon/' . $coupon->id) }}" data-name="{{$coupon->name}}" > <i class="fa fa-btn fa-trash"></i> </button> </td> </tr> @endforeach </tbody> </table> </div> </div> @endif {{ $coupons->links() }} </div> </div> @endsection @section('scripts') <script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.1/dist/jquery.validate.min.js" integrity="sha256-sPB0F50YUDK0otDnsfNHawYmA5M0pjjUf4TvRJkGFrI=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.1/dist/additional-methods.min.js"></script> <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.19.1/localization/messages_ja.js"></script> <script src="{{ mix('js/admin/coupon/index.js') }}"></script> @endsection前回やっていないのは、ページング用のタグをいれたことくらいか。
@endif + {{ $coupons->links() }} </div> </div> @endsectionクライアントサイドのバリデーション
ライブラリの追加
viewに以下を追記。
今回は一意のチェックをjquery-validationのremoteで行うので、jqueryをslimではないものにしている。
TypeError: undefined is not an object (evaluating 'b.apply'). Exception occurred when checking element , check the 'remote' method.のようなエラーがslimだと出てしまう。
また、正規表現でのチェックをpatternで行うので、additional-methodsライブラリも追加している。resouces/views/admin/coupon.blade.php@section('scripts') <script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.1/dist/jquery.validate.min.js" integrity="sha256-sPB0F50YUDK0otDnsfNHawYmA5M0pjjUf4TvRJkGFrI=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.1/dist/additional-methods.min.js"></script> <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.19.1/localization/messages_ja.js"></script> <script src="{{ mix('js/admin/coupon/index.js') }}"></script> @endsectionトランスパイル設定
webpack.mix.js+ mix.ts('resources/ts/admin/coupon/index.ts', 'public/js/admin/coupon').version();typescript用の設定の追加
@types/jquery.validationはバージョンが1.16で止まっており、最新は1.19であったため使わない。
コンパイルを通すために以下の設定をおこなった。tsconfig.json{ "compilerOptions": { "outDir": "./built/", // 省略 + "typeRoots": ["./resources/ts/types", "node_modules/@types"] }, "include": ["resources/ts/**/*"] }resouces/ts/types/index.d.tsinterface JQuery { validate(options: any): JQuery; } interface JQueryStatic { validator: any; }バリデーションの設定
検証失敗/成功時に、bootstrap4のクラスを設定するようにしている。
resources/ts/admin/coupon/index.tsimport { ModalEventHandler } from 'bootstrap'; $.validator.setDefaults({ debug: false, // trueの場合、デバッグモードになりフォームは送信されない onkeyup: false, // 有効の場合はkeyupの度にremoteが走ってしまうため。。 success: null, validClass: 'valid-feedback', errorClass: 'invalid-feedback', errorElement: 'span', errorPlacement: function(error: JQuery, element: JQuery) { error.addClass('invalid-feedback'); element.closest('.form-group').append(error); }, highlight: function(element: HTMLElement, errorClass: string, validClass: string) { $(element).addClass('is-invalid'); }, unhighlight: function(element: HTMLElement, errorClass: string, validClass: string) { $(element).removeClass('is-invalid'); } }); const $createForm: JQuery<HTMLFormElement> = $('#create-form'); $createForm .submit(event => { // bootstrap4のカスタムバリデーション const form = event.target; if (form.checkValidity() === false) { event.preventDefault(); event.stopPropagation(); } form.classList.add('was-validated'); }) .validate({ rules: { id: { required: true, remote: '/api/coupon/unique', pattern: '[a-zA-Z0-9_-]+' // patternを使うにはadditonalの読み込みが必要 }, name: { required: true }, point: { required: true, number: true } }, messages: { id: { remote: '既に使われているIDです', pattern: '半角英数字と-_を使用できます' } } }); // 編集 $('#editModal').on('show.bs.modal', function(event: ModalEventHandler<HTMLElement>) { const target = event.relatedTarget; if (target === undefined) { return; } const $button = $(target); // モーダル切替えボタン const action = $button.data('action'); // data-* 属性から情報を抽出 const name = $button.data('name'); const point = $button.data('point'); const type = $button.data('type'); const is_display = $button.data('is_display') === 1; const $modal = $(this); $modal.find('#edit-coupon-name').val(name); $modal.find('#edit-coupon-type').val(type); $modal.find('#edit-coupon-point').val(point); $modal.find('#edit-coupon-is_display').prop('checked', is_display); $modal.find('#edit-form').attr('action', action); }); // HTML5標準のエラーメッセージのカスタマイズ $('#edit-coupon-name').on('invalid', e => { const nameInput = e.target as HTMLInputElement; if (nameInput.value === '') { nameInput.setCustomValidity('名前を入力してください。'); } }); // 削除 $('#deleteModal').on('show.bs.modal', function(event: ModalEventHandler<HTMLElement>) { const target = event.relatedTarget; if (target === undefined) { return; } const $button = $(target); // モーダル切替えボタン const action = $button.data('action'); // data-* 属性から情報を抽出 const name = $button.data('name'); const $modal = $(this); $modal.find('#delete-item-name').text(name); $modal.find('#delete-form').attr('action', action); });サーバ側でのチェック
jquery.validateのremoteで呼び出され、true/falseを返すAPIを作る。
バリデーションに失敗したらfalseを帰すようにする。ルーティング設定
routes/api.php+ Route::get('/coupon/unique','Actions\CouponAction@unique');コントローラ
app/Http/Controllers/Actions/CouponAction.php<?php namespace App\Http\Controllers\Actions; use App\Http\Controllers\Controller; use App\Models\Coupon; use Illuminate\Http\Request; class CouponAction extends Controller { /** * 存在していなければtrue。存在していればfalseを返す */ public function unique(Request $request) { $result = true; if ($request->has('id')) { $result = ! Coupon::where('id', '=', $request->query('id'))->exists(); } return response()->json($result); } }参考
Laravel 6.0 バリデーション
jQuery Validation
jQuery Validation Plugin が使いやすくておすすめ
jsdelivr
Laravel 6.0 Blade テンプレート
Laravel のフロントエンドに TypeScript を導入する
Laravel mix の Vue.js を TypeScript にしていく
jquery-validation
【Laravel】Model を save すると、そのインスタンスの id が 0 になることがある
'Bootsrap 4.0.0 stable' causes 'Uncaught TypeError: Cannot read property 'apply' of undefined.'
jQuery Validation サーバーに通信しての値の検証(remote)
jquery.validate.js のエラーメッセージ表示制御にハマる
jQuery Validation を Twitter Bootstrap と組み合わせて使う
validate and bootstrap4
BootStrap4 でフロントエンド完結の password 一致のバリデーションを実現する
novalidate を付与する。html5 検証と validate は同時に使用できない。
大量の Input タグがあるページで jquery validation の Submit がどえらい遅いのは options.success のせいだったかもしれない。
jQuery Validate Plugin の解説と Validate 日本語環境用 Plugin と jQuery Form Plugin との連携
mdn:input
チェックボックスを作成する
php:date
jQuery Validation Plugin を使用した時、フォームが検証済みになった時にボタンを有効にする
LaravelCollective Form ファサード チートシート
- 投稿日:2019-11-01T18:03:19+09:00
Laravel5.8の環境をDockerで構築する(忙しいあなたに)
忙しいあなたにチャチャっとDocker for Laravel
参考
概要
上で載せたDokcer for Laravelの記事がクソ有能なのでめっちゃ見させてもらってるけど、
パッて使いたい時に説明読むのしんどいので自分用まとめ記事です。自分用なので大分言葉足りずだと思います。
環境
- Laravel5.8
- PHP7.3
- MySQL57
ディレクトリ構造
appがLaravelディレクトリ
docker-compose.yml
web:のports:が被らないようにするmysql:のports:が被らないようにするdocker-compose.ymlversion: '3' services: web: image: nginx:1.15.6 ports: - "8030:80" depends_on: - app volumes: - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf - .:/var/www/html app: build: ./docker/php depends_on: - mysql volumes: - .:/var/www/html mysql: image: mysql:5.7 environment: MYSQL_DATABASE: default MYSQL_USER: default MYSQL_PASSWORD: secret MYSQL_ROOT_PASSWORD: secret ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql volumes: mysql-data:
Dockerfile
DockerfileFROM php:7.3-fpm # install composer RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer RUN apt-get update \ && apt-get install -y \ git \ zip \ unzip \ vim RUN apt-get update \ && apt-get install -y libpq-dev \ && docker-php-ext-install pdo_mysql pdo_pgsql WORKDIR /var/www/htmlnpm使うなら以下を追記
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - RUN apt-get install -y nodejs RUN npm install npm@latest -g
default.conf
default.confserver { listen 80; root /var/www/html/app/public; index index.php index.html index.htm; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } }
実行
docker-compose up -d何かを変更したら
docker-compose restart
Laravelインストール
上記の手順を全て終えたら仮想環境に入る
docker-compose exec app bashLaravel5.8をComposerを使用してインストール
Laravelプロジェクト名を変更する際は最後の
app部分を任意の名前に変更するcomposer create-project --prefer-dist laravel/laravel=5.8.* appLaravel設定
DB_をdocker-compose.ymlで設定したものに変更する
.envAPP_NAME=Laravel APP_ENV=local APP_KEY=base64:KjL3igybtFcsPzAjU5B6WFXzMcuA3O8N5S4nA+pdGyg= APP_DEBUG=true APP_URL=http://localhost LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=default DB_USERNAME=default DB_PASSWORD=secret BROADCAST_DRIVER=log CACHE_DRIVER=file QUEUE_CONNECTION=sync SESSION_DRIVER=file SESSION_LIFETIME=120 REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"完了
- 投稿日:2019-11-01T17:30:56+09:00
Laravel6ををさくらのライトプランにアップしたら500エラーが止まらない
ローカルで作成したLaravelのプロジェクトをさくらのライトプランのサーバにアップしたらInternal Server Errorが出た。
まず解決法
.htaccessが原因。
RewriteBase /を追記すると直った。public/.htaccess<IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews -Indexes </IfModule> RewriteEngine On RewriteBase / # ←これを追加 # Handle Authorization Header RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] # Redirect Trailing Slashes If Not A Folder... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} (.+)/$ RewriteRule ^ %1 [L,R=301] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] </IfModule>調べ方
・トップページは表示できて、それ以外のページが見られない。cssなどの存在するファイルは表示できる。
→ .htaccessが原因かなぁとあたりをつける。・.htaccessを削除してファイルの存在しないURLにアクセスすると500エラーが404エラーに。
→ .htaccessでほぼ確定。・さくらのコントロールパネル > アプリケーションの設定 > アクセスログの設定 > エラーログ からエラーを確認。
AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary. Use 'LogLevel debug' to get a backtrace.→ Rerwiteで無限ループしているっぽいので修正。
- 投稿日:2019-11-01T12:27:16+09:00
LaravelでMonologの出力結果をテストする
この記事ではLaravelを利用している場合のユニットテストで、Monologの出力結果をテストする方法を紹介します。
テストする対象のクラスは以下のような感じです。class Log { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function exec(): void { $this->logger->info('sample log'); } }この
logger->infoで適切文言が出力されていることを確認するユニットテストを書きます。class LoggingTest extends TestCase { private $log; public function setUp(): void { parent::setUp(); config([ 'logging' => [ 'channels' => [ 'test' => [ 'driver' => 'custom', 'via' => function () { $handler = new TestHandler(); return new Logger('test', [$handler]); }, ], ], ], ]); $this->log = new Log($this->app->make(LoggerInterface::class)); } /** * @test * / public function logging() { $this->log->exec(); $records = $this->app->make(LoggerInterface::class) ->getHandlers()[0] ->getRecords(); $this->assertCount(1, $records); $this->assertEquals( 'sample log', $records[0]['message'] ); } }phpunit.xmlに以下を追加
<php> <env name="LOG_CHANNEL" value="test"/> </php>また、Laravel5.6以前であれば
setUpにconfigではなく、以下のコードを挿入でも行けるみたいです(検証してません)$this->app->configureMonologUsing(function ($monolog) { $monolog->pushHandler(new \Monolog\Handler\TestHandler()); });解説
$records = $this->app->make(LoggerInterface::class) ->getHandlers()[0] ->getRecords();この
getHandlersでは登録されているHandlerが配列で取得でき、今回はchannels.testは一つのHandlerしか登録していないので配列の先頭でTestHandlerが取得できます。
TestHandlerにはgetRecordsメソッドからlogとして出力されたレコードが取得できます。
これによってテスト時に実行されたログと比較して意図した出力になっているかどうかを確認することがきでます。
- 投稿日:2019-11-01T12:24:50+09:00
LaravelでMySQLのテーブルロックを実行する
実装例
占有ロック
DB::raw('LOCK TABLES lock_target_table WRITE');共有ロック
DB::raw('LOCK TABLES lock_target_table READ');Eloquentのクエリビルダを使う方法は見つけられなかったので、生クエリを実行している
ダメな例
HogeHogeModel::lockForUpdate()->get();全レコードに対して
lockForUpdate()しても、テーブルロックの挙動にならなかった。
(別のトランザクションから、まだ存在しないレコードに対して占有ロック(ギャップロック )をかけることができてしまい、その後デッドロックになるリスクがある)検証
全行ロックだと存在しない行のロックがブロックされない例
T1 T2 コメント BEGIN; BEGIN; SELECT * FROM locks FOR UPDATE; 行全部ロック SELECT * FROM locks WHERE name="not_exist_record" FOR UPDATE; 存在しない行のロックが取れてしまう >Empty set テーブルロックで存在しない行のロックがうまくブロックされる例
T1 T2 コメント BEGIN; BEGIN; LOCK TABLES locks WRITE; テーブルロック SELECT * FROM locks WHERE name="not_exist_record" FOR UPDATE; 存在しない行のロック ... T1のテーブルロックが終わるまで待つ COMMIT; >Empty set T1のテーブルロックが終わったので実行される Versions
- Laravel 5.8
- MySQL 5.7
- 投稿日:2019-11-01T10:58:48+09:00
Nuxt.jsのSPAモードでもOGPを動的に生成しSNSシェアに対応する(Laravel)
結論
Nuxt.jsだけではなく、サーバーサイドでなんらかしらの処理が必要
前提条件
- nuxt-laravelの環境構築がすでに完了している
参考にこちらNuxt.js2.10とLaravel(6.0)を同一ディレクトリで動かすnuxt-laravelをDockerで環境構築
実装例
nuxt-laravelを構築していれば、Nuxt.jsでもLaravelの機能を使える。
そこでLaravelのMiddlewareを使って、OGPを動的に生成する。今回はTwitterBotにのみOGPを動的に生成してみる。
php artisan make:middleware SetOgpToTwitterBot// nuxt-laravel/app/Http/Middleware/SetOgpToTwitterBot.php <?php namespace App\Http\Middleware; use Closure; class SetOgpToTwitterBot { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $userAgent = $request->header('User-Agent'); if (strpos($userAgent, 'Twitterbot') !== false) { print '<meta name="twitter:description" content="TwitterOnly">'; print '<meta name="twitter:title" content="ForTwitter">'; } return $next($request); } }// nuxt-laravel/app/Http/Kernel.php /** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\SetOgpToTwitterBot::class // 追加 ],こんな感じでTwitterBotが来たときだけOGPを生成することが可能。
実際にはURLからJSONのAPIの情報を見てゴニョゴニョする処理が出てくると思う。
- 投稿日:2019-11-01T09:08:45+09:00
Laravel5.5 インストール
・Composerインストール
CentOS7にComposerをインストールする
@inakadegaebal1. Laravel インストール
$composer create-prject "laravel/laravel=5.5.*" --prefer-dist {インストールフォルダ名}2. .env 編集。
/.envDB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE={データベース名} DB_USERNAME={ID} DB_PASSWORD={Password}3. /storageのパーミッション変更
/$chmod 777 -R storage4. DBの191文字超エラー回避
app/Providers/AppServiceProvider.phpclass AppServiceProvider extends ServiceProvider { public function boot() { //追記 \Illuminate\Support\Facades\Schema::defaultStringLength(191); } ...5. publicを {アプリドメイン}/ でアクセス
Php laravel 5.5 project .htaccess file
/.htaccess//ルートディレクトリに配置 <IfModule mod_rewrite.c> <IfModule mod_negotiation.c> Options -MultiViews </IfModule> RewriteEngine On RewriteCond %{REQUEST_FILENAME} -d [OR] RewriteCond %{REQUEST_FILENAME} -f RewriteRule ^ ^$1 [N] RewriteCond %{REQUEST_URI} (\.\w+$) [NC] RewriteRule ^(.*)$ public/$1 RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ server.php </IfModule>おまけ
git初期設定$git init $git add . $git commit -m 'start' //Git-Hub マイリポジトリから URL をコピーしておく $git remote add origin '{URL}' $git push -u origin master


