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

Laravel マイグレーション デフォルト値の設定 簡易版

目的

  • マイグレーションファイルでのデフォルト値の設定方法を簡易的にまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

記載例

  • 下記の様に記載することで当該カラムのデフォルト値を指定することができる。

    $table->データ型('カラム名', オプション)->default(デフォルト値に設定する文字列や値);
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

laravel queueではまった。

laravelの非同期処理を実現するためqueue(jobテーブル)機能にて実装。
supervisorにてphp artisan queue:workを定期実行。

しばらく運用していたが、failed_jobsテーブルにログが出るように。

ErrorException: include(/var/www/html/vendor/composer/../egulias/email-validator/EmailValidator/EmailValidator.php): failed to open stream: No such file or directory in /var/www/html/vendor/composer/ClassLoader.php:444

あれこれ調べても分らないのでcomposerのクリアや
composer dump-autoloadしてもダメだったが
supervisorのデーモンを再起動で直った。

よくわからないけど
supervisorの子プロセスはphpのclassマップのリロードしないのかもしれない。

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

【Laravel7でユーザー認証_10】マルチ認証機能で作成した管理者のパスワード変更・リセット機能を追加する

はじめに

Laravelのユーザー認証機能に、管理者権限を追加(マルチ認証機能)した後、管理者のパスワードを変更・リセットする機能を追加する手順をまとめます。

環境

XAMPP環境でLaravelが使えるように設定してあります。

  • Windows10 Pro 64bit
  • PHP 7.3.18
  • Laravel 7.12.0
  • MariaDB 10.1.32

また、Laravelプロジェクトは以下の手順で作業を進めており、管理者はすでに作成されている状態です。

パスワード変更 実装手順

コントローラの作成

パスワード変更用のコントローラを作成します。
今回は、ユーザー用に作成した Auth/ChangePasswordControllerAdmin/Auth ディレクトリの中にコピーして利用します。

namespaceをAdmin用に書き換えます。
また、パスワード変更の処理はadminログインを必須としたいので、コンストラクトでチェックするように指定します。

app/Http/Controllers/Admin/Auth/ChangePasswordController.php
  <?php

- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use App\Http\Requests\ChangePasswordRequest;
  use Illuminate\Support\Facades\Auth;

  class ChangePasswordController extends Controller
  {
      public function __construct()
      {
-         $this->middleware('auth');
+         $this->middleware('auth:admin');
          $this->middleware('verified');
      }

      public function showChangePasswordForm()
      {
          return view('auth\passwords\change');
      }

      public function changePassword(ChangePasswordRequest $request)
      {
          //ValidationはChangePasswordRequestで処理
          //パスワード変更処理
          $user = Auth::user();
          $user->password = bcrypt($request->get('password'));
          $user->save();

          //homeにリダイレクト
          return redirect()->route('home')->with('status', __('Your password has been changed.'));
    }
  }

ルーティングの設定

以下の仕様で、ルーティングを設定します。

通常のアクセス(GET)の場合は、「Admin/Auth/ChangePasswordController」コントローラの「showChangePasswordForm」メソッドを実行。
パスワードを変更の処理(POST)の場合は、「Admin/Auth/ChangePasswordController」コントローラの「changePassword」メソッドを実行。
それぞれのルーティングには、「admin.password.form」、「admin.password.change」という名前を付けました。

/routes/web.php
  Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function() {
      Route::get('home', 'HomeController@index')->name('home');

      Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
      Route::post('login', 'Auth\LoginController@login');

      Route::post('logout', 'Auth\LoginController@logout')->name('logout');
+
+     Route::get('/setting/password', 'Auth\ChangePasswordController@showChangePasswordForm')->name('password.form');
+     Route::post('/setting/password', 'Auth\ChangePasswordController@changePassword')->name('password.change');
  });

viewの作成

viewは、auth/passwords/change.blade.php をコピーして利用します。
formのアクション先は、ルーティングで付けた「admin.password.change」という名前を使います。

resources/views/admin/auth/passwords/change.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Change Password') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.password.change') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="current_password" class="col-md-4 col-form-label text-md-right">{{ __('Current Password') }}</label>

                            <div class="col-md-6">
                                <input id="current_password" type="password" class="form-control @error('current_password') is-invalid @enderror" name="current_password" required autocomplete="new_password">

                                @error('current_password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('New Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm New Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new_password">
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Change Password') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

viewを指定

コントローラの showChangePasswordForm メソッドで、作成したviewを指定します。

app/Http/Controllers/Admin/Auth/ChangePasswordController.php
      public function showChangePasswordForm()
      {
-         return view('auth\passwords\change');
+         return view('admin\auth\passwords\change');
      }

パスワード変更の処理

バリデーションチェック

FormRequest の設定

ユーザーのFormRequest app/Http/Requests/ChangePasswordRequest.phpapp/Http/Requests/Admin/ChangePasswordRequest.php に複製します。

app/Http/Request/Admin/ChangePasswordRequest.php
  <?php

- namespace App\Http\Requests;
+ namespace App\Http\Requests\Admin;

  use Illuminate\Foundation\Http\FormRequest;
  use Illuminate\Contracts\Validation\Validator;
  use Illuminate\Http\Exceptions\HttpResponseException;
  use Illuminate\Support\Facades\Auth;
  use Illuminate\Support\Facades\Hash;

  class ChangePasswordRequest extends FormRequest
  {
      /**
       * Determine if the user is authorized to make this request.
       *
       * @return bool
       */
      public function authorize()
      {
          return true;
      }

      /**
       * Get the validation rules that apply to the request.
       *
       * @return array
       */
      public function rules()
      {
          return [
              'current_password' => ['required', 'string', 'min:8'],
              'password' => ['required', 'string', 'min:8', 'confirmed']
          ];
      }

      public function withValidator(Validator $validator) {
           $validator->after(function ($validator) {
               $auth = Auth::user();

               //現在のパスワードと新しいパスワードが合わなければエラー
               if (!(Hash::check($this->input('current_password'), $auth->password))) {
                   $validator->errors()->add('current_password', __('The current password is incorrect.'));
               }
           });
       }
  }

コントローラに読み込み

FormRequest をコントローラから読み込むように修正します。
また、パスワード変更時のリダイレクト先が管理者のhomeになるよう変更します。

app/Http/Controllers/Admin/Auth/ChangePasswordController.php
  <?php

  namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
- use App\Http\Requests\ChangePasswordRequest;
+ use App\Http\Requests\Admin\ChangePasswordRequest;
  use Illuminate\Support\Facades\Auth;

===(中略)===

      public function changePassword(ChangePasswordRequest $request)
      {
          //ValidationはChangePasswordRequestで処理
          //パスワード変更処理
          $user = Auth::user();
          $user->password = bcrypt($request->get('password'));
          $user->save();

          //homeにリダイレクト
-         return redirect()->route('home')->with('status', __('Your password has been changed.'));
+         return redirect()->route('admin.home')->with('status', __('Your password has been changed.'));
    }
  }

ヘッダからリンクする

ログインした後、右上に表示されるメニューの中に、パスワード変更のリンクを張っておきます。

