- 投稿日:2019-03-05T23:50:37+09:00
Laravelで同時ログイン数制御をする
web系の方だと馴染みないかもしれませんが、SIerだと
同時ログイン数を制御したい
という要望って多少はあるのかなと思います。今日、
Laravelで同時ログイン数の制御をしたい
という相談を受けてサンプルを作ってみたのでその時のメモです。前提
- php: 7.3
- laravel: 5.8
- mysql: 5.7
ゴール
以下の要件を満たす処理サンプルを実装する
- 同時にログイン出来るユーザーは2人まで
- 判定は先勝ち
- すでに2人ログインしていたらその2人のいずれかがログアウトするまでログイン出来ない
成果物
今回実装したサンプルはこちらに置いてあります
https://github.com/TsukasaGR/laravel-concurrent-login-control-sample準備
Laravelだと認証の仕組みを作るまでは爆速で出来ますね。
本題ではないですが、さらっと記載しておきます。1. Laravelインストール
インストール手順はたくさん情報があるのでコマンドだけ記載します。
# Laravelプロジェクトを作成 composer create-project --prefer-dist laravel/laravel laravel-concurrent-login-control-sample # 作成したディレクトリに移動 cd laravel-concurrent-login-control-sample # パッケージインストール composer install # env書き換え # --- DBの設定を変更2. 認証機能をインストール
認証もコマンド実装するだけですね。
まずは認証機能を入れます。php artisan make:authあとはマイグレーションするだけです。
php artisan migrateこれで完成したのでアクセス確認してみると、
無事認証機能が追加されているのが確認出来ると思います。
同時ログイン実装
本題です。
同時ログインの実装方法ですが、今回はDBで制御しようと思います。
なんとかセッションの値使って出来ないかなと思ったんですが無理そうなので素直にDBで。同時ログイン管理用テーブル作成
まずは同時ログイン管理用のテーブルを作成します。
php artisan make:migration create_logged_in_users_table --create=logged_in_usersdatabase/migrations/xxxx_xx_xx_xxxxxx_create_logged_in_users_table.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateLoggedInUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('logged_in_users', function (Blueprint $table) { $table->unsignedBigInteger('user_id'); $table->timestamps(); $table->foreign('user_id', 'f_logged_in_users_user_id') ->references('id')->on('users')->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('logged_in_users'); } }これでマイグレーションを実行すれば完了です。
php artisan migrateログイン後に管理用テーブルにデータを追加
制御の前に、ログインしたら管理用テーブルにデータを追加するようにします。
いくつか方法はあると思いますが、今回はmake:authで作成したログイン機能をそのまま流用したいのでAuth/LoginController.phpを修正します。
ポイントは、
- 既存の認証処理はそのまま使い回す
- ログイン後、画面に遷移する前にデータを差し込む
の2点です。
まず1点目の使い回しですが、
AuthenticatesUsers.login
を使いまわしたいので、Traitのuseを以下のように変更します。app/Http/Controllers/Auth/LoginController.phpclass LoginController extends Controller { - use AuthenticatesUsers; + use AuthenticatesUsers { + // loginメソッドを自身のコントローラーに持たせたいので元のメソッドに別名を付与する + login as _login; + } + + // 自身のコントローラー側のメソッドを呼び出すようにする + public function login(Request $request) + { + // 処理は元のまま + return $this->_login($request); + }これで使いまわしの準備が出来たので、あとはログイン後にデータを追加するだけです。
モデルを作成して、
php artisan make:model LoggedInUser使えるようにします。
app/LoggedInUser.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class LoggedInUser extends Model { protected $fillable = ['user_id']; }モデルが使えるようになったら先程作成したloginメソッドを修正し、最終的にこのような形にします。
app/Http/Controllers/Auth/LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use App\LoggedInUser; class LoginController extends Controller { use AuthenticatesUsers { login as _login; } protected $redirectTo = '/home'; public function __construct() { $this->middleware('guest')->except('logout'); } public function login(Request $request) { // ログイン後と画面遷移の間に処理をはさみたいので返り値を一旦変数に入れる $response = $this->_login($request); // ログイン管理テーブルにデータを追加 LoggedInUser::create([ 'user_id' => \Auth::id() ]); // レスポンスを返す return $response; } }これでログイン後に管理テーブルにデータを追加することが出来ました。
ログアウト後に管理テーブルからデータを削除
こちらもログインと同様、既存の認証処理を使いまわします。
考え方は一緒で、以下の通りとします。app/Http/Controllers/Auth/LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use App\LoggedInUser; class LoginController extends Controller { use AuthenticatesUsers { login as _login; logout as _logout; } protected $redirectTo = '/home'; public function __construct() { $this->middleware('guest')->except('logout'); } public function login(Request $request) { // ログイン後と画面遷移の間に処理をはさみたいので返り値を一旦変数に入れる $response = $this->_login($request); // ログイン管理テーブルにデータを追加 LoggedInUser::create([ 'user_id' => \Auth::id() ]); // レスポンスを返す return $response; } public function logout(Request $request) { // ログアウト前にデータを削除 LoggedInUser::where('user_id', \Auth::id())->delete(); // 既存のログアウト処理を実行 return $this->_logout($request); } }これでログアウト後に管理テーブルからデータを削除することが出来ました。
同時ログイン数を制御する
最後にログイン数の制御です。
ログインのタイミングで判定出来るようにしたいのでloginに対してFormRequestを作成します。php artisan make:request LoginRequest今回は入力値とは関係ないのでクロージャでバリデーションロジックを直接記入します。
app/Http/Requests/LoginRequest.php<?php namespace App\Http\Requests; use App\LoggedInUser; use Illuminate\Foundation\Http\FormRequest; class LoginRequest extends FormRequest { public function authorize() { return \Auth::check(); } public function rules() { return [ 'email' => [ function ($attribute, $value, $fail) { $numberOfLoginUser = LoggedInUser::count(); $isLoggedIn = LoggedInUser::where('user_id', \Auth::id())->exists(); // ログインユーザーが2名いる場合で、自身がまだログインしていなければエラーを返す if ($numberOfLoginUser > 2 && !$isLoggedIn) { return $fail(trans('validation.custom.limit-login')); // エラーメッセージは適宜修正 } } ] ]; } }これで完成です。
さいごに
今回はあくまでサンプル実装なので、
- ユーザー登録時も管理用テーブルにデータを入れる必要がある
- 同一ユーザーが再度ログインした場合はデータ追加でなく更新する必要がある
- ログアウトせずにユーザーが放置したことを考慮して定期的にリフレッシュする必要がある
等考慮する点はまだまだありますが、それでもここまで実装できていれば要件決めと決まったものをつらつら書くくらいで事足りるのではないかなと思います。
さくっと考えて試しただけですので、もっと良い方法がありましたらぜひご指摘頂けますと幸いです?
- 投稿日:2019-03-05T23:50:37+09:00
Laravelで同時ログイン数を制御をする
web系の方だと馴染みないかもしれませんが、SIerだと
同時ログイン数を制御したい
という要望って多少はあるのかなと思います。今日、
Laravelで同時ログイン数の制御をしたい
という相談を受けてサンプルを作ってみたのでその時のメモです。前提
- php: 7.3
- laravel: 5.8
- mysql: 5.7
ゴール
以下の要件を満たす処理サンプルを実装する
- 同時にログイン出来るユーザーは2人まで
- 判定は先勝ち
- すでに2人ログインしていたらその2人のいずれかがログアウトするまでログイン出来ない
成果物
今回実装したサンプルはこちらに置いてあります
https://github.com/TsukasaGR/laravel-concurrent-login-control-sample準備
Laravelだと認証の仕組みを作るまでは爆速で出来ますね。
本題ではないですが、さらっと記載しておきます。1. Laravelインストール
インストール手順はたくさん情報があるのでコマンドだけ記載します。
# Laravelプロジェクトを作成 composer create-project --prefer-dist laravel/laravel laravel-concurrent-login-control-sample # 作成したディレクトリに移動 cd laravel-concurrent-login-control-sample # パッケージインストール composer install # env書き換え # --- DBの設定を変更2. 認証機能をインストール
認証もコマンド実装するだけですね。
まずは認証機能を入れます。php artisan make:authあとはマイグレーションするだけです。
php artisan migrateこれで完成したのでアクセス確認してみると、
無事認証機能が追加されているのが確認出来ると思います。
同時ログイン実装
本題です。
同時ログインの実装方法ですが、今回はDBで制御しようと思います。
なんとかセッションの値使って出来ないかなと思ったんですが無理そうなので素直にDBで。同時ログイン管理用テーブル作成
まずは同時ログイン管理用のテーブルを作成します。
php artisan make:migration create_logged_in_users_table --create=logged_in_usersdatabase/migrations/xxxx_xx_xx_xxxxxx_create_logged_in_users_table.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateLoggedInUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('logged_in_users', function (Blueprint $table) { $table->unsignedBigInteger('user_id'); $table->timestamps(); $table->foreign('user_id', 'f_logged_in_users_user_id') ->references('id')->on('users')->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('logged_in_users'); } }これでマイグレーションを実行すれば完了です。
php artisan migrateログイン後に管理用テーブルにデータを追加
制御の前に、ログインしたら管理用テーブルにデータを追加するようにします。
いくつか方法はあると思いますが、今回はmake:authで作成したログイン機能をそのまま流用したいのでAuth/LoginController.phpを修正します。
ポイントは、
- 既存の認証処理はそのまま使い回す
- ログイン後、画面に遷移する前にデータを差し込む
の2点です。
まず1点目の使い回しですが、
AuthenticatesUsers.login
を使いまわしたいので、Traitのuseを以下のように変更します。app/Http/Controllers/Auth/LoginController.phpclass LoginController extends Controller { - use AuthenticatesUsers; + use AuthenticatesUsers { + // loginメソッドを自身のコントローラーに持たせたいので元のメソッドに別名を付与する + login as _login; + } + + // 自身のコントローラー側のメソッドを呼び出すようにする + public function login(Request $request) + { + // 処理は元のまま + return $this->_login($request); + }これで使いまわしの準備が出来たので、あとはログイン後にデータを追加するだけです。
モデルを作成して、
php artisan make:model LoggedInUser使えるようにします。
app/LoggedInUser.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class LoggedInUser extends Model { protected $fillable = ['user_id']; }モデルが使えるようになったら先程作成したloginメソッドを修正し、最終的にこのような形にします。
app/Http/Controllers/Auth/LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use App\LoggedInUser; class LoginController extends Controller { use AuthenticatesUsers { login as _login; } protected $redirectTo = '/home'; public function __construct() { $this->middleware('guest')->except('logout'); } public function login(Request $request) { // ログイン後と画面遷移の間に処理をはさみたいので返り値を一旦変数に入れる $response = $this->_login($request); // ログイン管理テーブルにデータを追加 LoggedInUser::create([ 'user_id' => \Auth::id() ]); // レスポンスを返す return $response; } }これでログイン後に管理テーブルにデータを追加することが出来ました。
ログアウト後に管理テーブルからデータを削除
こちらもログインと同様、既存の認証処理を使いまわします。
考え方は一緒で、以下の通りとします。app/Http/Controllers/Auth/LoginController.php<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use App\LoggedInUser; class LoginController extends Controller { use AuthenticatesUsers { login as _login; logout as _logout; } protected $redirectTo = '/home'; public function __construct() { $this->middleware('guest')->except('logout'); } public function login(Request $request) { // ログイン後と画面遷移の間に処理をはさみたいので返り値を一旦変数に入れる $response = $this->_login($request); // ログイン管理テーブルにデータを追加 LoggedInUser::create([ 'user_id' => \Auth::id() ]); // レスポンスを返す return $response; } public function logout(Request $request) { // ログアウト前にデータを削除 LoggedInUser::where('user_id', \Auth::id())->delete(); // 既存のログアウト処理を実行 return $this->_logout($request); } }これでログアウト後に管理テーブルからデータを削除することが出来ました。
同時ログイン数を制御する
最後にログイン数の制御です。
ログインのタイミングで判定出来るようにしたいのでloginに対してFormRequestを作成します。php artisan make:request LoginRequest今回は入力値とは関係ないのでクロージャでバリデーションロジックを直接記入します。
app/Http/Requests/LoginRequest.php<?php namespace App\Http\Requests; use App\LoggedInUser; use Illuminate\Foundation\Http\FormRequest; class LoginRequest extends FormRequest { public function authorize() { return \Auth::check(); } public function rules() { return [ 'email' => [ function ($attribute, $value, $fail) { $numberOfLoginUser = LoggedInUser::count(); $isLoggedIn = LoggedInUser::where('user_id', \Auth::id())->exists(); // ログインユーザーが2名いる場合で、自身がまだログインしていなければエラーを返す if ($numberOfLoginUser > 2 && !$isLoggedIn) { return $fail(trans('validation.custom.limit-login')); // エラーメッセージは適宜修正 } } ] ]; } }これで完成です。
さいごに
今回はあくまでサンプル実装なので、
- ユーザー登録時も管理用テーブルにデータを入れる必要がある
- 同一ユーザーが再度ログインした場合はデータ追加でなく更新する必要がある
- ログアウトせずにユーザーが放置したことを考慮して定期的にリフレッシュする必要がある
等考慮する点はまだまだありますが、それでもここまで実装できていれば要件決めと決まったものをつらつら書くくらいで事足りるのではないかなと思います。
さくっと考えて試しただけですので、もっと良い方法がありましたらぜひご指摘頂けますと幸いです?
- 投稿日:2019-03-05T18:25:07+09:00
Laravel HomesteadがVBoxHeadlessで重い
状況
作業中Macがやたら重いと思ってアクティビティモニタを確認。
VBoxHeadless
というプロセスがCPUの限界を超して101%
もCPUを使用している。
笑えない解決方法
こちらの方法で解決。
vagrant halt
で仮想マシンを停止vagrant plugin install vagrant-vbguest
でプラグインをインストールvagrant plugin update vagrant-vbguest
でプラグインをアップデート拡張機能のバージョンの食い違いで遅くなるらしい。
ゲストプラグインをインストールすると治る。再び
vagrant up
でマシンを起動し、アクティビティモニタを確認するとVBoxHeadless
のCPU使用率が6%
ほどになっていた。
良かった
- 投稿日:2019-03-05T18:07:51+09:00
[初心者]Laravelのインストール
# composerのインストール satounoMacBook-Pro:~ satou$ brew install homebrew/core/composer ==> Downloading https://getcomposer.org/download/1.8.4/composer.phar ######################################################################## 100.0% ?/usr/local/Cellar/composer/1.8.4: 3 files, 1.8MB, built in 2 minutes 36 seconds# laravelのインストール satou$ composer global require "laravel/installer" Changed current directory to /Users/satou/.composer Using version ^2.0 for laravel/installer ./composer.json has been created Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 12 installs, 0 updates, 0 removals - Installing symfony/process (v4.2.4): Downloading (100%) - Installing symfony/polyfill-ctype (v1.10.0): Downloading (100%) - Installing symfony/filesystem (v4.2.4): Downloading (100%) - Installing symfony/polyfill-mbstring (v1.10.0): Downloading (100%) - Installing symfony/contracts (v1.0.2): Downloading (100%) - Installing symfony/console (v4.2.4): Downloading (100%) - Installing guzzlehttp/promises (v1.3.1): Downloading (100%) - Installing ralouphie/getallheaders (2.0.5): Downloading (100%) - Installing psr/http-message (1.0.1): Downloading (100%) - Installing guzzlehttp/psr7 (1.5.2): Downloading (100%) - Installing guzzlehttp/guzzle (6.3.3): Downloading (100%) - Installing laravel/installer (v2.0.1): Downloading (100%) symfony/contracts suggests installing psr/cache (When using the Cache contracts) symfony/contracts suggests installing psr/container (When using the Service contracts) symfony/contracts suggests installing symfony/cache-contracts-implementation symfony/contracts suggests installing symfony/service-contracts-implementation symfony/contracts suggests installing symfony/translation-contracts-implementation symfony/console suggests installing psr/log (For using the console logger) symfony/console suggests installing symfony/event-dispatcher symfony/console suggests installing symfony/lock guzzlehttp/guzzle suggests installing psr/log (Required for using the Log middleware) Writing lock file Generating autoload files# パスを通す?いまいち理解してない vi ~/.bashrc satounoMacBook-Pro:~ satou$ source ~/.bashrc# Laravelのバージョン確認 satounoMacBook-Pro:helloapp satou$ php artisan --version =>Laravel Framework 5.8.2# 新規アプリケーションの作成 taskappはアプリケーション名 satounoMacBook-Pro:projects satou$ laravel new taskapp Crafting application... Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 74 installs, 0 updates, 0 removals - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing dragonmantank/cron-expression (v2.2.0): Loading from ca ~~~~省略〜〜〜 Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. Application ready! Build something amazing.# マイグレーションファイルの作成 # --create=tasks はオプション。テーブルの名前を指定できる。 satou$ php artisan make:migration create_tasks_table --create=tasks Created Migration: 2019_03_05_052059_create_tasks_table# マイグレート実行、テーブルの作成 $php artisan migrate# モデルの作成 $ php artisan make:model Task Model created successfully.
- 投稿日:2019-03-05T17:48:21+09:00
初心者のLaravel5.8、Herokuデプロイ備忘録
#gitリポジトリの作成 git init#githubリモートリポジトリの追加 git remote add origin https://github.com/satou-yuuki/taskapp.git#githubへpush git push origin master#herokuのリモートリポジトリの追加? $ heroku git:remote -a taskapp-laravel set git remote heroku to https://git.heroku.com/taskapp-laravel.git#リモートリポジトリの確認 $ git remote heroku origin#herokuへpush $ git push heroku master#herokuアドオン postgreSQLの追加 無料版 $ heroku addons:create heroku-postgresql:hobby-dev#herokuの環境変数確認 $ heroku config === taskapp-laravel Config Vars APP_KEY: base64:euuylwlZCqo3Ae9z32Le0gZcQO3653b+E1mABIQMuTs= DATABASE_URL: postgres://sgahvfpoxsthtn:1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad@ec2-54-83-196-179.compute-1.amazonaws.com:5432/d6etanlh202i2t satounoMacBook-Pro:taskapp satou$ heroku run php artisan migrate#環境変数の登録 $ heroku config:set DB_HOST=ec2-54-83-196-179.compute-1.amazonaws.com Setting DB_HOST and restarting ⬢ taskapp-laravel... done, v17 DB_HOST: ec2-54-83-196-179.compute-1.amazonaws.com $ heroku config:set DB_USERNAME=sgahvfpoxsthtn Setting DB_USERNAME and restarting ⬢ taskapp-laravel... done, v18 DB_USERNAME: sgahvfpoxsthtn $ heroku config:set DB_PASSWORD=1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad Setting DB_PASSWORD and restarting ⬢ taskapp-laravel... done, v19 DB_PASSWORD: 1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad $ heroku config:set DB_DATABASE=d6etanlh202i2t Setting DB_DATABASE and restarting ⬢ taskapp-laravel... done, v20 DB_DATABASE: d6etanlh202i2t#migrarationsテーブルの作成らしい。 必要なかったかも。 $ heroku run "php artisan migrate:install" Running php artisan migrate:install on ⬢ taskapp-laravel... up, run.3082 (Free)#マイグレート実行 マイグレーションファイルがマイグレートされる $ heroku run "php artisan migrate" Running php artisan migrate on ⬢ taskapp-laravel... up, run.9664 (Free) ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yse //yseでも通るってことはyだけでokぽい Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_03_05_052059_create_tasks_table Migrated: 2019_03_05_052059_create_tasks_table#ブラウザ立ち上がってアプリが開く $heroku open
- 投稿日:2019-03-05T17:48:21+09:00
[初心者]Laravel5.8、Herokuデプロイ備忘録
#gitリポジトリの作成 git init#githubリモートリポジトリの追加 git remote add origin https://github.com/satou-yuuki/taskapp.git#githubへpush git push origin master#herokuのリモートリポジトリの追加? $ heroku git:remote -a taskapp-laravel set git remote heroku to https://git.heroku.com/taskapp-laravel.git#リモートリポジトリの確認 $ git remote heroku origin#herokuへpush $ git push heroku master#herokuアドオン postgreSQLの追加 無料版 $ heroku addons:create heroku-postgresql:hobby-dev#herokuの環境変数確認 $ heroku config === taskapp-laravel Config Vars APP_KEY: base64:euuylwlZCqo3Ae9z32Le0gZcQO3653b+E1mABIQMuTs= DATABASE_URL: postgres://sgahvfpoxsthtn:1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad@ec2-54-83-196-179.compute-1.amazonaws.com:5432/d6etanlh202i2t satounoMacBook-Pro:taskapp satou$ heroku run php artisan migrate#環境変数の登録 $ heroku config:set DB_HOST=ec2-54-83-196-179.compute-1.amazonaws.com Setting DB_HOST and restarting ⬢ taskapp-laravel... done, v17 DB_HOST: ec2-54-83-196-179.compute-1.amazonaws.com $ heroku config:set DB_USERNAME=sgahvfpoxsthtn Setting DB_USERNAME and restarting ⬢ taskapp-laravel... done, v18 DB_USERNAME: sgahvfpoxsthtn $ heroku config:set DB_PASSWORD=1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad Setting DB_PASSWORD and restarting ⬢ taskapp-laravel... done, v19 DB_PASSWORD: 1680d670e04921e66f1047536b43b9c1b8464400fb7c8397b702c38b9dd99fad $ heroku config:set DB_DATABASE=d6etanlh202i2t Setting DB_DATABASE and restarting ⬢ taskapp-laravel... done, v20 DB_DATABASE: d6etanlh202i2t#migrarationsテーブルの作成らしい。 必要なかったかも。 $ heroku run "php artisan migrate:install" Running php artisan migrate:install on ⬢ taskapp-laravel... up, run.3082 (Free)#マイグレート実行 マイグレーションファイルがマイグレートされる $ heroku run "php artisan migrate" Running php artisan migrate on ⬢ taskapp-laravel... up, run.9664 (Free) ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yse //yseでも通るってことはyだけでokぽい Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_03_05_052059_create_tasks_table Migrated: 2019_03_05_052059_create_tasks_table#ブラウザ立ち上がってアプリが開く $heroku open
- 投稿日:2019-03-05T13:30:01+09:00
Laravel入門者が マイグレーション処理の流れを クソ丁寧に追ったら、追いきれなくなった話
結論から申し上げます
本記事はなにもまとまっておらず、ただただmigrationに係るファイルを一つ一つ追っていったログ です。
本当は↓↓こんな感じの図を交えつつ
「Laravelのmigration処理を解き明かしたった!」みたいなタイトルで一世を風靡しようと思ったのですが、挫折しました。
せっかく途中まで調べたので恥を忍んで公開しておこうと思います。
悔しいからまた挑戦します!!ということで、以下ログです。
migrationってなに?
まずはmigrationの本来の意味を調べます。
mi·gra·tion
1. seasonal movement of animals from one region to another.
2. movement from one part of something to another.ざっくり"場所から場所への移動"と捉えておけば問題ないでしょう。
次に、公式ドキュメントを確認。
https://readouble.com/laravel/5.5/ja/migrations.html...横文字だらけでわかりづらいのですが、要するにデータベースの定義をLaravelからできるものです。
- Migrationファイルを作って
- そのファイルの中身に指令を書き込んで
- その指令通りにLaravelにデータベースを操作してもらう
この3パターンで完了です!
ということで、それぞれの処理について詳細をみていきましょう。
登場人物
migrationファイルを作る
まずは、マイグレーション処理をするために、migrationファイルを作成しましょう。
マイグレーションファイルを作るためには、
php artisan
コマンドを使います。Laravel用のコマンドですね。
(artisanは職人という意味らしいです)ここでは、
test
というテーブルを作成したいとします。root@8c8e2d4a121f:/var/www# php artisan make:migration create_tests_table Created Migration: 2019_03_01_085359_create_tests_tableすると、database/migrations 配下に新しいファイルができているのがわかります。
ファイルの中身をみてみましょう。
2019_03_01_085359_create_tests_table.php<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateTestsTable extends Migration { public function up() { Schema::create('tests', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('tests'); } }
Migration
クラスを継承する形でCreateTestsTable
クラスが作成されていることがわかると思います。中身をみてみると、
up()
という関数とdown()
という関数があることがわかります。
up()
メソッドは新規テーブル、カラム、インデックスをデータベースに追加することができます。down()
は逆に、up()
メソッドの処理を元に戻す機能を記述します。そして、両メソッド -
up()
とdown()
- の中身はSchema
ファサードが使えます。Schemaとは何か。それを次にみていきます。回り道: Migration.phpを覗いてみる
では、少し回り道をして
Migration
クラスを覗いてみましょう。抽象クラスのようですね。
コメントを日本語化して掲載してみます。※抽象クラスの解説はこちらの記事が詳しいです。
https://laraweb.net/surrounding/1972/<?php namespace Illuminate\Database\Migrations; abstract class Migration { //データベース接続するために使う名前。 protected $connection; //サポートされている場合、トランザクション内でマイグレーションをラップすることを可能にします。 public $withinTransaction = true; //マイグレーション接続の名前を取得します。 public function getConnection() { return $this->connection; } }Migrationクラス自体は抽象メソッド(abstract)ですが、内部のメソッド・プロパティには
abstract
が定義されていないので、これら3つはそのままCreateTestsTable
クラスに継承されるわけですね。up()の中身を記述する - Schemaファサード
さて、では
up()
メソッドの中身を改めて確認していきましょう。2019_03_01_085359_create_tests_table.phppublic function up() { Schema::create('tests', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); }ファサードってなんぞ?となると思いますが、ここでファサードの元々の意味をまた確認してみましょう。
fa·cade
the face of a building, especially the principal front that looks onto a street or open space.建物の正面、という意味です。詳細はこちらの記事がわかりやすかったです。
https://qiita.com/yousan/items/18dab4dbac4a27ce3662
Laravelのファサードとは、建築用語での「建築物 の 正面部分」という意味が、プログラミングでの「ライブラリ(建築物)の呼び出し部分(正面部分)」という意味になるわけですね。
ということで、Schemaファサード(ここでは
create
)についてみていきましょうSchema::create('tests', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); });
::
はスコープ定義演算子です。クラスをインスタンス化しなくても、クラス内のプロパティを扱うことができます。
http://php.net/manual/ja/language.oop5.paamayim-nekudotayim.phpつまり、Schemaのcreateメソッドを実行して、という意味でそのまんま捉えてOKです。
Schema::create([テーブル名], [Blueprintオブジェクトを受け取るクロージャ関数] )という形式で書かれています。
回り道:createを探す旅
さて、ここでまた回り道をしてcreateがどこに記述されているかを確認してみましょう。
Schemaクラスを確認してみます。
vendor/laravel/framework/src/Illuminate/Support/Facades/Schema.php
Schema.php<?php namespace Illuminate\Support\Facades; /** * @method static \Illuminate\Database\Schema\Builder create(string $table, \Closure $callback) * ... * @see \Illuminate\Database\Schema\Builder */ class Schema extends Facade { /** * 接続のために Schema Builder インスタンスを取得する。 * @return \Illuminate\Database\Schema\Builder */ public static function connection($name) { return static::$app['db']->connection($name)->getSchemaBuilder(); } /** * Schema Builder インスタンスを デフォルトの接続として取得する。 * @return \Illuminate\Database\Schema\Builder */ protected static function getFacadeAccessor() { return static::$app['db']->connection()->getSchemaBuilder(); } }...あれ?createがない?
ということで、継承元のFacade
クラスを確認してみるが、ない。あちこち探してみると、こんな場所にいました。
vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php
Builder.php<?php namespace Illuminate\Database\Schema; use Closure; use LogicException; use Illuminate\Database\Connection; class Builder { ... /** * schema上にテーブルを作成する。 */ public function create($table, Closure $callback) { $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) { $blueprint->create(); $callback($blueprint); })); } ... }
create($table, Closure $callback)
となっており、先ほどの定義と一致しますね。でも
Builder
クラスなんてSchema
クラスにはどこにも出てきません。
では、どこで接続するのか?...それは、後半のお楽しみ。
閑話休題。
Facade
クラスにもconnection()
メソッドはありませんでした。
するとどういうことが起こるのか?
Facade
クラス内の__callStatic
に注目してください。vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
Facade.php<?php namespace Illuminate\Support\Facades; ... abstract class Facade { ... public static function __callStatic($method, $args) { $instance = static::getFacadeRoot(); if (! $instance) { throw new RuntimeException('A facade root has not been set.'); } return $instance->$method(...$args); } }
__callStatic
はマジックメソッドと呼ばれるPHPの文法の一種で、アンダーライン2つ(__)から始まる関数名は、ある条件下の時に発動するメソッドです。
http://php.net/manual/ja/language.oop5.magic.php
__callStatic
は存在しない静的メソッドが呼ばれたら実行されます。ここで振り返ってみてください。
Schema::create
とcreate
メソッドを呼び出しているのに、Schema
クラスにも、Facade
クラスにもcreate
が存在しません。
...そう、つまり、__callStatic
が実行されるわけです!ということで、
__callStatic
の中身を見ましょう。すると、
$instance = static::getFacadeRoot();という処理を行なっていますね。
ちなみにここで
static::
が使用されていますが、これは遅延静的束縛と呼ばれるものです。
ここでは説明を割愛しますが、こちらのサイトがわかりやすいです。
https://ackintosh.github.io/blog/2013/08/25/late-static-binding/では、
getFacadeRoot
に関連する場所を探してみましょう。Facade.php/** * ファサードの裏にあるルートオブジェクトを取得する */ public static function getFacadeRoot() { return static::resolveFacadeInstance(static::getFacadeAccessor()); } /** * ファサードのルートのインスタンスをコンテナから解決する */ protected static function resolveFacadeInstance($name) { if (is_object($name)) { return $name; } if (isset(static::$resolvedInstance[$name])) { return static::$resolvedInstance[$name]; } return static::$resolvedInstance[$name] = static::$app[$name]; }まずは
static::resolveFacadeInstance(static::getFacadeAccessor())
という処理をさせたいわけですね。
ここで着目したいのがgetFacadeAccessor()
はどこにあるのか、という話。
これは、はるか昔に書いた、Scheme
クラスの中に出てきます。Schema.php<?php namespace Illuminate\Support\Facades; class Schema extends Facade { protected static function getFacadeAccessor() { return static::$app['db']->connection()->getSchemaBuilder(); } }
$app
はFacade側で定義されています(ややこしい...)
再び戻って確認します。
すると、こんな関数が。Facade.phppublic static function setFacadeApplication($app) { static::$app = $app; }どうやら、bootstrap中にこの
setFacadeApplication
メソッドを使って値を設定するようです。https://daylerees.com/laravel-facades/
The first static property contains a reference to the Laravel IoC container. It’s set during the framework bootstrap process using the setFacadeApplication() method found in the full example above.
で、このプロパティはabstract facadeにくっついているから、
Facade
クラスを継承する全てのクラスに適用されるらしい。なるほど。ということで、コメント部にあるように、
Facade.php/** * The application instance being facaded. * * @var \Illuminate\Contracts\Foundation\Application */ protected static $app;という定義で良いようだ。
Illuminate\Contracts\Foundation にはApplication.php
がある。
そして、これがサービスコンテナの操作をすることができるインスタンスなのだ。
やっとゴールが見えてきた気がするぞ!!さて、ここで更に寄り道。
return static::$app['db']->connection()->getSchemaBuilder();これ、なんで配列になっているか、気になりませんか?
クッソ調べまくって見つけました。
https://stackoverflow.com/questions/50530710/how-laravel-uses-object-as-an-array-in-facade-class?rq=1PHPにある
ArrayAccess
インターフェイスを使っているからでした。
http://php.net/manual/ja/class.arrayaccess.php
スッキリした...!なので、
$app['db']
インスタンスを参照ているわけです。つまり、下記とニアリーイコールです。
DB
ファサードはIlluminate\Database\DatabaseManager
を参照します。ここのファサードクラス一覧に書いてあります。
https://readouble.com/laravel/5.5/ja/facades.htmlということで、当該の
DatabaseManager.php
をみてみましょうDatabaseManager.php<?php namespace Illuminate\Database; use PDO; use Illuminate\Support\Arr; use Illuminate\Support\Str; use InvalidArgumentException; use Illuminate\Database\Connectors\ConnectionFactory; /** * @mixin \Illuminate\Database\Connection */ class DatabaseManager implements ConnectionResolverInterface { ... /** * The application instance. * * @var \Illuminate\Foundation\Application */ protected $app; ... /** * 新規の DataBaaseManagerインスタンスを作成する * @param \Illuminate\Foundation\Application $app * @param \Illuminate\Database\Connectors\ConnectionFactory $factory * @return void */ public function __construct($app, ConnectionFactory $factory) { $this->app = $app; $this->factory = $factory; } ... /** * Database Connection インスタンス を取得する。 * @param string $name * @return \Illuminate\Database\Connection */ public function connection($name = null) { list($database, $type) = $this->parseConnectionName($name); $name = $name ?: $database; // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will // set the "fetch mode" for PDO which determines the query return types. if (! isset($this->connections[$name])) { $this->connections[$name] = $this->configure( $this->makeConnection($database), $type ); } return $this->connections[$name]; } ... }先ほど言ったように、
DB
ファサードはインスタンス化されているため、__construct
メソッドは実行ずみです。では、
connection()
メソッドの内部を深掘りしていきましょう。
すると、PHP弱者には初めましての文法がたくさん...確認していきます。
list($database, $type) = $this->parseConnectionName($name);これは、List関数です。右辺の配列を順に左側の変数に代入してくれます。
http://php.net/manual/ja/function.list.phphttps://www.sejuku.net/blog/24406
こちらの例がわかりやすいですね。引用します。$fruits_array = ['りんご', 'オレンジ', 'メロン']; //listの複数変数を配列から代入 list($apple, $orange, $melon) = $fruits_array;今回の例では、
parseConnectionName($name)
で取得した配列の結果を順に代入している、ということのようですね。
parseってことはもともと繋がって入力されたものを分割するんでしょうね。
$name
は今回入力がないため、connectionメソッドのargsデフォルト値である$name = null
が適用されます。DatabaseManager.phpprotected function parseConnectionName($name) { $name = $name ?: $this->getDefaultConnection(); return Str::endsWith($name, ['::read', '::write']) ? explode('::', $name, 2) : [$name, null]; }出たな、三項演算子!
?:
と書かれると「ん?」となりますが、怖がることはありません。$name = $name ?: $this->getDefaultConnection();は
$name = $name ? $name : $this->getDefaultConnection();と同義です。
もっというと、
if ( $name ) { $name = $name; } else { $name = $this->getDefaultConnection(); }です。
ということで、今回は前述のように$name
はnull
なので、getDefaultConnection()
メソッドが発動します。DatabaseManager.phppublic function getDefaultConnection() { return $this->app['config']['database.default']; }appは$appが代入されている箇所があります。つまり、先ほどと同じ、
\Illuminate\Foundation\Application
のインスタンスです。では、
$this->app['config']['database.default']
とはなんでしょうか。またこちらをreferします。
https://readouble.com/laravel/5.5/ja/facades.html
Config
はIlluminate\Config\Repository
らしい。
というところまではわかったのですが、どこの値を参照しているかまでは追えず....
StackOverflowにも投げてみようかと思いまうす。
わかるかた、コメントいただけると嬉しいです。一度立ち戻って、
parseConnectionName($name)
はコメント通りParse the connection into an array of the name and read / write type.
つまり、名前と読み書き権限の配列を返してくれるメソッドだと理解して次に進みましょう。
随分上になってしまったので
connection()
メソッドを再掲します。connection()/** * Database Connection インスタンス を取得する。 * @param string $name * @return \Illuminate\Database\Connection */ public function connection($name = null) { list($database, $type) = $this->parseConnectionName($name); $name = $name ?: $database; // If we haven't created this connection, we'll create it based on the config // provided in the application. Once we've created the connections we will // set the "fetch mode" for PDO which determines the query return types. if (! isset($this->connections[$name])) { $this->connections[$name] = $this->configure( $this->makeConnection($database), $type ); } return $this->connections[$name]; }
parseConnectionName($name)
のおかげで、名前
と読み書き権限
を$database
と$type
に格納することができました。
そして、もしconnections[$name]
が存在しなかったら、configure()
メソッドを実行して、設定をしてくれます。DatabaseManager.php/** * DatabaseConnectionインスタンスを準備する * * @param \Illuminate\Database\Connection $connection * @param string $type * @return \Illuminate\Database\Connection */ protected function configure(Connection $connection, $type) { $connection = $this->setPdoForType($connection, $type); // First we'll set the fetch mode and a few other dependencies of the database // connection. This method basically just configures and prepares it to get // used by the application. Once we're finished we'll return it back out. if ($this->app->bound('events')) { $connection->setEventDispatcher($this->app['events']); } // Here we'll set a reconnector callback. This reconnector can be any callable // so we will set a Closure to reconnect from this manager with the name of // the connection, which will allow us to reconnect from the connections. $connection->setReconnector(function ($connection) { $this->reconnect($connection->getName()); }); return $connection; }そして、注目して欲しいのが戻り値の型。
@return \Illuminate\Database\Connectionとなっていますね。
Connection
クラスを確認してみましょう。vendor/laravel/framework/src/Illuminate/Database/Schema/Connection.php
Connection.php<?php namespace Illuminate\Database; ... use Illuminate\Database\Schema\Builder as SchemaBuilder; ... class Connection implements ConnectionInterface { .... /** * 接続のために SchemaBuilderインスタンスを作成する * * @return \Illuminate\Database\Schema\Builder */ public function getSchemaBuilder() { if (is_null($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } return new SchemaBuilder($this); } ... }なんか見覚えのある奴がいませんか?
...そう、'Builder'クラスだ!
覚えていない人のためにもう一度。
create()
メソッドがいる場所でしたね。vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php
Builder.php<?php namespace Illuminate\Database\Schema; use Closure; use LogicException; use Illuminate\Database\Connection; class Builder { ... /** * schema上にテーブルを作成する。 */ public function create($table, Closure $callback) { $this->build(tap($this->createBlueprint($table), function ($blueprint) use ($callback) { $blueprint->create(); $callback($blueprint); })); } ... }この
Builder
クラスがConnection
クラスでuse Illuminate\Database\Schema\Builder as SchemaBuilder;
として呼び出されています。つまり、SchemaBuilder
として先ほどのBuilder
クラスが使われているわけですね。
そして、getSchemaBuilder()
というメソッドでSchemaBuilder, すなわちBuilder
クラスのインスタンスを作成しているんですね!
(ちなみに名前をわざわざSchemaBuilder
としているのは、 ...\ Query \Builder もuseしているためです。こちらをQueryBuilder
として名前を分けています。)SchemaBuilder($this)
$this
とは、Connection
クラス自身を指します。では、改めてSchema/Builder.phpの
__constructor
(クラスがインスタンス化された時に呼び出されるメソッド)を確認しましょう。Builder.phppublic function __construct(Connection $connection) { $this->connection = $connection; $this->grammar = $connection->getSchemaGrammar(); }ここで注目するのは
getSchemaGrammer()
です。Builderをインスタンス化するときに、Schemaの文法を取得しろ、と命令しているわけです。
怪しくなってきましたね。ということで、再び
Connection
クラスに戻り、getSchemaGrammer()
を確認します。Connection.phppublic function getSchemaGrammar() { return $this->schemaGrammar; }このschemaGrammerは
setSchemaGrammar()
によって定義されます。Connection.phppublic function setSchemaGrammar(Schema\Grammars\Grammar $grammar) { $this->schemaGrammar = $grammar; }
...頭がごっちゃごちゃになったので一旦ここまでで勘弁してください....
参考
How Laravel Facades Work and How to Use Them Elsewhere
ファサードの概念を掴むのにめっちゃわかりやすい
Laravel Facades
これもファサードの解説
PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応
サービスコンテナの概要はこの本。
- 投稿日:2019-03-05T11:50:01+09:00
LaravelのEloquentでカラムの変更情報を取得
カラムが変更されたか判断する必要があり、動作を確認したメモ。
>>> $group = App\Group::all()->last(); => App\Group {#49052 id: 10, name: "Aグループ", mailaddress: "hoge@example.com", prefectures_id: 13, city: "千代田区", address: "大手町一丁目", telephone: "03XXXXXXXX", created_at: "2019-01-09 12:09:56", updated_at: "2019-01-09 12:09:56", }値を変更してみる
>>> $group->name = "hoge"; => "hoge" >>> $group->telephone = "03YYYYYYYY" => "03YYYYYYYY"
isDrity
メソッドで確認。変更されてるのでtrue
ですね。>>> $group->isDirty(); => trueカラム絞っても確認できます。
>>> $group->isDirty("name"); => true >>> $group->isDirty("city"); => false >>> $group->isDirty(["name", "city"]); => true変更された内容は
getDirty
メソッドで確認。
変更されたnameとtelephoneの内容が確認できる。>>> $group->getDirty(); => [ "name" => "hoge", "telephone" => "03YYYYYYYY", ]変更前の値が知りたい場合には
getOriginal
メソッド>>> $group->getOriginal(); => [ "id" => 10, "name" => "Aグループ", "mailaddress" => "hoge@example.com", "prefectures_id" => 13, "city" => "千代田区", "address" => "大手町一丁目", "telephone" => "03XXXXXXXX", "created_at" => "2019-01-09 12:09:56", "updated_at" => "2019-01-09 12:09:56", ] >>> $group->getOriginal("name"); => "Aグループ"
save
してみる。
isDirty
はfalseを返すようになり、getOriginal
で確認しても更新後の値に書き換わっている。>>> $group->save(); => true >>> $group->isDirty(); => false >>> $group->getOriginal("name"); => "hoge"
- 投稿日:2019-03-05T03:02:29+09:00
Laravel で最低限書いてあると嬉しいテスト
TLDR;
簡単なWEBアプリケーションを書くときに最低限書いておいて良かったと思うテストの紹介
リポジトリは下記
https://github.com/katsuren/laravel-test前提
Not SPA - JS が絡むものは NodeJS のテストフレームワーク使ったほうがよい
Not SPA でも JS が多い場合は dusk や codeception を使ったほうが良いと思う
Laravel についての知識、下記は知っているものとして解説する
factory、リレーションあたり
feature/unit テストについてサンプルアプリケーションの概要
CDの管理アプリケーション作る
アーティストとそのアルバム、曲名を管理できるアプリケーション
簡単のためにログインは省くテスト戦略的なもの
モデルが利用可能なこと、リレーションが正しいことをおさえる
基本的なリソースの CRUD のテストをおさえる
具体的には、各ルーティングがエラーで落ちていないことを確認する
また、各機能画面にてあるべき input があることを確認する (nameで確認)その他、このテストでは RefreshDatabase を利用するので、.env.testing を用意して別DBでテストしたほうが良い。
さもなくばテストのたびにDBの中身がクリアされる。M(モデル)のテスト
・マイグレーションを書く(テーブル定義)
・ファクトリーを書く(レコード生成の定義)
・ファクトリーが使えることを確認これで最低限モデルの定義が間違っていないことが保証される
具体的なファイルは下記のようになる
# database/migrations/2019_03_04_123520_create_artists_table.php <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateArtistsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('artists', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('artists'); } }# app/Eloquents/Artist.php <?php namespace App\Eloquents; use Illuminate\Database\Eloquent\Model; class Artist extends Model { protected $guarded = [ 'id', ]; }# database/factories/ArtistFactory.php <?php use App\Eloquents\Artist; use Illuminate\Support\Str; use Faker\Generator as Faker; $factory->define(Artist::class, function (Faker $faker) { return [ 'name' => $faker->name, ]; });テストは下記
# tests/Unit/Eloquents/ArtistTest.php <?php namespace Tests\Unit\Eloquents; use App\Eloquents\Artist; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ArtistTest extends TestCase { use RefreshDatabase; public function testFactoryable() { $eloquent = app(Artist::class); $this->assertEmpty($eloquent->get()); // 初期状態では空であることを確認 $entity = factory(Artist::class)->create(); // 先程作ったファクトリーでレコード生成 $this->assertNotEmpty($eloquent->get()); // 再度getしたら中身が空ではないことを確認し、ファクトリ可能であることを保証 } }M(モデル)のテスト2(リレーション)
アーティストは複数のアルバムを保持することができ、
アルバムはアーティストに所属している、ことを確認する
これをテストすることで、リレーションが保証される。各モデルは下記のようになる
# app/Eloquents/Artist.php <?php class Artist extends Model { // 下記追加 public function albums() { return $this->hasMany(Album::class); } }# app/Eloquents/Album.php <?php namespace App\Eloquents; use Illuminate\Database\Eloquent\Model; class Album extends Model { protected $guarded = [ 'id', ]; public function artist() { return $this->belongsTo(Artist::class); } }テストは次のようになる
# tests/Unit/Eloquents/ArtistTest.php <?php namespace Tests\Unit\Eloquents; use App\Eloquents\Album; use App\Eloquents\Artist; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ArtistTest extends TestCase { // 下記のテスト追加 public function testArtistHasManyAlbums() { $count = 5; $artistEloquent = app(Artist::class); $albumEloquent = app(Album::class); $artist = factory(Artist::class)->create(); // アーティストを作成 $albums = factory(Album::class, $count)->create([ 'artist_id' => $artist->id, ]); // アーティストに紐づくアルバムレコードを作成 (create の引数に指定するとその値でデータ作成される) // refresh() で再度同じレコードを取得しなおし、リレーション先の件数が作成した件数と一致することを確認し、リレーションが問題ないことを保証 $this->assertEquals($count, count($artist->refresh()->albums)); } }# tests/Unit/Eloquents/AlbumTest.php <?php namespace Tests\Unit\Eloquents; use App\Eloquents\Album; use App\Eloquents\Artist; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class AlbumTest extends TestCase { use RefreshDatabase; public function testFactoryable() { $eloquent = app(Album::class); $this->assertEmpty($eloquent->get()); $entity = factory(Album::class)->create(); $this->assertNotEmpty($eloquent->get()); } public function testAlbumBelongsToArtist() { $albumEloquent = app(Album::class); $artistEloquent = app(Artist::class); $artist = factory(Artist::class)->create(); $album = factory(Album::class)->create([ 'artist_id' => $artist->id, ]); $this->assertNotEmpty($album->artist); } }VC (ビュー・コントローラー)のテスト
具体的なソースコードは長くなってしまうので概要のリポジトリを参照のこと。
見た目は変わりやすいのでここではテスト対象にしない。(崩れやすいテスト) もし必要な場合は dusk などでスクリーンショット比較などをする。
簡単な CRUD コントローラーで、各ルーティングにアクセスして 4xx または 5xx が出ないことを確実にする。
リソースをそれぞれ簡単に説明するとindex = リソースの一覧表示の画面、検索フォームなどがある場合が多い
show = 指定のリソースの詳細表示画面、
create = 新規作成画面、フォームがあることを確認する
store = 新規作成処理(POST)
edit = リソースの更新設定画面、フォームがあることを確認する
update = 更新処理(PUT)
destroy = 削除処理(DELETE)となる。
各画面の機能として、input の name="xxx" がすべて表示されることを assert することで必ず input があるということを保証する。
これにより、最低限入力フォームの存在とその処理が保証されることになる。
例えば ArtistsController は下記のような実装だとする# app/Http/Controllers/ArtistsController.php <?php namespace App\Http\Controllers; use App\Eloquents\Artist; use App\Http\Requests\ArtistRequest; class ArtistsController extends Controller { protected $artistEloquent; public function __construct(Artist $artistEloquent) { $this->artistEloquent = $artistEloquent; } public function index() { $artists = $this->artistEloquent->pimp(request()->input('search', []))->get(); return view('artists.index')->with([ 'artists' => $artists, ]); } public function show($id) { $artist = $this->artistEloquent->find($id); return view('artists.show')->with([ 'artist' => $artist, ]); } public function create() { return view('artists.edit')->with([ 'isCreate' => true, ]); } public function store(ArtistRequest $request) { $artist = $this->artistEloquent->create($request->input('artist')); return redirect('/artists/' . $artist->id)->with('flash_message', 'アーティストを作成しました'); } public function edit($id) { $artist = $this->artistEloquent->find($id); return view('artists.edit')->with([ 'isCreate' => false, 'artist' => $artist, ]); } public function update(ArtistRequest $request, $id) { $artist = $this->artistEloquent->find($id); $artist->fill($request->input('artist')); $artist->save(); return redirect('/artists/' . $id)->with('flash_message', 'アーティストを更新しました'); } public function destroy($id) { $artist = $this->artistEloquent->find($id); $artist->delete(); return redirect('/artists')->with('flash_message', 'アーティストを削除しました'); } }それに対するテストは下記のように HTTP アクセスで確認する
基本的にHTMLの検査は文字列の検査しかできない、つまりdocumentの要素をリッチに検索するAPIは提供されておらず、その場合はduskを利用する。
簡素に変更がされないものを対象に検査する。具体的には下記の項目。
・200(HTTP OK)が返ること、またはリダイレクト(301 or 302)が返ること。
・フォームが存在する場合はそのアクションURLが存在すること、正しいこと。
・フォームが存在する場合はその項目(name=xxx)すべてが存在すること。# tests/Feature/ArtistsControllerTest.php <?php namespace Tests\Feature; use App\Eloquents\Artist; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Str; use Tests\TestCase; class ArtistsControllerTest extends TestCase { use RefreshDatabase; public function testCanSeeIndex() { $response = $this->get('/artists'); $response->assertOk() // インデックスにアクセスして 200 が返ることを確認 ->assertSee('action="/artists"') // 検索フォームのアクションがあることを確認 ->assertSee('name="search[name]"'); // search[name] という検索項目があることを確認 } public function testCanSeeShow() { $artist = factory(Artist::class)->create(); $response = $this->get('/artists/' . $artist->id); $response->assertOk() ->assertSee($artist->name); // 詳細ページでレコード固有の文字列が閲覧できることを確認 } public function testCanCreate() { $response = $this->get('/artists/create'); $response->assertOk() ->assertSee('action="/artists"') // 作成フォームがあること ->assertSee('name="artist[name]"'); // 名前入力フォームがあること $name = Str::random(10); $response = $this->from('/artists/create')->post('/artists', [ 'artist' => [ 'name' => $name, ], ]); // アクションURLに対してポストしてリダイレクトされること $response->assertRedirect(); $this->assertDatabaseHas('artists', ['name' => $name]); // DBにデータが作成されていることで新規作成処理を保証する } public function testCanUpdate() { $artist = factory(Artist::class)->create(); $response = $this->get('/artists/' . $artist->id . '/edit'); $response->assertOk() ->assertSee('action="/artists/' . $artist->id . '"') // 編集フォームがあること ->assertSee('name="artist[name]"'); // 名前入力フォームがあること $name = Str::random(10); $response = $this->from('/artists/' . $artist->id . '/edit')->put('/artists/' . $artist->id, [ 'artist' => [ 'name' => $name, ], ]); // アクションURLに対してポストしてリダイレクトされること $response->assertRedirect(); $this->assertDatabaseHas('artists', ['name' => $name]); // レコードに変更した名前のレコードがあることで編集処理を保証する } public function testCanDelete() { $artist = factory(Artist::class)->create(); $this->assertDatabaseHas('artists', ['id' => $artist->id]); // テスト前にレコードがDBに存在することを確認 $response = $this->from('/artists')->delete('/artists/' . $artist->id); $response->assertRedirect(); $this->assertDatabaseMissing('artists', ['id' => $artist->id]); // DELETEルートにアクセス後、DBからレコードがなくなっていることで削除処理を保証する } }最後にまとめ
上記のものではもちろんテストが十分というわけではないが、最低限書いてあると嬉しい。レビューもテスト書かれてないよりはあったほうがいい。
あと、実際のアプリケーションではもちろんこのように単純ではない場合が多いので、必要に応じて書き足す必要がある。というかこのサンプルでも色々足りていないのは承知している。
簡単に書いたつもりだが意外と時間かかった。
しかし、各モデルがしっかり使えるものであることの保証と、各ルーティングにアクセス可能であることが証明できていれば、多少手荒な更新があってもなんとかやっていけるはず。テストどう書いていいかわからない、方針がたたないような初学者の人の助けになればと思う。