Laravelやってて事前知識のつまづきが多く、基礎的な事でググる時間のコストが無駄なので、2日かけて基礎として1冊やるぞ。10日かかった。
この本は『PHPフレームワーク Laravel入門』をクリアしている中級?向けに書かれているので、最初は入門本からやるのが良いです。
最初からこの本をやると死ぬと思います。Amazonのレビューを見ると初心者に優しくなくて低評価みたいになってるようです;
本書は、2017年9月刊行の大好評『PHPフレームワークLaravel入門』を補足する続編。読者の「これも知りたかった!」という声に応えました!
私には素晴らしい内容でありがたいのです。
もくじ
入門編はこっち
この本を見ながらポートフォリオを制作してアプリ会社に転職できました😊
この記事の対象読者
- Laravel勉強中の人、私とか!
- 『PHPフレームワーク Laravel実践開発』を買うか迷っている人
- 紅茶に安いウィスキーをたらして飲むのが好きな人
ざっくり内容
- ルーティング, ミドルウェア
- ファイルシステム, 画像操作
- リクエスト, レスポンス
- サービスコンテナ
- Collection
- キュー, イベント
- Vue.js フロントエンドとの連携
- テスト, モックの作成
完全に実践的な内容になっています。
ご注意
入門本をひたすらやっても『自分で何かを0から作れるようにはならない』ので、短期的にざくっと本を終わらせてから、実際に本を参考にググりながら自分で何かを作ってみるのがおすすめ。
だからあまりだらだら本をやるのはよくない、その為の2日縛り。
- 読んですらすらわかる内容はやらない。
プログラミングは暗記する必要はないこと、カンニングOK!必要になったら本やメモ、リファレンスを見ながら実装すれば良いです。 - 言語やフレームワークができることを覚える。実装方法を理解する
- 曖昧な単元のみ演習する
これで効率的に学習できます。
過去にやった おすすめ本
[amazon_link asins=’4798052582′ template=’Original’ store=’izayoi55-22′ marketplace=’JP’ link_id=’3cde6811-3100-49c6-8389-5a7fcc048388′]
就活、転職で『Laravelを利用してポートフォリオ作らなきゃ』って方はこれをまるっと1冊通してからだと進みが早いです。正誤表で一度修正してから取り組むのがおすすめです。
リファレンスの前に、サンプルコードそのままに打ち込めば動作するものをやると、動いてからどうして動くのだろう?って楽しみながら学習ができます。まず動かす、そして理解する。この順番が良い。
- まず動かす、そして理解する。
言語やフレームワークに慣れないうちはこの順番で大丈夫。
何かわからないけど動くぞ楽しい!から理解の順番 - 慣れたら、理解して動かすフェーズ
フレームワークやクラス関連の作法に少し慣れてくると
理解して、動かすができます。
この状態になるとコードが読める状態なので、本を写経しなくても良いです。
確認と記憶の定着という意味で、理解してからアウトプットとして写経するのがおすすめ。ここらへんの学び方は賛否あるのでこのへんで(๑╹ω╹๑ ) - とはいっても私は写経もする
本を読んで理解したと確認する意味で写経する。
→アウトプットすることで記憶の定着を強化する
・読んで理解してると思っても細部を理解していなかった
・本のままでは実際動かなかった - 暗記不要
私なんかはお酒で頭がぱっぱらぱーになっているので、関数の正しい表記なんかは暗記しても1週間もたないで忘れます;でもできることを覚えていたり、実装の理解をしていればググったり、関数リファレンスを見返すことで実装できます。 - 正誤表を確認してから取り組む
すごく悩んでも出来なくて・・・、本がおかしいよね?
そうなのです。普通に本のほうがおかしいことってたくさんあるのが技術書です。編集さんはIT技術者ではないのでなかなか難しい問題。出版社のサイトから正誤表を確認してから取り組もう。
ある程度なれたら
一旦読んで理解してから、写経するようにしています。写経する場合も可能な限り自分でコーディングしている感覚で動作の理解をしながら行いましょう。
演習メモ
入門を終えていることが前提で書かれている為、入門書の内容は可能な限り排除されています。
- サンプルコードがなく日本語で指示されている箇所
- 本には書いていないが、自分なりに補足してコーディングしたことで動作した箇所
- 大切なポイント
ここらへんをコーディングしながらブログにメモしていきます。このブログのメモだけ見てもなんのこっちゃって感じですが、目次的にはわかると思うので『これ知らなかったな』っていう要素があれば、本を買ってみるのが早いかな。
最低限クラス関連のエラー部分を補なって演習出来ないと、本のままでは動かせない印象。入門編の内容はショートカットされるので、ただサンプルを打ち込めば動かせる、そういう本ではないです。入門編を終えてから取り組むのが宜しいかと。書店で売られるのって初心者向けがほぼなのですが、技術書には珍しい本で本当の初心者向けではないかな。だからおすすめなんですよね!
本では既存メソッドを上書きとかしたりするのですけれど、残したいので別メソッドに定義などするなどしています。
コントローラ作成
php artisan make:controller HelloController
※私みたいにdocker-compose環境の場合は下記みたいになる。
docker-compose exec php-fpm php artisan make:controller HelloController
HTTPステータスのビューテンプレート作成
$ php artisan vendor:publish --tag=laravel-errors Copied Directory [/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/views] To [/resources/views/errors] Publishing complete.
コマンドで生成できるようにLaravelで用意してくれている。
私これ、知らなくて手作業で作った覚えがあるんだが…!?(怒)
illustrated-layoutに変更するとillustrated-layout.blade.phpを読み込むようになり、表示が綺麗になる。
{{-- @extends('errors::minimal') --}} @extends('errors::illustrated-layout') ・・・
P14
ミドルウェア作成
$ php artisan make:middleware HelloMiddleware
routes/web.php
・・・ use App\Http\Middleware\HelloMiddleware; Route::middleware([HelloMiddleware::class])->group(function(){ Route::get('/hello', 'HelloController@index'); Route::get('/hello/other', 'HelloController@other'); });
useでミドルウェアを指定しないと動かなかった。
P16
routes/web.phpで名前空間の記述をシンプルにする
Route::namespace('Sample')->group(function(){ Route::get('/sample', 'SampleController@index'); Route::get('/sample/other', 'SampleController@other'); });
下記のように書かなくて済む
Route::get('/sample', 'Sample\SampleController@index'); Route::get('/sample/other', 'Sample\SampleController@other');
何が便利なのか?
- 階層が深い名前空間を利用している場合にコントローラ@メソッドでシンプルに記述できる
P18
自分でデータを入れる必要があります。
php artisan make:model Models/Person -m php artisan make:seeder PeopleTableSeeder php artisan make:controller PersonController
/database/migrations/xxxx_create_table.php
public function up() { Schema::create('people', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('mail'); $table->integer('age'); $table->timestamps(); }); }
/database/seeds/PeopleTableSeeder.php
public function run() { DB::table('people')->insert([ 'name' => 'yamada', 'mail' => 'yamada@example.net', 'age' => 12, 'created_at' => new DateTime(), 'updated_at' => new DateTime(), ]); ・・・ }
/database/seeds/DatabaseSeeder.php
public function run() { // $this->call(UsersTableSeeder::class); $this->call(PeopleTableSeeder::class); }
マイグレーション, シーディング
$ php artisan migrate --seed
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Person; class HelloController extends Controller { public function index(Person $person) { $data = [ 'msg' => $person ]; return view('hello.index', $data); } public function other(Request $request) { $data = [ 'msg' => $request->bye ]; return view('hello.index', $data); } }
/routes/web.php
Route::get('/hello/{person}', 'HelloController@index');
ルーティングをセットする
http://localhost/hello/1
idを入れてあげることでDBアクセスするクエリビルダやEloquentの技術なしで、レコードの内容が取得されて表示されます。
ルーティングを司る/app/Providers/RouteServiceProvider.php
public function boot() { // parent::boot(); + Route::model('person', \App\Models\Person::class); }
本とは異なってModelsにモデルを格納した場合は、上記みたいにフルパス指定してあげる。
\app\Http\Controllers\HelloController.php
- public function index(Person $person) + public function index($person)
こうすることでも動作される。
/app/Providers/RouteServiceProvider.php
- Route::model('person', \App\Models\Person::class);
この記述を消すと、1が返される。
P39 ファイルの存在をチェックする
copyやremoveでエラーが出る
- 元ファイルが存在していない場合
- 複製先や移動先にファイルが存在している場合
このエラーを回避する為に、「exists」メソッドを利用して存在確認を行う。
public function other($msg) { if(Storage::disk('public')->exists('bk_' . $this->fname)) { Storage::disk('public')->delete('bk_' . $this->fname); } Storage::disk('public')->copy($this->fname, 'bk_' . $this->fname); if(Storage::disk('local')->exists('bk_') . $this->fname) { Storage::disk('local')->delete('bk_' . $this->fname); } Storage::disk('local')->move('public/bk_' . $this->fname, 'bk_' . $this->fname); return redirect()->route('hello'); }
P51 リクエストとレスポンス
P56
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\Response; use App\Models\Person; class HelloController extends Controller { public function index(Request $request, Response $response) { $msg = 'please input text:'; $keys = []; $values = []; if ($request->isMethod('post')) { $form = $request->all(); $result = '<html><body>'; foreach($form as $key => $value) { $result .= $key . ': ' . $value . "<br>"; } $result .= '</body></html>'; $response->setContent($result); return $response; } $data = [ 'msg' => $msg, 'keys' => $keys, 'values' => $values, ]; return view('hello.index', $data); }
P68 サービスとコンテナ結合
サービスコンテナ?
- 必要なクラスのインスタンスが自動的に引数として用意される機能
public function index(Request $request) { // }
- あるクラスと依存関係にあるクラスのインスタンスを管理する機能として提供
依存性注入
- 依存関係にあるクラスのインスタンスを外部からクラスに注入する
サービスコンテナ = 『Laravelに用意されているDI機能を実装しただけのクラス』と考えることができる。
P72 明示的にインスタンスを生成する
下記はすべて同じ効果で、指定したクラスのインスタンスを明示的に取得する
$myservice = app('App\MyClasses\MyService'); $myservice = app()->make('App\MyClasses\MyService'); $myservice = resolve('App\MyClasses\MyService');
ここの本質ではないけれど、
class MyService { private $id = -1; private $msg = 'no id...'; private $data = ['Hello', 'Welcome', 'Bye']; public function __construct() { } public function setId($id) { $this->id = $id; if($id >= 0 && $id < count($this->data)) { $this->msg = "select id:" . $id . ', data:"' . $this->data[$id] . '"'; } }
パラメータの値がない時にきちんとデフォルトの値を設定をしておく、大事。
シングルトン結合を行う
public function boot() { - app()->bind('App\MyClasses\MyService', + app()->singleton('App\MyClasses\MyService', function($app){ $myservice = new Myservice(); $myservice->setId(0); return $myservice; }); }
bind()からsingleton()に変更
これだけでMyServieクラスをシングルトンとして利用できるようになった。
引数を必要とする結合
MyService.php
public function __construct(int $id) { $this->setId($id); $this->serial = rand(); echo "[" . $this->serial . "]"; }
AppServiceProvider.php
public function boot() { app()->when('App\MyClasses\MyService') ->needs('$id') ->give(1); }
P86-87
- class MyService + class MyService implements MyServiceInterface { private $serial; private $id = -1; private $msg = 'no id...'; private $data = ['Hello', 'Welcome', 'Bye']; - public function __construct(int $id) + public function __construct() { - $this->setId($id); - $this->serial = rand(); - echo "[" . $this->serial . "]"; }
こういう風に変更しないと動かなかった。
結合時のイベント処理
結合時に呼び出される
app()->resolving( function($obj, $app){ 実行する処理});
特定のクラスとの結合時に呼び出される
app()->resolving( クラス, function($obj, $app){ 実行する処理});
結合イベントを利用する
public function boot() { app()->resolving(function($obj, $app){ if(is_object($obj)) { echo get_class($obj) . '<br>'; }else{ echo $obj . '<br>'; } }); app()->resolving(PowerMyService::class, function($obj, $app){ $newdata = ['ハンバーグ', 'カレーライス', '唐揚げ', '餃子']; $obj->setData($newdata); $obj->setId(rand(0, count($newdata))); }); app()->bind('App\MyClasses\MyServiceInterface', // 'App\MyClasses\MyService'); 'App\MyClasses\PowerMyService'); }
P93 ファサードの利用
サービスプロバイダを作成する
$ docker-compose exec php-fpm php artisan make:provider MyServiceProvider
/app/Providers/MyServiceProvider.php
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class MyServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { app()->singleton('App\MyClasses\MyServiceInterface', 'App\MyClasses\PowerMyService'); echo "<b>MyServiceProvider/register</b><br>"; } /** * Bootstrap services. * * @return void */ public function boot() { echo "<b>MyServiceProvider/boot</b><br>"; } }
P97-100 MyServiceファサードを作成する
- app/Facadesディレクトリを作成
- app/Facades/MyService.phpファイルを作成
app/Facades/MyService.php
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; class MyService extends Facade { protected static function getFacadeAccessor() { return 'myservice'; } }
config/app.php
'aliases' => [ + 'myservice' => App\Facades\MyService::class,
app/Providers/MyServiceProviders.php
public function register() { + app()->singleton('myservice', + 'App\MyClasses\PowerMyService'); app()->singleton('App\MyClasses\MyServiceInterface', 'App\MyClasses\PowerMyService'); echo "<b>MyServiceProvider/register</b><br>"; }
app/Http/Controllers/HelloController.php
<?php namespace App\Http\Controllers; - use App\MyClasses\MyServiceInterface; + use App\Facades\MyService; class HelloController extends Controller { - public function index(MyServiceInterface $myservice, int $id = -1) + public function index(int $id = -1) { - $myservice->setId($id); + myservice::setId($id); $data = [ - 'msg' => $myservice->say(), - 'data' => $myservice->alldata() + 'msg' => myservice::say(), + 'data' => myservice::alldata() ]; return view('hello.index', $data); } }
P101 ミドルウェアの利用
- ミドルウェアをリクエストを拡張する仕組み
- リクエストを操作するだけのもの
ミドルウェアの作成
$ docker-compose exec php-fpm php artisan make:middleware MyMiddleware
ミドルウェアの雛形
<?php namespace App\Http\Middleware; use Closure; class クラス名 { public function handle($request, Closure $next) { return $next($request); } }
handleメソッド
- handleの引数で渡されたRequestインスタンスをClosureの引数に指定して呼び出したり、戻り値をreturn
- $next($request)でRequestインスタンスを得て、これをreturnすることでクライアントにレスポンスが返される
routes/web.php
Route::get('/hello/{id}', 'HelloController@index') ->middleware(App\Http\Middleware\MyMiddleware::class); Route::get('/hello/', 'HelloController@index') ->middleware(App\Http\Middleware\MyMiddleware::class);
/app/Http/Controllers/HelloController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HelloController extends Controller { public function index(Request $request) { $data = [ 'msg' => $request->msg, 'data' => $request->alldata ]; return view('hello.index', $data); } }
beforeとafter
- ✖︎ミドルウェアはコントローラより前の処理を実行するもの
- ◎ beforeとafterがあり、リクエストが処理される前、処理された後に実行させることができる
/app/Http/Middleware/MyMiddleware.php
<?php namespace App\Http\Middleware; use Closure; use App\Facades\MyService; class MyMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { // ●beforeの処理・開始 $id = rand(0, count(MyService::alldata())); MyService::setId($id); $merge_data = [ 'id' => $id, 'msg' => MyService::say(), 'alldata' => MyService::alldata() ]; $request->merge($merge_data); // ●beforeの処理・終了 $response = $next($request); // ●after処理・開始 $content = $response->content(); $content .= '<style> body { background-color:#eef; } p { font-size:18px; } li { color: red; font-weight:bold;} </style>'; $response->setContent($content); // ●after処理・処理 return $response; } }
after処理
ここでRequestインスタンスを取り出す
$response = $next($request);
$content = $response->content(); ・・・ $contentを操作 $response->setContent($content);
- content()メソッド
コンテンツを取り出す - setContent()メソッド
コンテンツを設定する
after処理は返送される直前にコンテンツを操作するもの
ミドルウェアの利用範囲と設定
現状の知識だと、Route一つ一つに書き込まないといけずに煩雑になるという問題がある。
- すべてのRouteで指定したい時はどうするの?
- グループ単位で指定したい
こういった要件を満たしたい。
routes/web.phpで設定していたミドルウェア設定を削除してプレーンに戻す
Route::get('/hello/{id}', 'HelloController@index'); Route::get('/hello/', 'HelloController@index');
app/Http/Kernel.php
グループミドルウェア:web, apiといったグループで指定できる
protected $middlewareGroups = [ 'web' => [ ], 'api' => [ ], ];
ルートミドルウェア:ミドルウェアごとに名前を設定できる
protected $routeMiddleware = [ ];
プライオリティの設定:ミドルウェアの実行順序を指定
protected $middlewarePriority = [ ];
Aのミドルウェアが実行された後でないと、Bのミドルウェアが実行できないといった設計の時に利用します。
グローバルミドルウェアに登録する
protected $middleware = [ + \App\Http\Middleware\MyMiddleware::class ];
すべてのRouteに影響する
データベースの活用
この章は飛ばして次いこうかと思ったけれど、知らないこともありました。
$result = DB::table('people') ->where('name', 'like', '%' . $name . '%')->get();
同じ働きをするのでwhereRaw()がある
$result = DB::table('people') ->whereRaw('name like ?', ['%'.$name.'%'])->get();
Raw系を利用する場合は、パラメータを利用して、SQLインジェクションを防止すること。
最初と最後のレコードを取得する
public function index($name = "hoge") { $msg = "get people records."; $first = DB::table('people')->first(); $last = DB::table('people')->orderBy('id', 'desc')->first(); $result = [$first, $last]; $data = [ 'msg' => 'Database access', 'data' => $result ]; return view('hello.index', $data); }
idを指定してレコードを取得する find()
$result = [DB::table('people')->find($id)];
不思議だが、[]が必要。
特定のフィールドのみ取得する pluck()
$name = DB::table('people')->pluck('name');
public function index($id = -1) { $name = DB::table('people')->pluck('name'); $value = $name->toArray(); $msg = implode(',', $value); $result = DB::table('people')->get(); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
P123 chunkByIdによる分割処理
DB::table('テーブル名')->chunkById(要素数, function(引数) { 処理 } );
検索結果を指定した要素数ずつ分割処理するメソッド
- 引数にテーブルの参照データが要素数毎に代入される
- クロージャで処理される
function(引数) { 処理 }部分 - return false or true
・trueを返すと、次のレコード群がレコードに渡される
・falseを返すとchunkByIdを抜けて次に進む
レコードのIDが奇数のものだけをまとめた処理
class HelloController extends Controller { public function index($id = -1) { $data = ['msg' => '', 'data' => []]; $msg = 'get: '; $result = []; DB::table('people')->chunkById(2, function($items) use (&$msg, $result) { foreach($items as $item) { $msg .= $item->id . ' '; $result += array_merge($result, [$item]); break; } return true; }); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
useで&をつけて参照渡しをしている。
function($items) use (&$msg, $result)
これをしないと別の変数として扱われるので注意
orderByとchunkを使う
- idではなく別の基準でレコードをなら並び替え、分割処理したい場合はchunkByIdは使えない
- chunkとorderByを利用する
DB::table('people')->orderBy('name', 'asc') ->chunk(2, function($items) use (&$msg, &$result) { foreach($items as $item) { $msg .= $item->id . ': ' . $item->name; $result += array_merge($result, [$item]); break; } return true; });
一定の部分だけを抜き出して処理する
一応これでhttp://localhost/hello/{id}をすると、そのid + 3をしたidから昇順で3レコード取れるサンプルコード。アプリ側で処理するからレコード数が増えると相当重くなる気がして、実装して良いコードなのかは今の私にはわからない。
class HelloController extends Controller { public function index($id) { $data = ['msg' => '', 'data' => []]; $msg = 'get: '; $result = []; $count = 0; DB::table('people') ->chunkById(3, function($items) use (&$msg, &$result, &$id, &$count) { if($count == $id) { foreach($items as $item) { $msg .= $item->id . ': ' . $item->name . ' '; $result += array_merge($result, [$item]); } return false; } $count++; return true; }); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); } }1
AND検索
$result = DB::table('people') ->where('id', '>=', 2) ->where('id', '<=', 6) ->get();
OR検索
$result = DB::table('people') ->where('id', '<=', 2) ->orWhere('id', '>=', 6) ->get();
2つの値の範囲
<Build> ->whereBetween(フィールド名 、[最小値, 最大値])
<Build>->orWhereBetween(フィールド名 、[最小値, 最大値])
orがついたメソッドは他の条件の後にOR検索で繋げます。
2つの値の範囲外を検索
<Build> -> whereNotBetween(フィールド名 、[最小値, 最大値]) <Build> -> orWhereNotBetween(フィールド名 、[最小値, 最大値])
idが2〜5のレコードを取得する
http://localhost/hello/2,5
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class HelloController extends Controller { public function index($id) { $ids = explode(',', $id); // var_dump($ids); array(2) { [0]=> string(1) "2" [1]=> string(1) "5" } // exit(); $msg = 'get people.'; $result = DB::table('people') ->whereBetween('id', $ids) ->get(); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); } }
配列で検索値を指定
<Build>->WhereIn(フィールド名 、[値の配列]) <Build>->orWhereIn(フィールド名 、[値の配列])
idが1, 2, 5を抽出したい
http://localhost/hello/1,2,5
public function index($id) { $ids = explode(',', $id); $msg = 'get people.'; $result = DB::table('people') ->whereIn('id', $ids) ->get(); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
配列に含まれている値と等しいもの以外を検索
<Build>->WhereNotIn(フィールド名 、[値の配列]) <Build>->orWhereNotIn(フィールド名 、[値の配列])
指定フィールドがnullのものを検索
<Build>->WhereNull(フィールド名) <Build>->orWhereNull(フィールド名)
指定フィールドがnullではないものを検索
<Build>->WhereNotNull(フィールド名) <Build>->orWhereNotNull(フィールド名)
日付の値のチェック
<Build>->WhereDate(フィールド名) <Build>->WhereYear(フィールド名) <Build>->WhereMonth(フィールド名) <Build>->WhereDay(フィールド名) <Build>->WhereTime(フィールド名)
2つのフィールドの値が等しいものを検索
<Build>->WhereColumn(フィールド名1, フィールド名2) <Build>->orWhereColumn(フィールド名1, フィールド名2)
ページネーション
指定ページのレコードを得る
DB::table('people')->paginate(項目数, フィールド, ページ名, 番号);
例
DB::table('people') ->paginate(3, ['*'], 'page', $id);
public function index($id) { $msg = 'show page: ' . $id; $result = DB::table('people') ->paginate(3, ['*'], 'page', $id); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
- http://localhost/hello/?page=1
- http://localhost/hello/?page=2
public function index(Request $request) { $id = $request->query('page'); $msg = 'show page: ' . $id; $result = DB::table('people') ->paginate(3, ['*'], 'page', $id); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
Bootstrap4を有効化させる
bootstrap/app.php
+ Illuminate\Pagination\AbstractPaginator::defaultView("pagination::bootstrap-4"); return $app;
/Http/Controllers/HelloController.php
<!DOCTYPE html> <html lang="ja"> <head> <title>Index</title> + <link href="/css/app.css" rel="stylesheet"> </head> <body> <h1>Hello/Index</h1> <p>{!!$msg!!}</p> <ol> @foreach($data as $item) <li>ID:{{ $item->id }} {{ $item->name }} [{{ $item->mail }}, {{$item->age}}]</li> @endforeach </ol> + {!! $data->links() !!} <hr> </body> </html>
アクセスします
- http://localhost/hello/?page=1
- http://localhost/hello/?page=2
prev, Next表記のsimplePaginate()に変更する
$result = DB::table('people') ->simplePaginate(3);
- paginate(), simplePaginate()の引数は1つだけでもクエリパラメータで渡された値を自動で参照してくれる。
- 第一パラメータは必須。
Eloquentの利用
App/Models/Person.phpを作っていたら
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Person extends Model { // }
下記みたいに、Eloquentで呼び出すことができる。
+ use App\Models\Person; class HelloController extends Controller { public function index(Request $request) { $id = $request->query('page'); $msg = 'show page: ' . $id; - DB::table('people')->simplePaginate(3); + $result = Person::paginate(3);
カスタムページネーションリンク
App\Http\paginationフォルダを作る
App/Http/pagination/Mypagination.php
<?php namespace App\Http\Pagination; use Illuminate\Contracts\Pagination\Paginator; class MyPaginator { private $paginator; public function __construct(Paginator $paginator) { $this->paginator = $paginator; } public function link() { $prev = $this->paginator->currentPage() == 1 ? 'disabled' : ''; $next = $this->paginator->currentPage() == $this->paginator->count() ? 'disabled' : ''; $result = '<ul class="pagination" role="navigation">'; $result .= '<li class="pagi-item"' . $prev . '"><a class="page-link" href="' . $this->paginator->previousPageUrl() .'">前のページ</a></li>'; $result .= '<li class="pagi-item disabled"><a class="page-link">' . $this->paginator->currentPage() . '</a></li>'; $result .= '<li class="pagi-item"' . $next . '"><a class="page-link" href="' . $this->paginator->nextPageUrl() .'">次のページ</a></li>'; $result .= '</ul>'; return $result; } }
App/Http/Controllers/HelloController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Models\Person; use App\Http\Pagination\MyPaginator; class HelloController extends Controller { public function index(Request $request) { $id = $request->query('page'); $msg = 'show page: ' . $id; $result = Person::paginate(3); $paginator = new MyPaginator($result); $data = [ 'msg' => $msg, 'data' => $result, 'paginator' => $paginator ]; return view('hello.index', $data); } }
resources/views/hello/index.blade.php
- {!! $data->links() !!} + {!! $paginator->link() !!}
Paginatorのメソッド
- $results->count()
レコード全体のページ数を取得 - $results->currentPage()
現在のページ番号を取得 - $results->firstItem()
現在のページの最初のレコードがなんばんめのものなのかを取得 - $results->hasMorePages()
次のページがあるかどうかを返す - $results->lastItem()
現在のページの最後のレコードがなんばんめのものなのかを返す - $results->lastPage() (simplePaginateでは使用不可)
最後のページ番号を返す - $results->nextPageUrl()
次のページを表示するURLを返す - $results->perPage()
1ページあたりに表示されるレコード数を返す - $results->previousPageUrl()
前のページを表示するURLを返す - $results->total() (simplePaginateでは使用不可)
レコードの総数を返す - $results->url(ページ番号)
そのページを表示するURLを返す
Eloquent リスト表示の基本形
App/Models/Person.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Person extends Model { // }
App/Http/Controllers/HelloController.php
class HelloController extends Controller { public function index(Request $request) { $msg = 'show people record.'; $result = Person::get(); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); } }
resouces/views/hello/index.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <title>Index</title> <link href="/css/app.css" rel="stylesheet"> </head> <body> <h1>Hello/Index</h1> <p>{!!$msg!!}</p> <ol> <table> @foreach($data as $item) <tr> <th>{{ $item->id }}</th> <th>{{ $item->name }}</th> <th>{{ $item->mail }}</th> <th>{{ $item->age }}</th> </tr> @endforeach </table> </ol> <hr> </body> </html>
コレクション
- モデルから取得されたレコード類はコレクションとして返される
- Illuminate\Database\Eloquent名前空間のCollectionクラスのインスタンス
- コレクションはイテレータ機能があり、foreachなどを使ってレコードを処理できる
- レコード1つ1つが、対応するモデルクラスのインスタンスとして保管される
→モデルを利用して取得されるのはモデルクラスのインスタンス
フィルター
- filterメソッドで未成年を取得する
$result = Person::get()->filter(function($person) { return $person->age < 20; });
- rejectメソッドで未成年を排除する
$result = Person::get()->reject(function($person) { return $person->age < 20; });
rejectメソッドのクロージャの引数は、モデルクラスのインスタンスが入ります。そして処理として、対象レコードを排除して返します。
diff 差分を取得する
public function index(Request $request) { $msg = 'show people record.'; $result = Person::get()->filter(function($person) { return $person->age < 70; }); $result2 = Person::get()->filter(function($person) { return $person->age < 10; }); // 70歳以下のレコード群から、10歳以下のレコードを取り除いたもの $result3 = $result->diff($result2); $data = [ 'msg' => $msg, 'data' => $result3 ]; return view('hello.index', $data); }
P150 コレクションの機能:modelKyesとonlyおよびexcept
コレクションに関しては下記が参考になる
-
【5.5対応】Laravel の Collection を使い倒してみたくなった 〜 サンプルコード 115 連発 1/3
-
【5.5対応】Laravel の Collection を使い倒してみたくなった 〜 サンプルコード 115 連発 2/3
$keys = Person::get()->modelKeys(); $even = array_filter($keys, function($key){ return $key % 2 === 0; }); $result = Person::get()->only($even); $data = [ 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data);
配列にまとめたIDのモデルを取得して
$keys = Person::get()->modelKeys(); $even = array_filter($keys, function($key){ return $key % 2 === 0; });
idが偶数のレコードをonly()で取得
$result = Person::get()->only($even);
except()は配列にまとめたID以外のモデルを取得する。
mergeとunique
merge: 2つのコレクションを1つにまとめる
$id_even = Person::get()->filter(function($item){ return $item->id % 2 === 0; }); $age_even = Person::get()->filter(function($item){ return $item->age % 2 === 0; }); $result = $id_even->merge($age_even);
mergeでは同じidのコレクションがあった場合は上書きされるので、レコードの重複はない。
unique:はコレクションを1つにまとめて、重複したものを除いてまとめたものを返す。
map
PHPのarrya_map()と同じ働きをする。配列を加工して新しい配列を作る
$id_even = Person::get()->filter(function($item){ return $item->id % 2 === 0; }); $map = $id_even->map(function($item, $key){ return $item->id . ':' . $item->name; }); $data = [ 'msg' => $map, 'data' => $id_even ]; return view('hello.index', $data);
重要 カスタムコレクション
@see
実装方法
- モデルの中でnewCollection()を実装すると、検索結果として独自のコレクションメソッドを返すことができる。
- MyCollection()のカスタムコレクションの中で配列全体の操作や、メソッドを定義できる
- get()するだけで、newCollection()を実装していれば、自動的にコールされて新しいコレクションで検索結果が返る
/app/Http/Models/Person.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Collection; class Person extends Model { public function newCollection(array $models = []) { return new MyCollection($models); } } class MyCollection extends Collection { public function fields() { $item = $this->first(); return array_keys($item->toArray()); } }
class Person extends Model { public function newCollection(array $models = []) { return new MyCollection($models); } }
App/Http/Controllers/HelloController.php
public function index(Request $request) { $msg = 'show people record.'; $re = Person::get(); $fields = Person::get()->fields(); $data = [ 'msg' => implode(', ', $fields), 'data' => $re ]; return view('hello.index', $data); }
アクセサ
デフォルトでモデルから$this->nameみたいにしてテーブルのフィールドがプロパティとして値を取得できる。
→
テーブルのフィールドに対応するプロパティを上書きして取得できる = アクセサ
/app/Models/Person.php
class Person extends Model { public function getNameAndAgeAttribute() { return $this->name . '(' . $this->age . ')'; } }
- get<カラム名>Attribute()で定義する
- 複数の場合はAndで結んだキャメル型
getNameAndAgeAttribute() - 呼び出しは$this->name_and_ageみたいにスネーク型になる
resouces/views/hello/index.blade.php
@foreach($data as $item) <tr> <th>{{ $item->id }}</th> <th>{{ $item->name_and_age }}</th> </tr> @endforeach
既存のプロパティを変更する
/app/Models/Person.php
public function getNameAttribute($value) { return strtoupper($value); }
これでviewで$this->nameしているところは大文字になる。
ミューテータ
値を設定する処理を上書きするのがミューテータ。
/app/Models/Person.php
class Person extends Model { + protected $guarded = ['id']; //保護 + public static $rules = [ // バリデーション + 'name' => 'required', + 'mail' => 'email', + 'age' => 'integer' + ]; public function newCollection(array $models = []) { return new MyCollection($models); } public function getNameAndAgeAttribute() { return $this->name . '(' . $this->age . ')'; } public function getNameAttribute($value) { return strtoupper($value); } + public function setNameAttribute($value) // ミューテータ + { + $this->attributes['name'] = strtoupper($value); + } }
routes/web.php
Route::get('/hello/{id}', 'HelloController@index'); Route::get('/hello', 'HelloController@index')->name('hello'); + Route::get('/hello/{id}/{name}', 'HelloController@save');
ルート設定
App/Http/Controllers/HelloController.phpにsave()メソッドを追加する
public function save($id, $name) { $record = Person::find($id); // 1. id = 1のモデルを取得する $record->name = $name; // 2. id = 1のモデルのnameプロパティに値を入れる $record->save(); // 3. 保存する return redirect()->route('hello'); }
これはEloquentでの更新の基本的な流れ、抑えておく。
http://localhost/hello/1/yamada-kun2
上記でアクセスする。
データベース上はこうなっている。ミューテータによってレコード idの1が大文字で登録されている。
配列の保存
/app/Models/Person.php
public function setAllDataAttribute(Array $value) { $this->attributes['name'] = $value[0]; $this->attributes['mail'] = $value[1]; $this->attributes['age'] = $value[2]; }
App/Http/Controllers/HelloController.php
public function other() { $person = new Person(); $person->all_data = ['yudetarou','yudetarou@example.net', 42]; // ダミーデータ $person->save(); return redirect()->route('hello'); }
routes/web.php
Route::get('/other', 'HelloController@other');
JSON形式でのレコード取得(toJson)
public function json($id = -1) { if($id == -1) { return Person::get()->toJson(); // 全レコードを出力 } else{ return Person::find($id)->toJson(); } }
routes/web.php
Route::get('/hello/json', 'HelloController@json'); Route::get('/hello/json/{id}', 'HelloController@json');
JavaScriptからアクセスする
resouces/views/hello/index.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <title>Index</title> <link href="/css/app.css" rel="stylesheet"> <script> function doAction(){ var id = document.querySelector('#id').value; var xhr = new XMLHttpRequest(); xhr.open('GET', '/hello/json/' + id, true); xhr.responseType = 'json'; xhr.onload = function(e){ if(this.status == 200){ var result = this.response; document.querySelector('#name').textContent = result.name; document.querySelector('#mail').textContent = result.mail; document.querySelector('#age').textContent = result.age; } }; xhr.send(); } </script> </head> <body> <h1>Hello/Index</h1> <div> <input type="number" id="id" value="1"> <button onclick="doAction();">Click</button> </div> <ul> <li id="name"></li> <li id="mail"></li> <li id="age"></li> </ul> </body> </html>
キューとジョブ
$ docker-compose exec php-fpm php artisan make:job MyJob
app/Jobs/Myjob.phpが作成される
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; class MyJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; /** * Create a new job instance. * * @return void */ public function __construct() { // } /** * Execute the job. * * @return void */ public function handle() { + echo '<p class="myjob">THIS IS MYJOB!</p>'; } }
$ docker-compose exec php-fpm php artisan make:provider MyJobProvider
app/Providers/MyJobProvider.phpを作成する
ジョブの登録
app/Providers/MyJobProvider.php
public function register() { $this->app->bindMethod(MyJob::class.'@handle', function($job, $app){ return $job->handle(); }); }
$this->app->bindMethod(クラス::class.'@handle', クロージャ );
App/Http/Controllers/HelloController.php
+ use App\Jobs\MyJob; class HelloController extends Controller { public function index(Request $request) { MyJob::dispatch(); // ●dispatch: ジョブを発行し、キューに登録する $msg = 'show people record.'; $result = Person::get(); $data = [ 'input' => '', 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
データベースにアクセスする
app/Jobs/Myjob.php
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use App\Models\Person; class MyJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $person; /** * Create a new job instance. * * @return void */ public function __construct(Person $person) { $this->person = $person; } /** * Execute the job. * * @return void */ public function handle() { $sufix = ' [+MYJOB]'; if(strpos($this->person->name, $sufix)) { $this->person->name = str_replace( $sufix, '', $this->person->name ); }else{ $this->person->name .= $sufix; } $this->person->save(); } }
App/Http/Controllers/HelloController.php
use App\Jobs\MyJob; class HelloController extends Controller { public function index(Person $person = null) { if($person != null) { MyJob::dispatch($person); } $msg = 'show people record.'; $result = Person::get(); $data = [ 'input' => '', 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
routes/web.php
Route::get('/hello/{person}', 'HelloController@index');
http://localhost/hello/2
id =2のpeopleテーブルのレコードのnameフィールドに対して文字列が付与されたり、はずされたりする。
非同期に対応させる
キュー用テーブル, 実行失敗時テーブルの作成
$ docker-compose exec php-fpm php artisan queue:table $ docker-compose exec php-fpm php artisan queue:failed-table
作成
$ docker-compose exec php-fpm php artisan migrate
.envの修正
## キュー #QUEUE_CONNECTION=sync QUEUE_CONNECTION=database QUEUE_DRIVER=database
デフォルトではsync(同期)になっているので、databaseに変更
ワーカの実行
$ docker-compose exec php-fpm php artisan queue:work
if($person != null) { - MyJob::dispatch($person); + MyJob::dispatch($person)->delay(now()->addMinutes(3)); }
- <<PendingDispatch>>->delay(日時)
- <<PendingDispatch>>->delay(now()->addMinutes(3))
3分後に実行
※ソースコードを変更した場合はキューの再起動が必要
laravel $ docker-compose exec php-fpm php artisan queue:restart Broadcasting queue restart signal. laravel $ docker-compose exec php-fpm php artisan queue:work
http://localhost/hello/3
アクセスするとキューが登録される。
時間が経つとキューが実行される。
- php artisan queue:work
起動 - php artisan queue:restart
再起動をワーカに伝える
コードを変更した後などはキューを再起動しないと既存のキューに反映されない。 - php artisan queue:work –once
ワーカを起動してジョブを1つだけ実行する - php artisan queue:work –stop-when-empty
溜まったジョブをすべて実行 - php artisan queue:work –queue=名前
特定のキューを実行する
php artisan queue:work –queue=a,b,c
といったカンマ区切りで複数実行できる
特定のキューを指定する
if($person != null) { + $qname = $person->id % 2 == 0 ? 'even' : 'odd'; MyJob::dispatch($person)->onQueue($qname); }
- http://localhost/hello/1
・・・ - http://localhost/hello/6
アクセスしてキューを入れる
queueフィールドに名前がついていることがわかる。
odd(奇数)のキューかつ溜まったジョブをすべて実行
laravel $ docker-compose exec php-fpm php artisan queue:work --stop-when-empty --queue=odd [2019-09-13 13:48:22][3] Processing: App\Jobs\MyJob [2019-09-13 13:48:22][3] Processed: App\Jobs\MyJob [2019-09-13 13:48:23][5] Processing: App\Jobs\MyJob [2019-09-13 13:48:23][5] Processed: App\Jobs\MyJob [2019-09-13 13:48:23][7] Processing: App\Jobs\MyJob [2019-09-13 13:48:23][7] Processed: App\Jobs\MyJob
クロージャをキューに登録する
App/Http/Controllers/HelloController.php
class HelloController extends Controller { public function index(Person $person = null) { $msg = 'show people record.'; $result = Person::get(); $data = [ 'input' => '', 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); } public function send(Request $request) { $id = $request->input('id'); $person = Person::find($id); dispatch(function() use ($person){ Storage::append('person_access_log.txt', $person->all_data); }); return redirect()->route('hello'); }
resources/views/hello/index.blade.php
<form action="/hello" method="post"> @csrf ID: <input type="text" id="id" name="id"> <input type="submit"> </form>
routes/web.php
Route::get('/hello', 'HelloController@index')->name('hello'); Route::post('/hello', 'HelloController@send');
フォームからidに対応する数字を入れると、storage/app/public/person_access_log.txtにかきこれる
ERIKA(33) [erika@example.net]
イベント
- 必要な情報をまとめたオブジェクト
- 何かのイベントを作ろうとした時にそのイベントでどういう情報が必要かを考えてイベントのクラスをまとめる
イベントリスナー
- イベントの発生を監視
- イベントが発生した時にリスナーに用意されている処理が実行される。
PersonEventを登録する
app/Providers/EventServiceProvider.php
class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], + 'App\Events\PersonEvent' => [ + 'App\Listeners\PersonEventListener', + ], ];
- app/Events/PersonEvent.php
- app/Listeners/PersonEventListener.php
を生成
$ docker-compose exec php-fpm php artisan event:generate
app/Events/PersonEvent.php
<?php namespace App\Events; use Illuminate\Queue\SerializesModels; class PersonEvent { use SerializesModels; public $person; /** * Create a new event instance. * * @return void */ public function __construct(Person $person) { $this->person = $person; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('channel-name'); } }
app/Listeners/PersonEventListener.php
<?php namespace App\Listeners; use App\Events\PersonEvent; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use App\Models\Person; use Illuminate\Support\Facades\Storage; class PersonEventListener { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param PersonEvent $event * @return void */ public function handle(PersonEvent $event) { Storage::append('person_access_log.txt', '[PersonEvent] ' . now() . ' ' . $event->person->all_data); } }
app/Events/PersonEvent.php
<?php namespace App\Events; use Illuminate\Queue\SerializesModels; use App\Models\Person; class PersonEvent { use SerializesModels; public $person; /** * Create a new event instance. * * @return void */ public function __construct(Person $person) { $this->person = $person; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('channel-name'); } }
App/Http/Controllers/HelloController.php
class HelloController extends Controller { public function index(Person $person = null) { $msg = 'show people record.'; $result = Person::get(); $data = [ 'input' => '', 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); } public function send(Request $request) { $id = $request->input('id'); $person = Person::find($id); event(new PersonEvent($person)); $data = [ 'input' => '', 'msg' => 'id=' . $id, 'data' => [$person], ]; return redirect()->route('hello'); }
http://localhost/hello
フォームからidとなる数字を入力して送信すると、
storage/app/public/person_access_log.txtに追記される。
ERIKA(33) [erika@example.net] + [PersonEvent] 2019-09-13 16:29:58 SATOU(6) [satou@example.net]
処理の流れ
- イベントクラスのインスタンス作成
- イベントの発行
- イベントリスナーのhandleでイベントクラスを受け取る
購読 subscrive
購読クラスの基本
<?php namespace App\Listeners; class クラス名 { public function subscribe($events) { // 登録 } }
イベントのリッスン
$events->listen( イベントクラス, イベントリスナー );
- 第二引数にイベントリスナーのハンドラとなるメソッドを指定
- $eventsにはDispatcherクラスのインスタンスが渡される
この中のlistenメソッドを利用することでイベントのリッスンが行える
購読クラス
app/Listeners/MyEventSubscriber.php
<?php namespace App\Listeners; class MyEventSubscriber { public function subscribe($events) { $events->listen( 'App\Events\PersonEvent', 'App\Listeners\PersonEventListener@handle' ); } }
購読クラスを登録する
app/providers/EventServiceProvider.php
protected $subscribe = [ 'App\Listeners\MyEventSubscriber', ];
http://localhost/hello
アクセスしてフォームにIDに対応する番号を打ち込むと、person_access_log.txtに書き込まれることを確認。
イベントディスカバリ
app/providers/EventServiceProvider.php
public function shouldDiscoverEvents() { return true; }
このメソッドを追加すると、
- 面倒なイベントの登録処理を自動で全て登録を行ってくれる。
→記述したイベントが全て動作してしまう。 - しかし、今は使わないといったイベントもすべて動作してしまう、諸刃の剣
→手作業で登録した方が、明示的で安心できることが多い。
キューを利用してイベント発行する
use Illuminate\Contracts\Queue\ShouldQueue; // ●重要 use App\Models\Person; use Illuminate\Support\Facades\Storage; - class PersonEventListener + class PersonEventListener implements ShouldQueue // ●重要 {
これでキューを利用してイベントを発行することができる。
- PersonEventが発生
- PersonEventListenerの実行をジョブとしてキューに登録
※キュー利用するから、ワーカーが起動していないとイベントリスナーのハンドラは実行されなくなる。
ワーカーが起動していない場合
- キューがjobsに登録される
- ワーカーが起動したら実行される
ジョブを利用するか、イベントを利用するか?
ジョブ
- ジョブはそれ自体で処理を行う
- 『必要な処理を必要なタイミングで実行させるようにキューに登録する』するだけ
イベント
- イベントはリスナーに処理を委任する
- PersonEventのイベントリスナーは、PersonEventListerだけとは限らない。
→いくつもリスナーを用意しておいて、必要に応じて最適なリスナーのハンドラを実行することができる。
※ジョブをキューに登録するのとは大きく違う - サブスクライブのように、必要な一連のイベントをまとめて購読するなど、イベントの発生とハンドラ実行の設定が色々と用意されている
発生するタイミングと実行する処理を組み合わせる必要がある場合
=> 基本として、イベントを利用する。
タスクとスケジューラ
スクリプトなどを実行させる。
スクリプト作成
$ vi mycmd.sh #!/bin/sh echo "[$(date)] This is MyCmd.sh." >> mycmd_log.txt
実行権限付与
$ chmod +x mycmd.sh
/app/Console/Kernel.php
protected function schedule(Schedule $schedule) { // $schedule->command('inspire') // ->hourly(); + $schedule->exec('./mycmd.sh'); }
実行する
$ docker-compose exec php-fpm php artisan schedule:run Running scheduled command: ./mycmd.sh > '/dev/null' 2>&1
実行された。
定期的に自動実行登録するにはLinux側でCronに登録する
$ vi /etc/crontab * * * * * root php <プロジェクトルートパス>/artisan schedule:run 1 >> /dev/null 2>&1
溜まったキューを実行する
protected function schedule(Schedule $schedule) { $schedule->command('queue:work --stop-when-empty'); }
$ docker-compose exec php-fpm php artisan schedule:run Running scheduled command: '/usr/local/bin/php' 'artisan' queue:work --stop-when-empty > '/dev/null' 2>&1
クロージャで処理を実行する
$schedule->call( クロージャ );
callは引数に指定した具体的な実行する処理を記述したクロージャを実行する。
/app/Console/Kernel.php
+ use App\Models\Person; + use App\Jobs\MyJob; ・・・ protected function schedule(Schedule $schedule) { $count = Person::all()->count(); $id = rand(0, $count) + 1; $schedule->call(function() use ($id) { $person = Person::find($id); MyJob::dispatch($person); }); }
$ docker-compose exec php-fpm php artisan schedule:run Running scheduled command: Closure
invoke実装クラスをcallする
『__invoke』マジックメソッドをクラスに実装することで、インスタンスそのものをcallで実行させることができる。
/app/Console/Kernel.php
+ use App\Models\Person; + use App\Jobs\MyJob; + use Illuminate\Support\Facades\Storage; ・・・ class Kernel extends ConsoleKernel { protected function schedule(Schedule $schedule) { $count = Person::all()->count(); $id = rand(0, $count) + 1; $obj = new ScheduleObj($id); $schedule->call($obj); } ・・・ // 追記 class ScheduleObj { private $person; public function __construct($id) { $this->person = Person::find($id); } public function __invoke() { Storage::append('person_access_log.txt', $this->person->all_data); MyJob::dispatch($this->person); return 'true'; } }
ジョブをinvoke化する
app/Jobs/Myjob.php
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use App\Models\Person; use Illuminate\Support\Facades\Storage; class MyJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $person; public function getPersonId() { return $this->person->id; } /** * Create a new job instance. * * @return void */ public function __construct($id) { $this->person = Person::find($id)->first(); } public function __invoke() { $this->handle(); } /** * Execute the job. * * @return void */ public function handle() { $this->doJob(); } public function doJob() { $sufix = ' [+MYJOB]'; if(strpos($this->person->name, $sufix)) { $this->person->name = str_replace( $sufix, '', $this->person->name ); }else{ $this->person->name .= $sufix; } $this->person->save(); Storage::append('person_access_log.txt', $this->person->all_data); } }
/app/Console/Kernel.php
protected function schedule(Schedule $schedule) { $count = Person::all()->count(); $id = rand(0, $count) + 1; /* インスタンス実行 ※直接処理だけを実行する場合 $schedule->call(new MyJob($id)); */ /* // ディスパッチする ※dispatchでキューを利用して処理する場合 $schedule->call(function() use($id){ MyJob::dispatch($id); }); */ }
どちらでも実行できるが、$schedule->call(new MyJob($id));の方が使いやすい。
実行
$ docker-compose exec php-fpm php artisan schedule:run Running scheduled command: Closure
ジョブメソッドによるジョブ実行
単純にジョブをディスパッチしたいだけの時
$schedule->job(ジョブ, キュー)
/app/Console/Kernel.php
protected function schedule(Schedule $schedule) { $count = Person::all()->count(); $id = rand(0, $count) + 1; $schedule->job(new MyJob($id)); }
単純にjobを実行したいなら、callよりjobを利用する。
フロントエンドとの連携
npm, node.jsを入れる為に、PHP-FPMのDockerfileを修正した
FROM php:7-fpm ENV DEBIAN_FRONTEND noninteractive ## Timezon ENV TZ Asia/Tokyo RUN echo "${TZ}" > /etc/timezone \ && dpkg-reconfigure -f noninteractive tzdata ## Basic Install RUN apt-get update && apt-get install -y git zlib1g-dev zip unzip libzip-dev RUN docker-php-ext-install zip mysqli pdo_mysql ## npm install RUN apt-get install -y gnupg npm RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - RUN apt-get install -y nodejs RUN npm update -g npm RUN npm i -g npm RUN npm cache verify RUN npm install ## Permission RUN mkdir -p /app ADD ./ /app WORKDIR /app RUN usermod -u 1000 www-data RUN groupmod -g 1000 www-data RUN chown -R www-data:www-data /app ## Deploy Laravel Libs by Composer ENV COMPOSER_ALLOW_SUPERUSER 1 ENV COMPOSER_HOME /composer ENV PATH $PATH:/composer/vendor/bin RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN composer global require hirak/prestissimo RUN cp .env.example .env RUN composer install RUN php artisan key:generate RUN php artisan cache:clear RUN php artisan config:clear RUN php artisan route:clear RUN php artisan view:clear RUN composer dump-autoload RUN php artisan clear-compiled ## Laravel Permission RUN chmod -R a+w storage/ bootstrap/cache RUN chown -R www-data:www-data /app/storage RUN chmod -R 775 /app/storage
上記のDockerfileでビルドした。
プロジェクトをビルドする
$ docker-compose exec php-fpm npm run dev
- /resources/js/components/ExampleComponent.vueファイルが作成される。
- /resources/js/components/配下にコンポーネントを用意すると自動的に認識されて使えるようになる
resources/views/hello/index.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <title>Index</title> <link href="{{ mix('css/app.css') }}" rel="stylesheet" type="text/css"> <meta name="csrf-token" content="{{ csrf_token() }}"> </head> <body style="padding:10px"> <h1>Hello/Index</h1> <p>{{ $msg }}</p> <div id="app"> <example-component></example-component> </div> <script src=" mix('js/app.js') "></script> </body> </html>
/resources/js/components/ExampleComponent.vue
<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Example Component</div> <div class="card-body"> I'm an example component. </div> </div> </div> </div> </div> </template> <script> export default { mounted() { console.log('Component mounted.') } } </script>
Vue.jsのコンポートネントは2構成
- <template>タグの中にHTMLタグを使って内容を記述して表示を構築するテンプレート
- コンポーネント用スクリプト
<template>の中に内容を記述して、<script>タグに必要な処理を追加する
ビルドして監視
$ docker-compose exec php-fpm npm run dev $ docker-compose exec php-fpm npm run watch
http://localhost/hello/
アクセスする
コンポーネントを作成する
resources/js/components/MyComponent.vue作成
<template> <div class="container"> <p>{{ msg }}</p> <hr> <input type="text" v-model="name"> <button v-on:click="doAction">click</button> </div> </template> <script> export default { data:function(){ return { msg:'please your name:', name:'', }; }, methods:{ doAction:function(){ this.msg = 'Hello, ' + this.name + '!!'; } } } </script>
resources/js/app.js
Vue.component('example-component', require('./components/ExampleComponent.vue').default); + Vue.component('my-component', require('./components/Mycomponent.vue').default);
resources/views/hello/index.blade.php
<div id="app"> - <example-component></example-component> + <My-component></My-component> </div>
$ docker-compose exec php-fpm npm run dev
http://localhost/hello/
アクセスする
axiosでJSONデータを取得する
resources/js/components/MyComponent.vue
<template> <div class="container"> <p>{{ msg }}</p> <hr> <ul> <li v-for="(person,key) in people"> {{person.id}}: {{person.name}} [{{person.mail}}] ({{person.age}}) </li> </ul> </div> </template> <script> const axios = require('axios'); export default { mounted(){ axios.get('/hello/json') .then(response => { this.people = response.data; this.msg = "get data"; }); }, data:function(){ return { msg:'wail...', name:'', people:[] }; }, methods:{ doAction:function(){ this.msg = 'Hello, ' + this.name + '!!'; } } } </script>
axios.get()
mounted(){ axios.get('/hello/json') .then(response => { this.people = response.data; this.msg = "get data"; }); },
- get()は引数に指定したアドレスにGETアクセスをする。
- アクセス後の処理はthen()でクロージャで処理される。クロージャの引数にはサーバからのレスポンス情報を管理するresponseオブジェクトが渡される
JSONデータの場合、そのままJavaScriptオブジェクトとして取り出すことができる
v-for
<li v-for="(person,key) in people"> {{person.id}}: {{person.name}} [{{person.mail}}] ({{person.age}}) </li>
peopleから順に値を取得して、繰り返し処理をします。
App/Http/Controllers/HelloController.php
public function json(int $id = -1) { if($id = -1) { return Person::get()->toJson(); } else { return Person::find($id)->toJson(); } }
http://localhost/hello/
一瞬遅れて非同期でリストが表示される
Laravel側はアクションを書くだけ
- Laravel側
データの取得と出力の処理を書く - フロントエンド
サーバへのアクセスと表示を書く
ReactとAngularも同じように紹介されていたが、飛ばすぜ!
ユニットテスト
- プログラム本体
/vendor/bin/phpunit - 設定ファイル
プロジェクトルートにphpunit.xml - スクリプト
testsディレクトリにunit, featureと別れてスクリプトがある
testsディレクトリ配下のスクリプト
- tests/Unit
unit(単体)テストの基本と言えるもの。可能な限り小さな単位でテストを行う、そうしたテストを記述するためのもの。 - tests/Feature
多数のオブジェクトが組み合わせられるようなテストを行う
Featureディレクトリ配下にunitテストのスクリプトを書いても動く。開発者が識別しやすくするもの。
tests/Unit/ExampleTest.php
/helloにアクセスして200が返ってくるか
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function testBasicTest() { - $this->assertTrue(true); + //$this->assertTrue(true); + $response = $this->get('/hello'); + $response->assertStatus(200); } }
テスト実行
laravel $ docker-compose exec php-fpm vendor/bin/phpunit PHPUnit 7.5.15 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 2.15 seconds, Memory: 16.00 MB OK (2 tests, 2 assertions) laravel $ docker-compose exec php-fpm vendor/bin/phpunit PHPUnit 7.5.15 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 2.95 seconds, Memory: 16.00 MB OK (2 tests, 2 assertions)
コントローラのテスト
URIやパラメータを入れて期待した動作をするかをテストするテスト。
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function testBasicTest() { $this->get('/')->assertStatus(200); $this->get('/hello')->assertOk(); //$this->post('/hello')->assertOk(); $this->get('/hello/1')->assertOk(); $this->get('/hoge')->assertStatus(404); $this->get('/hello')->assertSeeText('Index'); $this->get('/hello')->assertSee('<h1>'); $this->get('/hello')->assertSeeInOrder(['<html','<head','<body','<h1>']); $this->get('/hello/json/1')->assertSeeText('YAMADA'); $this->get('/hello/json/2')->assertExactJson( ["id"=>2,"name"=>"SATOU","mail"=>"satou@example.net","age"=>6, "created_at"=>"2019-09-15 06:24:40","updated_at"=>"2019-09-15 06:24:40"] ); } }
テスト実行
laravel $ docker-compose exec php-fpm vendor/bin/phpunit
新しくどこかを作ったら、どこかが動かないとかまずいので、テストを書くのは大事だなと。
- テストコードで設計する
- 動作する処理を書く
- テストする
大事にしたい。
モデルのテスト
キャッシュを消しておく
$ docker-compose exec php-fpm php artisan config:clear
mysql80コンテナのDBに『appdb_testing』データベースを追加しておく。
phpunit.xml
<php> <server name="APP_ENV" value="testing"/> <server name="BCRYPT_ROUNDS" value="4"/> <server name="CACHE_DRIVER" value="array"/> <server name="MAIL_DRIVER" value="array"/> <server name="QUEUE_CONNECTION" value="sync"/> + <env name="DB_DATABASE" value="appdb_testing"/> </php>
テス用データベースの指定をします。envとなっているので注意。
.env.testing作成
APP_ENV=testing APP_KEY=base64:pFUKh3mDrUeCwaSwRqHE76oTnYLMxynCWeACaWcSpw8= APP_DEBUG=true APP_URL=http://localhost ## ログチャネル 開発:develop 本番:production LOG_CHANNEL=develop ## テスト用DB ●重要 DB_CONNECTION=mysql DB_HOST=mysql80 DB_PORT=3306 DB_DATABASE=appdb_testing DB_USERNAME=root DB_PASSWORD=secret ## Redis CACHE_DRIVER=redis SESSION_DRIVER=redis REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 REDIS_READ_WRITE_TIMEOUT=60 ## キュー #QUEUE_CONNECTION=sync QUEUE_CONNECTION=database QUEUE_DRIVER=database
テストの記述
tests/Unit/ExampleTest.php
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; use App\Models\Person; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function testBasicTest() { $this->get('/')->assertStatus(200); $this->get('/hello')->assertOk(); //$this->post('/hello')->assertOk(); $this->get('/hello/1')->assertOk(); $this->get('/hoge')->assertStatus(404); $this->get('/hello')->assertSeeText('Index'); $this->get('/hello')->assertSee('<h1>'); $this->get('/hello')->assertSeeInOrder(['<html','<head','<body','<h1>']); $this->get('/hello/json/1')->assertSeeText('YAMADA'); $this->get('/hello/json/1')->assertExactJson( ["id"=>1,"name"=>"YAMADA","mail"=>"yamada@example.net","age"=>12, "created_at"=>"2019-09-15 06:24:40","updated_at"=>"2019-09-15 06:24:40"] ); } public function testPersonModel() { $data = [ 'id' => 1, 'name' => 'yamada', 'mail' => 'yamada@example.net', 'age' => '12', "created_at"=>"2019-09-15 06:24:40", "updated_at"=>"2019-09-15 06:24:40" ]; $this->assertDatabaseHas('people', $data); $dummy_data = [ 'name' => 'DUMMY', 'mail' => 'dummy@example.net', 'age' => 0 ]; $person = new Person(); $person->fill($dummy_data)->save(); $this->assertDatabaseHas('people', $dummy_data); $person->name = 'NOT-DUMMY'; $person->save(); $this->assertDatabaseMissing('people', $dummy_data); // 存在しないことをチェック $dummy_data['name'] = 'NOT-DUMMY'; $this->assertDatabaseHas('people', $dummy_data); $person->delete(); $this->assertDatabaseMissing('people', $dummy_data); } }
マイグレーションとシーディング
$ docker-compose exec php-fpm php artisan migrate:refresh --seed --env=testing
テストの実行
laravel $ docker-compose exec php-fpm vendor/bin/phpunit PHPUnit 7.5.15 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 4.84 seconds, Memory: 18.00 MB OK (3 tests, 15 assertions)
ファクトリの利用
ファクトリを作成する
$ docker-compose exec php-fpm php artisan make:factory PersonFactory
database/factories/PersonFactory.phpが作成される
<?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Models\Person; use Faker\Generator as Faker; $factory->define(Person::class, function (Faker $faker) { return [ + 'name' => $faker->name, + 'mail' => $faker->email, + 'age' => $faker->numberBetween(1,100), ]; });
use のモデルのパスやらモデル名などを自動出力されたファイルから修正が必要
fakerのメソッドはたくさんので、『Laravel 実践開発』とかネットを見て参照すると良いです。
@see
Factoryを利用したテストクラスを作成
public function testPersonFactory() { for($i = 0;$i < 100;$i++) { factory(Person::class)->create(); } $count = Person::get()->count(); $person = Person::find(rand(1, $count)); $data = $person->toArray(); print_r($data); $this->assertDatabaseHas('people', $data); $person->delete(); $this->assertDatabaseMissing('people', $data); }
Factoryを利用して100レコード作成
for($i = 0;$i < 100;$i++) { factory(Person::class)->create(); }
テスト実行
$ docker-compose exec php-fpm vendor/bin/phpunit
ステートを設定する
単純な形
$factory->state(モデルクラス, 名前, 連想配列);
複雑な形
$factory->state(モデルクラス, 名前, function($faker){ return 連想配列; });
ステートの実行
<<モデル>>->state(名前)
database/factories/PersonFactory.phpに追記
$factory->state(Person::class, 'upper', function($faker){ return [ 'name' => strtoupper($faker->name()) ]; }); $factory->state(Person::class, 'lower', function($faker){ return [ 'name' => strtolower($faker->name()) ]; });
tests/Unit/ExampleTest.php
public function testState() { $list = []; for($i = 0;$i < 10;$i++) { $p1 = factory(Person::class)->create(); $p2 = factory(Person::class)->state('upper')->create(); $p3 = factory(Person::class)->state('lower')->create(); $p4 = factory(Person::class) ->state('upper') ->state('lower') ->create(); $list = array_merge($list, [ $p1->id, $p2->id, $p3->id, $p4->id ]); } for($i = 0;$i < 10;$i++) { shuffle($list); $item = array_shift($list); $person = Person::find($item); $data = $person->toArray(); print_r($data); $this->assertDatabaseHas('people', $data); $person->delete(); $this->assertDatabaseMissing('people', $data); } }
コールバックの設定
モデル保存後のステート実行後の処理
$factory->afterCreatingState(モデルクラス, クロージャ);
クロージャ関数の定義
function (モデルインスタンス, $faker) { ...コールバック処理 }
database/factories/PersonFactory.phpに追記
// モデル作成後の処理 $factory->afterMaking(Person::class, function ($person, $faker){ $person->name .= ' [making]'; $person->save(); }); // モデル保存後の処理 $factory->afterCreating(Person::class, function ($person, $faker){ $person->name .= ' [creating]'; $person->save(); }); // モデル作成後のステート実行後の処理 $factory->afterMakingState(person::class, 'upper', function ($person, $faker) { $person->name .= ' [making state]'; $person->save(); }); // モデル保存後のステート実行後の処理 $factory->afterMakingState(person::class, 'lower', function ($person, $faker) { $person->name .= ' [creating state]'; $person->save(); });
tests/Unit/ExampleTest.php
laravel $ docker-compose exec php-fpm vendor/bin/phpunit PHPUnit 7.5.15 by Sebastian Bergmann and contributors. ...Array ( [id] => 897 [name] => ANISSA LANGWORTH [MAKING] [CREATING] [mail] => mccullough.jamarcus@sauer.org [age] => 20 [created_at] => 2019-09-22 02:59:57 [updated_at] => 2019-09-22 02:59:57 ) .Array ( [id] => 1082 [name] => RUSS BARTELL [MAKING] [CREATING] [mail] => orrin.zboncak@miller.com [age] => 83 [created_at] => 2019-09-22 03:00:16 [updated_at] => 2019-09-22 03:00:16 ) ・・・(略)
モックの活用
ジョブをテストする
フェイク機能を作動させる
Bus::fake();
ジョブがディスパッチされているかをチェック
Bus::assertDispatched(ジョブクラス);
ジョブがディスパッチされていないことをチェック
Bus::assertNotDispatched(ジョブクラス);
app/Models/Person
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Collection; class Person extends Model { //protected $guarded = ['id']; protected $fillable= ['id','name','mail','age', 'created_at', 'updated_at'];
Personモデルを書き換え
tests/Unit/ExampleTest.php
<?php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; use App\Models\Person; use Illuminate\Support\Facades\Bus; use App\Jobs\MyJob; class ExampleTest extends TestCase { use RefreshDatabase; /** * A basic test example. * * @return void */ public function testBasicTest() { $data = [ 'id' => 1, 'name' => 'yamada', 'mail' => 'yamada@example.net', 'age' => '12', "created_at"=>"2019-09-15 06:24:40", "updated_at"=>"2019-09-15 06:24:40" ]; $person = new Person(); $person->fill($data)->save(); $this->get('/')->assertStatus(200); $this->get('/hello')->assertOk(); //$this->post('/hello')->assertOk(); $this->get('/hello/1')->assertOk(); $this->get('/hoge')->assertStatus(404); $this->get('/hello')->assertSeeText('Index'); $this->get('/hello')->assertSee('<h1>'); $this->get('/hello')->assertSeeInOrder(['<html','<head','<body','<h1>']); $this->get('/hello/json/1')->assertSeeText('YAMADA'); $this->get('/hello/json/1')->assertExactJson( ["id"=>1,"name"=>"YAMADA","mail"=>"yamada@example.net","age"=>12, "created_at"=>"2019-09-15 06:24:40","updated_at"=>"2019-09-15 06:24:40"] ); } public function testPersonModel() { $dummy_data = [ 'name' => 'DUMMY', 'mail' => 'dummy@example.net', 'age' => 0 ]; $person = new Person(); $person->fill($dummy_data)->save(); $this->assertDatabaseHas('people', $dummy_data); $person->name = 'NOT-DUMMY'; $person->save(); $this->assertDatabaseMissing('people', $dummy_data); // 存在しないことをチェック $dummy_data['name'] = 'NOT-DUMMY'; $this->assertDatabaseHas('people', $dummy_data); $person->delete(); $this->assertDatabaseMissing('people', $dummy_data); } public function testPersonFactory() { for($i = 0;$i < 100;$i++) { factory(Person::class)->create(); } $count = Person::get()->count(); $person = Person::find(rand(1, $count)); $data = $person->toArray(); print_r($data); $this->assertDatabaseHas('people', $data); $person->delete(); $this->assertDatabaseMissing('people', $data); } public function testState() { $list = []; for($i = 0;$i < 10;$i++) { $p1 = factory(Person::class)->create(); $p2 = factory(Person::class)->state('upper')->create(); $p3 = factory(Person::class)->state('lower')->create(); $p4 = factory(Person::class) ->state('upper') ->state('lower') ->create(); $list = array_merge($list, [ $p1->id, $p2->id, $p3->id, $p4->id ]); } for($i = 0;$i < 10;$i++) { shuffle($list); $item = array_shift($list); $person = Person::find($item); $data = $person->toArray(); print_r($data); $this->assertDatabaseHas('people', $data); $person->delete(); $this->assertDatabaseMissing('people', $data); } } public function testMyJob() { $id = 10002; $data = [ 'id' => $id, 'name' => 'DUMMY', 'mail' => 'dummy@mail.com', 'age' => 0 ]; $person = new Person(); $person->fill($data)->save(); $this->assertDatabaseHas('people', $data); Bus::fake(); Bus::assertNotDispatched(MyJob::class); MyJob::dispatch($id); Bus::assertDispatched(MyJob::class); } }
use RefreshDatabase;
- テスト前
リフレッシュしてマイグレーションとシーディング - テストが終わる
データの削除
便利!
クロージャでディスパッチ状況をチェック
assertDispatched()はディスパッチされているかだけでなく、ディスパッチされたジョブがどのような状態なのかもチェックできる。
Bus::assertDispatched(ジョブクラス, function($job){ ...実行する処理 return 戻り値; });
tests/Unit/ExampleTest.phpに追記
public function testDispatched() { $id = 10003; $data = [ 'id' => $id, 'name' => 'DUMMY2', 'mail' => 'dummy2@mail.com', 'age' => 0 ]; $person = new Person(); $person->fill($data)->save(); $this->assertDatabaseHas('people', $data); Bus::fake(); MyJob::dispatch($id); Bus::assertDispatched(MyJob::class, function($job) use ($id) { $p = Person::find($id)->first(); return $job->getPersonId() == $p->id; }); }
Person::find($id)で得たPersonインスタンスのidと、MyJobで得たgetPersonId()でインスタンスのidを取得して、idが等しいかをチェックする。
テスト実行
$ docker-compose exec php-fpm vendor/bin/phpunit
イベントをテストする
- ジョブと似た役割でイベントがある。
- Busと同様にfakeメソッドが用意されている。
イベントでのフェイク機能を実行
Event::fake();
イベントが発行されていることをチェック
Event::assertDispatched(イベントクラス);
イベントが発行されていないことをチェック
Event::assertNotDispatched(イベントクラス);
tests/Unit/ExampleTest.phpに追記
public function testPersonEvent() { factory(Person::class)->create(); $person = factory(Person::class)->create(); Event::fake(); Event::assertNotDispatched(PersonEvent::class); event(new PersonEvent($person)); Event::assertDispatched(PersonEvent::class); Event::assertDispatched(PersonEvent::class, function($event) use ($person){ return $event->person === $person; ←●ポイント }); }
return $event->person === $person;
同じであるか確認している。同じであればtrue, 異なればfalseを返す。trueあればテストを通過させる。
コントローラでイベントを発行させる
app/Controllers/HellowControllerl.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use App\Models\Person; use App\Http\Pagination\MyPaginator; use App\Jobs\MyJob; use Illuminate\Support\Facades\Storage; use App\Events\PersonEvent; class HelloController extends Controller { public function index(int $id = null) { if($id !== null) { event(PersonEvent::class); $result = Person::find($id); } else { $result = Person::get(); } $msg = 'show people record.'; $data = [ 'input' => '', 'msg' => $msg, 'data' => $result, ]; return view('hello.index', $data); }
tests/Unit/ExampleTest.phpに追記
public function testPersonEventId() { factory(Person::class)->create(); $person = factory(Person::class)->create(); Event::fake(); $this->get('/hello/' . $person->id)->assertOk(); Event::assertDispatched(PersonEvent::class); }
キューをテストする
フェイク機能の起動
Queue::fake();
指定のジョブが追加されていることを確認
Queue::assertPushed(ジョブクラス);
指定のジョブが追加されていないことを確認
Queue::assertNothingPushed(ジョブクラス);
追加されている個数を確認
Queue::assertPushed(ジョブクラス, 個数);
クロージャで具体的な処理を用意
Queue::assertPushed(ジョブクラス, クロージャ);
クロージャを利用する場合は、クロージャの引数にジョブが渡される。
MyJobとPersonEventでキューをテスト
tests/Unit/ExampleTest.phpに追記
public function testQueue() { factory(Person::class)->create(); $person = factory(Person::class)->create(); Queue::fake(); Queue::assertNothingPushed(); MyJob::dispatch($person->id); Queue::assertPushed(MyJob::class); event(PersonEvent::class); $this->get('/hello/' . $person->id)->assertOk(); Queue::assertPushed(CallQueuedListener::class, 2); Queue::assertPushed(CallQueuedListener::class, function($job) { return $job->class === PersonEventListener::class; }); }
テスト実行
$ docker-compose exec php-fpm vendor/bin/phpunit
リスナーの種類を確認
Queue::assertPushed(CallQueuedListener::class, function($job) { return $job->class === PersonEventListener::class; });
CallQueuedListenerインスタンスとPersonEventListener::classが同一かをチェックしています。
特定のキューを調べるには?
Queue::assertPushedOn(名前, クラス);
tests/Unit/ExampleTest.phpに追記
public function testQueueSpecific() { factory(Person::class)->create(); $person = factory(Person::class)->create(); Queue::fake(); Queue::assertNothingPushed(); MyJob::dispatch($person->id)->onQueue('myjob'); Queue::assertPushed(MyJob::class); Queue::assertPushedOn('myjob', MyJob::class); }
テスト実行
$ docker-compose exec php-fpm vendor/bin/phpunit
サービスをテストする
app/Http/Controllers/HelloController.php
+ use App\MyClasses\PowerMyService; public function index(PowerMyService $service) { $service->setId(1); $msg = $service->say(); $result = Person::get(); $data = [ 'input' => '', 'msg' => $msg, 'data' => $result ]; return view('hello.index', $data); }
resources/views/hello/index.blade.php
public function testPowerMyService() { $response = $this->get('/hello'); $content = $response->getContent(); echo $content; $response->assertSeeText( '1番のりんごですね!', $content ); }
クラスをモックする
サービスが組み込まれ、実行されている過程を検証したい場合、必ずしも実際のサービスクラスが必要となるわけではない。テスト用にフェイククラスを用意し、それを使って『サービスが組み込まれ、メソッドが呼び出されている』ということを確認する。
Mockey
特定のクラスのフェイクとなるもの(モック)を作成し、そのインスタンスを組み込んで本来のクラスに置き換える機能がある。
モックを作成
$mock = Mockey::mock(PowerMyService);
モックをクラスに設定する
$this->instance(PowerMyService::class, $mock)
tests/Unit/ExampleTest.phpに追記
public function testPowerMyService() { $msg = '1番のりんごですね!'; $response = $this->get('/hello'); $content = $response->getContent(); echo $content; $response->assertSeeText( $msg, $content ); } public function testPowerMyServiceByMock() { $msg = '*** OK ***'; $mock = Mockery::mock(PowerMyService::class); $mock->shouldReceive('setId') ->withArgs([1]) ->once() ->andReturn(null); $mock->shouldReceive('say') ->once() ->andReturn($msg); $this->instance(PowerMyService::class, $mock); $response = $this->get('/hello'); $content = $response->getContent(); $response->assertSeeText($msg, $content); }
使いこなせてなくて、、$msg = ‘iiuiui’;に適当に文字列を指定しても$response->assertSeeText($msg, $content);でテストに通ってしまうんよな。
ここではまってしまっていて。現状では私は制御できていないので、テストする場合はMockeyは利用しないようにする。
- shouldReceive
メソッド名を指定 - withArgs
引数が必要な場合はこれで設定。引数は、値を配列にまとめたもの - once
一度だけメソッドを呼び出す - andReturn
戻り値が必要な場合はこれで設定する。引数に戻り値を用意する
エラーメモ
一度イメージを決して、docker-compose up -dした時に立ち上がらなくなった。
app/Console/Kernel.php
protected function schedule(Schedule $schedule) { - $count = Person::all()->count(); - $id = rand(0, $count) + 1; - $schedule->job(new MyJob($id)); + //$count = Person::all()->count(); + //$id = rand(0, $count) + 1; + //$schedule->job(new MyJob($id)); }
kernel系でEloquentとかDB接続系行ってる場合は、php-fpmコンテナが起動できずエラーが出た。一旦無効化することで対応
おすすめ書籍
@see
テストコード