resource/views/layouts/app.blade.php
                                  <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                      <a class="dropdown-item" href="{{ route('admin.logout') }}"
                                         onclick="event.preventDefault();
                                                       document.getElementById('logout-form').submit();">
                                          {{ __('Logout') }}
                                      </a>

                                      <form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;">
                                          @csrf
                                      </form>
+
+                                     <a class="dropdown-item" href="{{ route('admin.password.form') }}">
+                                         {{ __('Change Password') }}
+                                     </a>
                                  </div>

動作確認

管理者のパスワード変更画面にアクセスして、希望の動作になるか確認します。
前回、secretという8桁以下のパスワードで設定したため、バリデーションがとおらなくなってしまいました。
tinkerを使ってDBを操作して、まずは直接パスワードを変更することにします。

$ php artisan tinker
$ \DB::table('admins')->where('id', 1)->update(['password' => \Hash::make('secretpassword')]);

これでパスワードが「secretpassword」に変更されました。
現在のパスワード欄に「secretpassword」、新しいパスワード欄に任意の文字列を入力して動作を確認します。


パスワードリセット 実装手順

ルーティングの定義

管理者のパスワードリセットについて、Auth::routes() を使うと、認証系に必要な定義を自動で追加してくれます。
管理者用は登録部分は利用しないことにするので、オプションで以下のように指定します。

routes/web.php
  Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function() {
      Route::get('home', 'HomeController@index')->name('home');

      Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
      Route::post('login', 'Auth\LoginController@login');

      Route::post('logout', 'Auth\LoginController@logout')->name('logout');

      Route::get('/setting/password', 'Auth\ChangePasswordController@showChangePasswordForm')->name('password.form');
      Route::post('/setting/password', 'Auth\ChangePasswordController@changePassword')->name('password.change');
+
+     Auth::routes([
+         'register' => false,
+         'reset'    => true,
+         'verify'   => false
+     ]);
  });

php artisan route:list をしてみると、admin.password.email や admin.password.update といったパスワードに関するルーティングが追加されているのが分かります。
Laravel-admin-resetpassword-01.fw.png

このルーティングに沿って、コントローラを作成していきます。

コントローラの作成

ユーザー用のパスワードリセット用のコントローラ app/Http/Controllers/Auth/ForgotPasswordController.phpapp/Http/Controllers/Auth/ResetPasswordController.php を、管理者用として app/Http/Controllers/Admin/Auth/ 以下にコピーします。

コピー後、namespaceやリダイレクト先を管理者向けに変更します。

app/Http/Controllers/Admin/Auth/ForgotPasswordController.php
  <?php

- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

  class ForgotPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset emails and
      | includes a trait which assists in sending these notifications from
      | your application to your users. Feel free to explore this trait.
      |
      */

      use SendsPasswordResetEmails;
  }
app/Http/Controllers/Admin/Auth/ResetPasswordController.php
  <?php

- namespace App\Http\Controllers\Auth;
+ namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use App\Providers\RouteServiceProvider;
  use Illuminate\Foundation\Auth\ResetsPasswords;

  class ResetPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset requests
      | and uses a simple trait to include this behavior. You're free to
      | explore this trait and override any methods you wish to tweak.
      |
      */

      use ResetsPasswords;

      /**
       * Where to redirect users after resetting their password.
       *
       * @var string
       */
-     protected $redirectTo = RouteServiceProvider::HOME;
+     protected $redirectTo = RouteServiceProvider::ADMIN_HOME;
  }

viewの作成

ユーザー用のパスワードリセット用のview resources/views/auth/passwords/reset.blade.phpresources/views/auth/passwords/email.blade.php を、管理者用として app/Http/Controllers/Admin/Auth/ 以下にコピーします。
コピー後、レイアウトやformの送信先を変更します。

resources/views/auth/passwords/reset.blade.php
- @extends('layouts.app')
+ @extends('layouts.admin.app')

  @section('content')
  <div class="container">
      <div class="row justify-content-center">
          <div class="col-md-8">
              <div class="card">
                  <div class="card-header">{{ __('Reset Password') }}</div>

                  <div class="card-body">
-                     <form method="POST" action="{{ route('password.update') }}">
+                     <form method="POST" action="{{ route('admin.password.update') }}">
                          @csrf

                          <input type="hidden" name="token" value="{{ $token }}">

                          <div class="form-group row">
                              <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                               <div class="col-md-6">
                                  <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus>

                                  @error('email')
                                      <span class="invalid-feedback" role="alert">
                                          <strong>{{ $message }}</strong>
                                      </span>
                                  @enderror
                              </div>
                          </div>

                          <div class="form-group row">
                              <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                              <div class="col-md-6">
                                  <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

                                  @error('password')
                                      <span class="invalid-feedback" role="alert">
                                          <strong>{{ $message }}</strong>
                                      </span>
                                  @enderror
                              </div>
                          </div>

                          <div class="form-group row">
                              <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                              <div class="col-md-6">
                                  <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                              </div>
                          </div>

                          <div class="form-group row mb-0">
                              <div class="col-md-6 offset-md-4">
                                  <button type="submit" class="btn btn-primary">
                                      {{ __('Reset Password') }}
                                  </button>
                              </div>
                          </div>
                      </form>
                  </div>
              </div>
          </div>
      </div>
  </div>
  @endsection
resources/views/auth/passwords/email.blade.php
- @extends('layouts.app')
+ @extends('layouts.admin.app')

  @section('content')
  <div class="container">
      <div class="row justify-content-center">
          <div class="col-md-8">
              <div class="card">
                  <div class="card-header">{{ __('Reset Password') }}</div>

                  <div class="card-body">
                      @if (session('status'))
                          <div class="alert alert-success" role="alert">
                              {{ session('status') }}
                          </div>
                      @endif
+
-                     <form method="POST" action="{{ route('password.email') }}">
+                     <form method="POST" action="{{ route('admin.password.email') }}">
                          @csrf

                          <div class="form-group row">
                              <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                              <div class="col-md-6">
                                  <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                                  @error('email')
                                      <span class="invalid-feedback" role="alert">
                                          <strong>{{ $message }}</strong>
                                      </span>
                                  @enderror
                              </div>
                          </div>

                          <div class="form-group row mb-0">
                              <div class="col-md-6 offset-md-4">
                                  <button type="submit" class="btn btn-primary">
                                      {{ __('Send Password Reset Link') }}
                                  </button>
                              </div>
                          </div>
                      </form>
                  </div>
              </div>
          </div>
      </div>
  </div>
  @endsection

管理者用パスワードリセットトークン保存テーブル作成

ユーザー用のパスワードリセットトークン保存テーブル password_resets と、管理者用のものを分けるため、admin_password_resets というテーブルを新たに作成します。

php artisan make:migration create_admin_password_resets_table 

日付_create_admin_password_resets_table.php の中身は、 日付_create_password_resets_table.php を参考にします。

database/migrations/日付_create_admin_password_resets_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateAdminPasswordResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admin_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admin_password_resets');
    }
}
php artisan migrate

