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

Laravel6.0 で バリデーションをサーバ側とクライアント側(bootstrap4 + jquery validate)で行ってみたメモ

概要

前回は一覧と作成ができることを確認した。
今回は、bootstrap4とjquery validationを使ったフロントのバリデーションも行った。

Form用ライブラリのインストール

bladeでFormを扱うのが楽になるライブラリを導入した。

composer require laravelcollective/html

Laravelでのバリデーション

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.php
    Route::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">&times;</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">&times;</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>{{-- 更新 --}}&nbsp;</th>
                                    <th>{{-- 削除 --}}&nbsp;</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.ts
interface JQuery {
    validate(options: any): JQuery;
}
interface JQueryStatic {
    validator: any;
}

バリデーションの設定

検証失敗/成功時に、bootstrap4のクラスを設定するようにしている。

resources/ts/admin/coupon/index.ts
import { 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 ファサード チートシート

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.8の環境をDockerで構築する(忙しいあなたに)

忙しいあなたにチャチャっとDocker for Laravel

参考

Laravelの環境をDockerで構築するチュートリアル

概要

上で載せたDokcer for Laravelの記事がクソ有能なのでめっちゃ見させてもらってるけど、
パッて使いたい時に説明読むのしんどいので自分用まとめ記事です。

自分用なので大分言葉足りずだと思います。

環境

  • Laravel5.8
  • PHP7.3
  • MySQL57

ディレクトリ構造

appがLaravelディレクトリ

スクリーンショット 2019-11-01 17.13.05.png


docker-compose.yml

  • web:ports:が被らないようにする
  • mysql:ports:が被らないようにする
docker-compose.yml
version: '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

Dockerfile
FROM 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/html

npm使うなら以下を追記

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.conf
server {
    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 bash

Laravel5.8をComposerを使用してインストール

Laravelプロジェクト名を変更する際は最後の app 部分を任意の名前に変更する

composer create-project --prefer-dist laravel/laravel=5.8.* app

Laravel設定

DB_をdocker-compose.ymlで設定したものに変更する

.env
APP_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-01 18.03.00.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel6ををさくらのライトプランにアップしたら500エラーが止まらない

Internal_Server_Error.png

ローカルで作成した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で無限ループしているっぽいので修正。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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以前であればsetUpconfigではなく、以下のコードを挿入でも行けるみたいです(検証してません)

$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として出力されたレコードが取得できます。
これによってテスト時に実行されたログと比較して意図した出力になっているかどうかを確認することがきでます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsのSPAモードでもOGPを動的に生成しSNSシェアに対応する(Laravel)

結論

Nuxt.jsだけではなく、サーバーサイドでなんらかしらの処理が必要

前提条件

参考にこちら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の情報を見てゴニョゴニョする処理が出てくると思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.5 インストール

・Composerインストール

CentOS7にComposerをインストールする
@inakadegaebal

1. Laravel インストール

$composer create-prject "laravel/laravel=5.5.*" --prefer-dist {インストールフォルダ名}

2. .env 編集。

/.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE={データベース名}
DB_USERNAME={ID}
DB_PASSWORD={Password}

3. /storageのパーミッション変更

/
$chmod 777 -R storage

4. DBの191文字超エラー回避

app/Providers/AppServiceProvider.php
class 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む