PHP

Laravel 実践開発 秀和システム 演習メモ



PHPフレームワーク Laravel実践開発

Laravelやってて事前知識のつまづきが多く、基礎的な事でググる時間のコストが無駄なので、2日かけて基礎として1冊やるぞ。10日かかった。

この本は『PHPフレームワーク Laravel入門』をクリアしている中級?向けに書かれているので、最初は入門本からやるのが良いです。

最初からこの本をやると死ぬと思います。Amazonのレビューを見ると初心者に優しくなくて低評価みたいになってるようです;

本書は、2017年9月刊行の大好評『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ファサードを作成する

  1. app/Facadesディレクトリを作成
  2. 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(引数) { 処理 } );

検索結果を指定した要素数ずつ分割処理するメソッド

  1. 引数にテーブルの参照データが要素数毎に代入される
  2. クロージャで処理される
    function(引数) { 処理 }部分
  3. 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

 

コレクションに関しては下記が参考になる

 

        $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]

 

処理の流れ

  1. イベントクラスのインスタンス作成
  2. イベントの発行
  3. イベントリスナーの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 // ●重要
{

これでキューを利用してイベントを発行することができる。

  1. PersonEventが発生
  2. 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構成

  1. <template>タグの中にHTMLタグを使って内容を記述して表示を構築するテンプレート
  2. コンポーネント用スクリプト

<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

新しくどこかを作ったら、どこかが動かないとかまずいので、テストを書くのは大事だなと。

  1. テストコードで設計する
  2. 動作する処理を書く
  3. テストする

大事にしたい。

 

モデルのテスト

 

キャッシュを消しておく

$ 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

テストコード

 

 

 

 

 

 

 

爆速レンタルサーバならConoHa WING

 

サーバはプロに全部お任せ!「仕事」に専念したいあなたにおすすめです。

   

ConoHa VPSで運営してま🐱

 

サーバの勉強がしたいあなたにおすすめ!現役エンジニアの管理人が選んだ、WordPress運用に適したVPSの終着点

   

ConoHa VPSで爆速WordPressを作ろう! 

 

Laravel, AWS ポートフォリオ作成サポート! 定員3名

https://menta.work/plan/2381

Laravel, AWSによるポートフォリオ作成を通して、エンジニア転職をサポートします

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)