パスワードブローカーの設定

管理者がパスワードリセット時に admin_password_resets テーブルを使用するため、config/auth.php で、管理者用のパスワードブローカーを設定します。

config/auth.php
      'passwords' => [
          'users' => [
              'provider' => 'users',
              'table' => 'password_resets',
              'expire' => 60,
              'throttle' => 60,
        'admins' => [
            'provider' => 'admins',
            'table' => 'admin_password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
          ],
      ],

configファイルを変更したので、キャッシュをクリアします。

php artisan config:cache

コントローラの修正

パスワードリセットのボタンを押すと、ForgotPasswordControllershowLinkRequestForm() が実行されます。現状だと、SendsPasswordResetEmailsvendor/laravel/ui/auth-backend/SendsPasswordResetEmails.php) に処理がまとめられているため、このままだと管理者もユーザーと同じ挙動になってしまいます。
管理者は管理者向けの処理になるよう、ForgotPasswordController 内で各メソッドをオーバーライドします。

app\Http\Controllers\Admin\Auth\ForgotPasswordController.php
  <?php

  namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
+ use Illuminate\Support\Facades\Password;

  class ForgotPasswordController extends Controller
  {
      /*
      |--------------------------------------------------------------------------
      | Password Reset Controller
      |--------------------------------------------------------------------------
      |
      | This controller is responsible for handling password reset emails and
      | includes a trait which assists in sending these notifications from
      | your application to your users. Feel free to explore this trait.
      |
      */

      use SendsPasswordResetEmails;
+
+     public function showLinkRequestForm()
+     {
+         return view('admin.auth.passwords.email');
+     }
+
+     public function broker()
+     {
+         return Password::broker('admins');
+     }
}
app\Http\Controllers\Admin\Auth\ResetPasswordController.php
  <?php

  namespace App\Http\Controllers\Admin\Auth;

  use App\Http\Controllers\Controller;
  use App\Providers\RouteServiceProvider;
  use Illuminate\Foundation\Auth\ResetsPasswords;
+ use Illuminate\Http\Request;
+ use Illuminate\Support\Facades\Password;

  class ResetPasswordController extends Controller
  {

  ==(中略)==

      protected $redirectTo = RouteServiceProvider::ADMIN_HOME;
+
+     public function showResetForm(Request $request, $token = null)
+     {
+         return view('admin.auth.passwords.reset')->with(
+             ['token' => $token, 'email' => $request->email]
+         );
+     }
+
+     public function broker()
+     {
+         return Password::broker('admins');
+     }
}

リセットメールの変更

ここまでで一旦動作確認してみると、パスワードリセットメールは届きますが、本文中のURLがユーザー用のURLのままです。
管理者用の通知メールを作成し、そちらを利用するようにします。

Notificationの作成

php artisan make:notification コマンドを実行します。

php artisan make:notification AdminResetPasswordNotification

app/Notifications/AdminResetPasswordNotification.php が作成されるので、内容を修正します。
メールの送信は、 vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php の中の toMail() を使っているので、このメソッドをコピーしてURL部分だけ管理者用に変更しました。

app/Notifications/AdminResetPasswordNotification.php
  <?php

  namespace App\Notifications;

  use Illuminate\Bus\Queueable;
  use Illuminate\Contracts\Queue\ShouldQueue;
  use Illuminate\Notifications\Messages\MailMessage;
  use Illuminate\Notifications\Notification;
+ use Illuminate\Support\Facades\Lang;

  class AdminResetPasswordNotification extends Notification
  {
      use Queueable;
+     public static $toMailCallback;
+     public static $createUrlCallback;

      /**
       * Create a new notification instance.
       *
       * @return void
       */
-     public function __construct()
+     public function __construct($token)
      {
+         $this->token = $token;
      }

  ===(中略)===

      public function toMail($notifiable)
      {
          return (new MailMessage)
-                     ->line('The introduction to the notification.')
-                     ->action('Notification Action', url('/'))
-                     ->line('Thank you for using our application!');
+         if (static::$toMailCallback) {
+             return call_user_func(static::$toMailCallback, $notifiable, $this->token);
+         }
+ 
+         if (static::$createUrlCallback) {
+             $url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
+         } else {
+             $url = url(route('password.reset', [
+                 'token' => $this->token,
+                 'email' => $notifiable->getEmailForPasswordReset(),
+             ], false));
+         }
+ 
+         return (new MailMessage)
+             ->subject(Lang::get('Reset Password Notification'))
+             ->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
+             ->action(Lang::get('Reset Password'), $url)
+             ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
+             ->line(Lang::get('If you did not request a password reset, no further action is required.'));
        }

Adminモデルの修正

管理者は AdminResetPasswordNotification を使うように、Adminモデルを修正します。

app/Admin.php
  <?php

  namespace App;

  use Illuminate\Contracts\Auth\MustVerifyEmail;
  use Illuminate\Foundation\Auth\User as Authenticatable;
  use Illuminate\Notifications\Notifiable;
  use Illuminate\Database\Eloquent\Model;
  use Illuminate\Database\Eloquent\SoftDeletes;
+ use App\Notifications\AdminResetPasswordNotification;

  class Admin extends Authenticatable implements MustVerifyEmail
  {

  ===(中略)===
+
+     public function AdminResetPasswordNotification($token)
+     {
+             $this->notify(new AdminResetPasswordNotification($token));
+     }
  }

おわりに

ここまでで、管理者のログイン・パスワード変更・パスワードリセットができるようになりました。
今回、管理者の登録・削除は作成しませんでしたが、同じようにして作成できそうです。
ユーザー認証についてのまとめは、ここまでで一旦完了としたいと思います。

参考サイト

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

Laravel で FromRequest のバリデーションをテストする

Laravel の FormRequest クラスを利用すると各フィールドのバリデーションルールを array の形で定義することができます。

<?php

namespace App\Http\Requests\UserRegistration;

use Illuminate\Foundation\Http\FormRequest;

class UserRegistrationFormRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
                'min:2',
                'regex:/^[A-Za-z0-9]+$/',
            ],
            'password' => [
                'required',
                'string',
                'min:8',
                'regex:/[a-z]/',
                'regex:/[A-Z]/',
                'regex:/[0-9]/',
                'regex:/@$!%*#?&/',
            ],
        ];
    }
}

一つ一つのルールをテストするメリットは特にないと思いますが、上記のような複数ルールの組み合わせやカスタムバリデーションを検証するために Unit Test を作成する際の Tips を紹介します。

テストコード

ベースとなるメソッドは Stack Overflow から拝借しています(というか、この記事の要点はこの方法の紹介です!)。
これをベースに汎用的な FormRequest 用の TestCase クラスを作ってみます。

<?php

namespace Tests\Unit\App\Requests\Base;

use Illuminate\Foundation\Testing\TestCase;

abstract class BaseFormRequestTestCase extends TestCase
{
    /**
     * @var array
     */
    protected $rules;
    /**
     * @var \Validator
     */
    protected $validator;

    public function setUp(): void
    {
        parent::setUp();
        $this->setRules();
        $this->validator = $this->app['validator'];
    }

    /**
     * 子クラスで Request Class が持つ rules を set する。
     */
    abstract protected function setRules(): void;

    protected function getFieldValidator($field, $value)
    {
        return $this->validator->make(
            [$field => $value],
            [$field => $this->rules[$field]]
        );
    }

    protected function validateField($field, $value)
    {
        return $this->getFieldValidator($field, $value)->passes();
    }
}

これを継承した形で TestCase を作成すれば比較的シンプルにバリデーションのテストを書くことができます。

<?php

namespace Tests\Unit\App\Requests\UserRegistration\UserRegistrationFormRequest;

use App\Http\Requests\UserRegistration\UserRegistrationFormRequest;
use Tests\Unit\App\Requests\Base\BaseFormRequestTestCase;

class UserRegistrationFormRequestTest extends BaseFormRequestTestCase
{
    protected function setRules(): void
    {
        $this->rules = (new UserRegistrationFormRequest())->rules();
    }

    public function testNameRules() {
        $this->assertFalse($this->validateField('name', null));
        $this->assertFalse($this->validateField('name', 'a'));
        $this->assertTrue($this->validateField('name', 'ab'));
        $this->assertTrue($this->validateField('name', 'AB'));
        $this->assertTrue($this->validateField('name', '01'));
        $this->assertTrue($this->validateField('name', 'aA0'));
        $this->assertFalse($this->validateField('name', 'ab@'));
        $this->assertFalse($this->validateField('name', 'ab cd'));
    }

    public function testPasswordRules() {
        $this->assertFalse($this->validateField('password', null));
        $this->assertFalse($this->validateField('password', 'abAB01@'));
        $this->assertTrue($this->validateField('password', 'abAB01@$'));
        $this->assertFalse($this->validateField('password', 'abcABC012'));
        $this->assertFalse($this->validateField('password', 'ABC012@$!'));
        $this->assertFalse($this->validateField('password', '012@$!abc'));
        $this->assertFalse($this->validateField('password', '@$!abcABC'));
        $this->assertFalse($this->validateField('password', 'ab AB 01 @$'));
    }
}

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

Vue + Laravel + Firebaseで始めるWeb Push通知

はじめに

業務で FCM(Firebase Cloud Messaging)を使ったWeb Push通知をSPAで実装する機会がありました。
この記事では、いくつかポイントを設けながら段階的に実装できるように書きたいと思います。
またLaravelと記載していますが、HTTPクライアントライブラリのGuzzleを使ってリクエストを投げるだけです。

この記事で説明する範囲

今回VueやLaravel、その他のライブラリなどの詳細な説明は省きます。あくまでもFCMを使ったフォアグラウンド・バックグラウンドへのPush通知がメインです。

FCM(Firebase Cloud Messaging)とは

公式ページには

Firebase Cloud Messaging(FCM)は、メッセージを無料で確実に送信するためのクロスプラットフォーム メッセージング ソリューションです。

と記載されています。
今回はWebを対象としますが仕組みは共通で、通知を受信する端末が一意なトークンをFCM側に発行してもらい、送信時に対象のトークンを用いてFCMを経由し、Push通知を送るという仕組みで、Firebaseが提供しているサービスの一つです。

手順

  1. Firebaseでのプロジェクト作成・アプリ登録
  2. Voluntary Application Server Identification鍵(VAPID鍵)の取得
  3. 通知に対するPermissionの実装
  4. フォアグラウンド・バックグラウンドでの通知の受信
  5. Guzzleでリクエストを投げる

1. Firebaseでのプロジェクト作成・アプリ登録

Firebase を JavaScript プロジェクトに追加する
上記のリンクを確認しながら進めていけば問題ないかなと思います。
Laravelであれば Firebase SDK を追加して Firebase を初期化するという部分に関しては、Vueインスタンスを表示するbladeでFirebase SDKを追加しましょう。

index.blade.php
<head>
    <script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-messaging.js"></script>

また初期化に関しては、私の場合resource/js/plugins/firebase.jsなどのようにディレクトリを構成し、

firebase.js
if (!firebase.apps.length) {
    firebase.initializeApp({
        apiKey: process.env.MIX_FIREBASE_API_KEY,
        authDomain: process.env.MIX_FIREBASE_AUTH_DOMAIN,
        databaseURL: process.env.MIX_FIREBASE_DATABASE_URL,
        projectId: process.env.MIX_FIREBASE_PROJECT_ID,
        storageBucket: process.env.MIX_FIREBASE_STORAGE_BUCKET,
        messagingSenderId: process.env.MIX_FIREBASE_MESSAGING_SENDER_ID,
        appId: process.env.MIX_FIREBASE_APP_ID,
    })
}

export default firebase

exportしてオブジェクトを各ファイルで参照できるようにしていますが、オブジェクトを使用したいファイルで直接初期化を行っても問題ありません。

2. Voluntary Application Server Identification鍵(VAPID鍵)の取得・設定

Web Push通知をWebアプリケーションに飛ばす際FirebaseプロジェクトとWebアプリケーションを紐づける必要があります。Firebaseコンソールから新しく生成する方法、既存のリソースをインポートする方法の両方用意されています。
鍵生成.png

VAPID鍵を生成・インポートしたらresource/js/App.vueに早速コードを書いていきます。
最初に私の場合はresource/js/plugins/firebase.jsでFirebaseの初期化を行ったので、そちらのファイルをimportし、ライフサイクルの中でfirebase.messagingを呼び出します。
これでメッセージングサービスを使用する事ができます。

App.vue
import Firebase from './plugins/firebase'

export default {
    created() {
        const messaging = Firebase.messaging()

上記のようにメッセージオブジェクトを呼び出した後に、Voluntary Application Server Identification鍵(VAPID鍵)の取得を行った際に生成・インポートしたVAPID鍵をコードに追加します。

App.vue
export default {
    created() {
        const messaging = Firebase.messaging()
        messaging.usePublicVapidKey(process.env.MIX_FIREBASE_APP_VAPPI_KEY)

こちらのusePublicVapidKeyメソッドを使用するとFCMで各種プッシュサービスにメッセージリクエストを送信するときにVAPID鍵の認証情報を使用できます。

3. 通知に対するPermissionの実装

実際にPush通知を送信する為に、ユーザーからの許可を要請する必要があります。
そのためにはrequestPermissionメソッドを使用してユーザーに対する要請の結果をPromissで取得します。

App.vue
        const messaging = Firebase.messaging()
        messaging.usePublicVapidKey(process.env.MIX_FIREBASE_APP_VAPPI_KEY)

        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
            })
            .catch(err => {
                console.log('Error Occured')
            })

この段階でローカル環境の画面にアクセスすると
PermissionのRequest.png
このような形で通知を表示のダイアログが確認できると思います。
またこの段階で許可をタップすると、ブラウザのコンソールにHave permissionと表示されることも確認できるでしょう。

次のステップに移る前にサービスワーカーファイルを作成しておきましょう。
ここではfirebase-messaging-sw.jsというファイル名で作成することが決まっているので、そのファイル名でファイルを作成したらドメインのルートに置きます。Laravelであれば、public/配下にファイルを設置します。今は作ったファイルは空でも構いません。

4. フォアグラウンド・バックグラウンドでの通知の受信

まず始めにFCMから発行されるトークンを取得します。先ほど書いたユーザーへの許可を要請した処理で、成功した時にトークンを取得するメソッドを呼び出すだけです。

App.vue
        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
                return messaging.getToken()
            })
            .then(token => {
                console.log(`firebase cloud messaging token: ${token}`)
            })
            .catch(err => {
                console.log('Error Occured')
            })

ここまで書いて画面にアクセスするとトークンの取得が出来ていれば
getToken.png
このように表示されていると思います。
前述しましたが、こちらのFCMから発行されたトークンを使用して、対象の端末に通知を表示させます。

ここでPush通知が表示される際に2つのパターンがある事を認識しておく必要があります。

  • 1つ目はWebブラウザが閉じられていたり、ブラウザが閉じられていたりした場合は、3の通知に対するPermissionの実装の最後で作成したサービスワーカーファイル内でメッセージが受け取られます。
  • 2つ目はユーザーがWebページを表示している時です。この場合は実際のWebページ内でメッセージを受け取ります。なのでメッセージを受け取った際に何かのイベント(ラベルやマーカーの表示)を考えている場合は、メッセージを受け取るイベントで処理を書く必要があります。

それではメッセージを受け取るために、空で作成したサービスワーカーファイルにFirebaseメッセージングService Worker定義しましょう。

firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-messaging.js')

firebase.initializeApp({
    messagingSenderId: 'hogehogehoge'
})

const messaging = firebase.messaging()

ここでも特に変わった事はなく、Firebaseの初期化の後にmessagingを呼び出すだけです。

フォアグラウンドでの受信

次にこの章の目的でもあるフォアグランドでの通知の取得をonMessageイベントを呼び出して確認してみましょう。

App.vue
        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
                return messaging.getToken()
            })
            .then(token => {
                console.log(`firebase cloud messaging token: ${token}`)
            })
            .catch(err => {
                console.log('Error Occured')
            })

        messaging.onMessage(function(payload) {
            console.log('onMessage: ', payload)
        })

ここまで出来たらFCMのAPIにcurlでリクエストを投げて挙動を確認してみましょう。

curl -X POST \
-H "Authorization: key={MIX_FIREBASE_SERVER_KEY}" \
-H "Content-Type: application/json" \
-d '{
    "notification": {
        "title": "Hello",
        "body": "World",
        "icon": ""
    },
    "to": "{firebase return token}"
}'  \
"https://fcm.googleapis.com/fcm/send"

上記を実行すると
Foregrandメッセージ表示.png
こちらのように表示されていて、通知をWebページ内で受信している事が確認できると思います。

バックグラウンドでの受信

バックグラウンドで受信する際には、通知を受信するWebページにフォーカスがない状態をブラウザで作る(別タブで他のページを閲覧している)か、他のアプリにフォーカスが当たっている状態に先ほどのリクエストを投げると挙動が確認できると思います。

Backgroundでのメッセージ.png

確認できましたね。

今回は対象としませんが、通知オプションを設定したい場合(データペイロードを使用する場合など)はこちらを参照してください
基本的には、サービスワーカーファイルにメソッドを一つ追加して、データオプションに設定するだけです。

Guzzleでリクエストを投げる

ほぼおまけの内容ですが、先ほどのcurlで投げたリクエストを、Guzzleで投げる際のサンプルコードぐらいにみていただけると嬉しいです。

public static function handle($token, $content, $link)
    {
        $client = new Client();

        $server_key = env('MIX_FIREBASE_SERVER_KEY');

        $url = env('APP_URL');

        $fcm_endpoint = 'https://fcm.googleapis.com/fcm/send';

        $headers = [
            'Authorization' => 'key=' . $server_key,
            'Content-Type' => 'application/json'
        ];

        $fields = [
            'notification' => [
                'title' => 'Sample-Notification',
                'body' => $content,
                'icon' => asset('img/icon/logo-main.png'),
                'click_action' => $url . $link
            ],
            'to' => $token
        ];

        $client->post($fcm_endpoint, [
            'headers' => $headers,
            'json' => $fields
        ]);
    }

最後に

今回FCMを触るきっかけとなった実装では、フォアグラウンドでの処理にvue-notificationを使用して、ユーザーになんらかのアクションを行う導線を実装したりしました。
無料で始められ手軽に実装できるので使う際の利点は多そうです。

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

Docker Laravel6のアプリにユーザ認証機能を付与する

目的

  • 環境構築とLaravelアプリ作成後にユーザ認証の機能を付与する方法をまとめる。

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB

前提条件

前提情報

  • 特になし

読後感

  • Laravelアプリにauthを用いたユーザ認証機能を付与することができる。

概要

  1. Dockerのphpコンテナに入る
  2. DBの作成と設定
  3. ライブラリのインストール
  4. Authファイルの取得
  5. 確認

詳細

  1. Dockerのphpコンテナに入る

    1. 下記コマンドを実行してDockerの起動しているコンテナの一覧を表示する。

      $ docker ps
      
    2. PHPのコンテナを探し下記コマンドを実行する。

      $ docker exec -it phpコンテナの名前 bash
      
    3. Dockerコンテナ内で下記コマンドを実行してnginxのドキュメントルートに移動する。

      $ cd /usr/share/nginx/html/app
      
  2. DBの設定

    1. Dockerコンテナ内のappディレクトリで下記コマンドを実行して.envファイルを開く。

      $ vi .env
      
    2. DBの記載を下記の様に修正・追記を行う。

      /usr/share/nginx/html/app/.env
      DB_CONNECTION=mysql
      DB_HOST=mariadb
      DB_PORT=3306
      DB_DATABASE=laravel
      DB_USERNAME=laravel
      DB_PASSWORD=larapass
      
    3. 記載後の.envファイルの内容を下記に記載する。

      /usr/share/nginx/html/app/.env
      APP_NAME=Laravel
      APP_ENV=local
      APP_KEY=base64:3aQ51W/eAA0ICIpKCYhAygUc1SCWOcKBL9Sijrn8iOQ=
      APP_DEBUG=true
      APP_URL=http://localhost
      
      LOG_CHANNEL=stack
      
      DB_CONNECTION=mysql
      DB_HOST=mariadb
      DB_PORT=3306
      DB_DATABASE=laravel
      DB_USERNAME=laravel
      DB_PASSWORD=larapass
      
      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
      MAIL_FROM_ADDRESS=null
      MAIL_FROM_NAME="${APP_NAME}"
      
      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}"
      
  3. ライブラリのインストール

    1. Dockerコンテナ内のappディレクトリで下記コマンドを実行してライブラリを取得する。(ライブラリのバージョン指定1.*を行わず実行するとエラーになる。バージョン指定をせずエラーになった話はこちら→Laravel 6 $ composer require laravel/uiを実行するとエラーが発生する)

      $ composer require laravel/ui 1.*
      
  4. Authファイルの取得

    1. Dockerコンテナ内のappディレクトリで下記コマンドを実行してAuthに必要なファイルを取得する。

      $ php artisan ui bootstrap --auth
      
    2. Dockerコンテナ内で下記コマンド実行してnvmをインストールする。

      $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
      
    3. Dockerコンテナ内で下記コマンドを実行してnode.jsとyarnをインストールする。

      $ . ~/.nvm/nvm.sh && nvm install node && npm i -g yarn
      
    4. Dockerコンテナ内で下記コマンドを実行して必要なCSSファイルをインストールする。

      $ npm install && npm run dev
      
  5. 確認

    1. 下記にアクセスし、Laravelのホーム画面が出力されることを確認する。

    2. 「REGISTER」をクリックする。

      Laravel.png

    3. 下記の画面が出力されることを確認する。

      Laravel.png

    4. Dockerコンテナ内の/usr/share/nginx/html/appディレクトリに移動し下記コマンドを実行する。

      $ php artisan migrate
      
    5. 各認証情報を入力し「Registar」をクリックする。

      Laravel.png

    6. 下記の様に初期登録とログインすることができればユーザ認証機能の付与は完了である。

      Laravel.png

参考文献

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

Laravelでページネーション

前提条件

eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

Laravelでデータベースを操作する(クエリビルダ編)
本記事は上記が完了している前提で書かれています
本記事は上記で作成したフォルダとファイルを使用します

サービスクラス修正

ここで修正するサービスクラスは
Laravelでデータベースを操作する(クエリビルダ編)
で作成したものです
(1) /sample/app/Services/Interfaces/Table2Service.php修正
selectメソッドの引数修正
引数に$limit = null, $page = null, $sort = null追加

Table2Service.php
‥‥
    public function select($id, $varchar_col, $int_col, $datetime_col, $date_col, $time_col, $limit = null, $page = null, $sort = null);
‥‥

(2) /sample/tests/Services/Impl/Table2ServiceImpl.php修正
selectメソッドの引数修正
引数に$limit = null, $page = null, $sort = null追加

Tests\Services\Impl\Table2ServiceImpl.php
‥‥
    public function select($id, $varchar_col, $int_col, $datetime_col, $date_col, $time_col, $limit = null, $page = null, $sort = null)
    {
    }
‥‥

(3) /sample/app/Services/Impl/Table2ServiceImpl.php修正
selectメソッド修正

App\Services\Impl\Table2ServiceImpl.php
‥‥
    public function select($id, $varchar_col, $int_col, $datetime_col, $date_col, $time_col, $limit = null, $page = null, $sort = null)
    {

        $columns = [
            'table1.id            as table1_id',
            'table1.varchar_col   as table1_varchar_col',
            'table1.int_col       as table1_int_col',
            'table1.datetime_col  as table1_datetime_col',
            'table1.date_col      as table1_date_col',
            'table1.time_col      as table1_time_col',
            'table2.id            as table2_id',
            'table2.varchar_col   as table2_varchar_col',
            'table2.int_col       as table2_int_col',
            'table2.datetime_col  as table2_datetime_col',
            'table2.date_col      as table2_date_col',
            'table2.time_col      as table2_time_col',
            'table3.id            as table3_id',
            'table3.varchar_col   as table3_varchar_col',
            'table3.int_col       as table3_int_col',
            'table3.datetime_col  as table3_datetime_col',
            'table3.date_col      as table3_date_col',
            'table3.time_col      as table3_time_col',
        ];

        $db = DB::table('table2')
                    ->leftJoin('table1', 'table2.table1_id', '=', 'table1.id')
                    ->leftJoin('table3', 'table2.id', '=', 'table3.table2_id')
                    ->select($columns);

        if (!is_null($id)) {
            $db->where('table2.id', '=', $id);
        }
        if (!is_null($varchar_col)) {
            $db->where('table2.varchar_col', 'like', '%'. addcslashes($varchar_col, '\\_%') . '%');
        }
        if (!is_null($int_col)) {
            $db->where('table2.int_col', '=', $int_col);
        }
        if (!is_null($datetime_col)) {
            $db->where('table2.datetime_col', '=', $datetime_col);
        }
        if (!is_null($date_col)) {
            $db->where('table2.date_col', '=', $date_col);
        }
        if (!is_null($time_col)) {
            $db->where('table2.time_col', '=', $time_col);
        }

        if (!is_null($sort)) {
            $sortCol = ['table2.id',
                        'table2.varchar_col',
                        'table2.int_col',
                        'table2.datetime_col',
                        'table2.date_col',
                        'table2.time_col'];
            $db->orderBy($sortCol[$sort]);
        }

        if (!is_null($page) && !is_null($limit)) {
            $recordList = $db->paginate($limit, $columns, 'page', $page);
        } else {
            $recordList = $db->get();
        }

        return $recordList;

    }
‥‥

大きな修正は$db->paginateメソッドを追加したことです
paginateメソッドの
第1引数は1ページに表示する件数です。
第2引数はselectするカラムです。
今回は
DB::table('table2')
‥‥
->select($columns);
で取得するカラムを指定しているので意味ないです
第3引数はページ番号が送信されてくるHTTPリクエストパラメーター名です。
第4引数はページ番号です。select文ではこの引数-1がoffsetになります
第2引数から第4引数は省略できます。省略した場合、第2引数は[*]、第3引数は'page'、第4引数はnullになり、ページ番号は第3引数名のHTTPパラメーター値になります(第3引数、第4引数を省略すれば、pageという名前のHTTPパラメーター値となる)
今回はクエリビルダを使いましたが、paginateメソッドはEloquentでも使えます
Laravelでデータベースを操作する(Eloquent編)
でEloquentを使うサービスクラスを作成しました。そこではgetメソッドをつかってselectしましたが、getメソッドの部分を今回と同じpaginateメソッドに変更すれば、Eloquentでページネーションできます。クエリビルダのpaginateメソッドもEloquentのpaginateメソッドも引数は同じです

フォームリクエストの修正

ここで修正するフォームリクエストは
Laravelでデータベースを操作する(クエリビルダ編)
で作成したものです
/sample/app/Http/Requests/Table2Request.php修正

Table2Request.php
‥‥
    public function rules()
    {
‥‥
        return [
            'id'               => [$idRequire, 'numeric', 'max:18446744073709551615'],
            'table1_id'        => ['nullable', 'numeric', 'max:18446744073709551615'],
            'varchar_col'      => ['nullable', 'max:255'],
            'int_col'          => ['nullable', 'integer', 'max:2147483647'],
            'datetime_col'     => ['nullable', 'date_format:Y-m-d H:i:s'],
            'date_col'         => ['nullable', 'date_format:Y-m-d'],
            'time_col'         => ['nullable', 'date_format:H:i:s'],
            'limit'            => ['nullable', 'integer', 'min:1', 'max:200'],
            'page'             => ['nullable', 'integer', 'min:1', 'max:2147483647'],
            'sort'             => ['nullable', 'integer', 'min:0', 'max:5'],
        ];
    }
‥‥

limit、page、sortの入力制限を追加しました

Controllerにメソッド追加

(1) /sample/app/Http/Controllers/SampleController.phpにpaginateQueryBuilderメソッドを追記
フォームリクエスト、サービスクラスのuse文は
Laravelでデータベースを操作する(クエリビルダ編)
で書きました

SampleController.php
    public function paginateQueryBuilder(Table2Request $request, Table2Service $table2Service)
    {
        if (is_null($request->session()->get('errors'))) {
            $request->flash();
        }

        $id = $request->input('id');
        $varchar_col = $request->input('varchar_col');
        $int_col =  $request->input('int_col');
        $datetime_col =  $request->input('datetime_col');
        $date_col =  $request->input('date_col');
        $time_col =  $request->input('time_col');
        $limit =  $request->input('limit') ?? 5;
        $page =  $request->input('page') ?? 1;
        $sort =  $request->input('sort') ?? 0;

        $recordList = null;
        if (is_null($request->session()->get('errors'))) {
            $recordList = $table2Service->select($id, $varchar_col, $int_col, $datetime_col, $date_col, $time_col, $limit, $page, $sort);
        }

        $data = [
            'recordList'   => $recordList,
            'id'           => $id,
            'varchar_col'  => $varchar_col,
            'int_col'      => $int_col,
            'datetime_col' => $datetime_col,
            'date_col'     => $date_col,
            'time_col'     => $time_col,
            'limit'        => $limit,
            'page'         => $page,
            'sort'         => $sort
        ];

        return view('sample.paginate', $data);
    }

(2) /sample/routes/web.phpに下記を追記
Route::get('sample/paginate-query-builder', 'SampleController@paginateQueryBuilder');

viewの作成

(1) ページネーションビューの作成
ページネーションビューとは前のページへリンク、次のページへリンク、ページ番号リンクを描画するビューです
コマンドラインで
cd sample
php artisan vendor:publish --tag=laravel-pagination
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseプロジェクトを右クリック→リフレッシュ
resources/views/vendor/paginationフォルダが現れます。そのフォルダの中に5つファイルができています
その中のbootstrap-4.blade.phpが今回使うページネーションビューになります。
ページネーションビューを独自のデザインにする場合、bootstrap-4.blade.phpを修正してください
今回修正したサービスクラスで使ったpaginateメソッドの戻り値(LengthAwarePaginatorクラスのインスタンス)がデフォルトで使用するページネーションビューがpagination::bootstrap-4です。
LengthAwarePaginator::defaultViewメソッドでデフォルトビューを変えることができます。また、デフォルトビューを変えなくても、後程使うlinksメソッドの第1引数にビュー名を渡すことでもカスタムページネーションビューを使用できます

(2) /sample/resources/views/sample/paginate.blade.phpファイル作成

paginate.blade.php
<html>
    <head>
        <title>sample</title>
        <style>
        .sample-table {
            border-collapse:collapse;
            white-space: nowrap;
            border: 1px solid #000000;
        }
        .sample-table thead {
            background-color: #33CCFF;
            color: #FFFFFF;
            font-weight: bold;
        }
        .sample-table td {
            padding-left:10px;
            padding-right:10px;
            border: 1px solid #000000;
        }
        .pagination li {
            display: inline-block;
        }
        </style>
    </head>
    <body>

        <form action="{{ url('sample/paginate-query-builder') }}" method="get">
            @error('id')
                @foreach ($errors->get('id') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>id<input type="text" name="id" value="{{ old('id') }}"></div>
            @error('varchar_col')
                @foreach ($errors->get('varchar_col') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>varchar_col<input type="text" name="varchar_col" value="{{ old('varchar_col') }}"></div>
            @error('int_col')
                @foreach ($errors->get('int_col') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>int_col<input type="text" name="int_col" value="{{ old('int_col') }}"></div>
            @error('datetime_col')
                @foreach ($errors->get('datetime_col') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>datetime_col<input type="text" name="datetime_col" value="{{ old('datetime_col') }}"></div>
            @error('date_col')
                @foreach ($errors->get('date_col') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>date_col<input type="text" name="date_col" value="{{ old('date_col') }}"></div>
            @error('time_col')
                @foreach ($errors->get('time_col') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>time_col<input type="text" name="time_col" value="{{ old('time_col') }}"></div>
            @error('limit')
                @foreach ($errors->get('limit') as $error)
                    <div style="color:red;">{{ $error }}</div>
                @endforeach
            @enderror
            <div>1ページ表示件数<input type="text" name="limit" value="{{ old('limit', 5) }}"></div>
            <input type="submit" >
        </form>

        <br>

        @isset($recordList)
            @if ($recordList->total() === 0)
                <div>
                    検索結果は0件です
                </div>
            @else
                <table class="sample-table">
                    <thead>
                        <tr>
                            <td colspan=6>table1</td>
                            <td colspan=6>table2</td>
                            <td colspan=6>table3</td>
                        </tr>
                        <tr>
                            <td>id</td>
                            <td>varchar_col</td>
                            <td>int_col</td>
                            <td>datetime_col</td>
                            <td>date_col</td>
                            <td>time_col</td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 0])
                                                       ->url($recordList->currentPage()) }}" >id</a></td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 1])
                                                       ->url($recordList->currentPage()) }}" >varchar_col</a></td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 2])
                                                       ->url($recordList->currentPage()) }}" >int_col</a></td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 3])
                                                       ->url($recordList->currentPage()) }}" >datetime_col</a></td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 4])
                                                       ->url($recordList->currentPage()) }}" >date_col</a></td>
                            <td><a href="{{ $recordList->appends([
                                                                    'id'           => $id,
                                                                    'varchar_col'  => $varchar_col,
                                                                    'int_col'      => $int_col,
                                                                    'datetime_col' => $datetime_col,
                                                                    'date_col'     => $date_col,
                                                                    'time_col'     => $time_col,
                                                                    'limit'        => $limit,
                                                                    'page'         => $page,
                                                                    'sort'         => 5])
                                                       ->url($recordList->currentPage()) }}" >time_col</a></td>
                            <td>id</td>
                            <td>varchar_col</td>
                            <td>int_col</td>
                            <td>datetime_col</td>
                            <td>date_col</td>
                            <td>time_col</td>
                        </tr>
                    </thead>
                    <tbody>
                            @foreach ($recordList as $record)
                                <tr>
                                    <td>{{ $record->table1_id }}</td>
                                    <td>{{ $record->table1_varchar_col }}</td>
                                    <td>{{ $record->table1_int_col }}</td>
                                    <td>{{ $record->table1_datetime_col }}</td>
                                    <td>{{ $record->table1_date_col }}</td>
                                    <td>{{ $record->table1_time_col }}</td>
                                    <td>{{ $record->table2_id }}</td>
                                    <td>{{ $record->table2_varchar_col }}</td>
                                    <td>{{ $record->table2_int_col }}</td>
                                    <td>{{ $record->table2_datetime_col }}</td>
                                    <td>{{ $record->table2_date_col }}</td>
                                    <td>{{ $record->table2_time_col }}</td>
                                    <td>{{ $record->table3_id }}</td>
                                    <td>{{ $record->table3_varchar_col }}</td>
                                    <td>{{ $record->table3_int_col }}</td>
                                    <td>{{ $record->table3_datetime_col }}</td>
                                    <td>{{ $record->table3_date_col }}</td>
                                    <td>{{ $record->table3_time_col }}</td>
                                </tr>
                            @endforeach
                    </tbody>
                </table>
            @endif
        @endisset

        @isset($recordList)
            {{ $recordList->appends([
                                    'id'           => $id,
                                    'varchar_col'  => $varchar_col,
                                    'int_col'      => $int_col,
                                    'datetime_col' => $datetime_col,
                                    'date_col'     => $date_col,
                                    'time_col'     => $time_col,
                                    'limit'        => $limit,
                                    'page'         => $page,
                                    'sort'         => $sort
                                    ])
                          ->links() }}
        @endisset

    </body>
</html>

thead->tr->tdタグ内のaタグのhref属性では$recordList->appends()->url()を実行してます。$recordListはLengthAwarePaginatorクラスのインスタンスです。
$recordList->currentPage()は現在のページ番号を返します
url($recordList->currentPage())は$recordList->currentPage()ページのURLを返します。
$recordList->appendsはurl()が返すURLに付加するクエリ文字列です
よく見ていただくと、appendsメソッドに渡している配列のsort要素の値を変えています

paginate.blade.php下部に書いた$recordList->appends()->links()は先ほど生成したbootstrap-4.blade.phpを描画します
linksメソッドの第1引数にビュー名を渡すことでbootstrap-4.blade.phpではないカスタムページネーションビューを描画することもできます
appendsメソッドは先ほど説明した通りページリンクURLに付加するクエリ文字列です

動作確認

http://localhost/laravelSample/sample/paginate-query-builder
にアクセスすると画面下部にページリンクが描画されています

POSTに対応してみる

今作成したものはGETメソッドに対応したものでした。
多くの場合、検索はGETメソッドで大丈夫でしょうが、
検索条件がたくさん設置され、長い文字列カラムが検索条件に入っている場合など、GETメソッドにした場合、URLの文字列制限に引っかかり、URLのクエリ文字列が途中で切れてしまうこともあります
そのような事情で検索をPOSTメソッドで実装した場合に対応してみましょう

(1) まず、resources/views/sample/paginate.blade.phpのformタグのmethod属性値をpostに変え、sample/routes/web.phpに定義した'sample/paginate-query-builder'のルーティングをRoute::getからRoute::match(['get', 'post']に変更します(今回は動作確認でsample/paginate-query-builderに直リンクするためgetも許容しておく)

(2) resources/views/vendor/pagination/bootstrap-4.blade.php修正
@if ($paginator->hasPages())直下(2行目)に下記を追記

    @php
        $___paginatorHiddenFunc = function ($name, $value) use (&$___paginatorHiddenFunc) {
            if (is_array($value)) {
                foreach ($value as $k => $v) {
                    $___paginatorHiddenFunc($name . '[' . $k . ']', $v);
                }
            } else {
                echo '<input type="hidden" name="' . htmlspecialchars($name, ENT_QUOTES) . '" value="' . htmlspecialchars($value, ENT_QUOTES) . '">';
            }
        };
        $___paginatorQueryList = null;
        parse_str(parse_url($paginator->url(1), PHP_URL_QUERY), $___paginatorQueryList);
    @endphp

やっていることは2つです
type属性値がhiddenのinputタグを出力するメソッドを定義している($___paginatorHiddenFunc)
1ページ目のページリンクのクエリ文字列を配列として取得している($___paginatorQueryList)

bootstrap-4.blade.phpにはaタグが3つ書いてあります
前のページへリンク、ページ番号リンク、次のページへリンクです
これらをformタグに直します

前のページへリンク

<form action="{{ $paginator->path() }}" method="post">
    @csrf
    @foreach ($___paginatorQueryList as $___paginatorQueryName => $___paginatorQueryValue)
        @if ($___paginatorQueryName === $paginator->getPageName())
            @php $___paginatorHiddenFunc($___paginatorQueryName, $paginator->currentPage() - 1); @endphp
        @elseif ($___paginatorQueryName !== '_token')
            @php $___paginatorHiddenFunc($___paginatorQueryName, $___paginatorQueryValue); @endphp
        @endif
    @endforeach
    <input type="submit" value="&lsaquo;">
</form>

ページ番号リンク

<form action="{{ $paginator->path() }}" method="post">
    @csrf
    @foreach ($___paginatorQueryList as $___paginatorQueryName => $___paginatorQueryValue)
        @if ($___paginatorQueryName === $paginator->getPageName())
            @php $___paginatorHiddenFunc($___paginatorQueryName, $page); @endphp
        @elseif ($___paginatorQueryName !== '_token')
            @php $___paginatorHiddenFunc($___paginatorQueryName, $___paginatorQueryValue); @endphp
        @endif
    @endforeach
    <input type="submit" value="{{$page}}">
</form>

次のページへリンク

<form action="{{ $paginator->path() }}" method="post">
    @csrf
    @foreach ($___paginatorQueryList as $___paginatorQueryName => $___paginatorQueryValue)
        @if ($___paginatorQueryName === $paginator->getPageName())
            @php $___paginatorHiddenFunc($___paginatorQueryName, $paginator->currentPage() + 1); @endphp
        @elseif ($___paginatorQueryName !== '_token')
            @php $___paginatorHiddenFunc($___paginatorQueryName, $___paginatorQueryValue); @endphp
        @endif
    @endforeach
    <input type="submit" value="&rsaquo;">
</form>

やっていることは
配列として取得した1ページ目のページリンクのクエリ文字列($___paginatorQueryList)をループし
type属性値がhiddenのinputタグを出力するメソッド($___paginatorHiddenFunc)を実行しているだけです

(3) resources/views/sample/paginate.blade.phpファイル作成
thead->tr->tdタグ内のaタグをformタグに変更します

<form action="{{ $recordList->path() }}" method="post">
 @csrf
 <input type="hidden" name="id" value="{{ $id }}">
 <input type="hidden" name="varchar_col" value="{{ $varchar_col }}">
 <input type="hidden" name="int_col" value="{{ $int_col }}">
 <input type="hidden" name="datetime_col" value="{{ $datetime_col }}">
 <input type="hidden" name="date_col" value="{{ $date_col }}">
 <input type="hidden" name="time_col" value="{{ $time_col }}">
 <input type="hidden" name="limit" value="{{ $limit }}">
 <input type="hidden" name="sort" value="0">{{-- ここは各カラムの番号にする --}}
 <input type="hidden" name="page" value="{{ $page }}">
 <input type="submit" value="id">{{-- ここは各カラム名にする --}}
</form>

動作確認

http://localhost/laravelSample/sample/paginate-query-builder
にアクセスします
ページリンクをクリックするとPOST送信できました
見た目は自由にCSSで変えてください

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