- 投稿日:2020-04-26T23:09:20+09:00
Laravelのミドルウェアをテストする方法を考えてみた
テストするときにミドルウェアを無効化する記事はあっても、ミドルウェアそのものをテストする記事があまり無かったので、書いてみます。
前提
- Laravel 6.18.10
- PHP 7.3.15
ミドルウェア
ここでは、リクエストに含まれるバージョンをチェックするミドルウェアを作ってみます。1 API という前提で書きますが、基本的な考え方は Web でも同じだと思います。
リクエストに含まれている
version
がMIN_VERSION
2 以上である場合は、許可します。一方、そもそも
version
が送信されなかったり、version
がMIN_VERSION
よりも小さい場合は、エラーの JSON レスポンスを返します。3namespace App\Http\Middleware; use Closure; class VersionCheck { /** * 利用可能な最低バージョン */ private const MIN_VERSION = '1.0.1'; /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $version = $request->get('version'); if ($version === null || $version < self::MIN_VERSION) { return response()->json([ 'message' => 'このバージョンは利用できません。' ], 400); } return $next($request); }テスト
ミドルウェアが出来たので、テストコードを作っていきます。
正常系
まずは、リクエストを作ります。
$request = app()->make('request'); $request->merge(['version' => '1.0.1']);そして、ミドルウェアを実行します。
$middleware = new VersionCheck(); $middleware->handle($request, function () { $this->assertTrue(true); });本来、
handle()
の第2引数は、バージョンチェックの次に動くミドルウェアを指定します。このことは、ミドルウェアの雛形を見ると、よく分かるかと思います。
public function handle($request, Closure $next) { // ... // 処理が全部終わったら $next で渡された関数を実行する。 return $next($request); }そのため、
handle()
の第2引数の中でassertTrue(true)
と書くことで、 バージョンチェックが全て終了して次のミドルウェアへ進むこと をテストすることができます。しかし、これではまだ不十分です。 エラーレスポンスが返ってこないこと をテストできていないからです。
バージョンチェックが正常な場合、本来ならば、ミドルウェアの後に動くコントローラがレスポンスを返します。
しかし、ここではミドルウェアしか動かしていないため、レスポンスは返ってきません。 つまり、
handle()
の戻り値がnull
であることをチェックすれば十分ということになります。ということで、正常系のテストコードは以下のようになります。
/** * @test */ public function 利用可能なバージョン(最低バージョンと等しい)である場合は正常終了すること() { $request = app()->make('request'); $request->merge(['version' => '1.0.1']); $middleware = new VersionCheck(); $response = $middleware->handle($request, function () { $this->assertTrue(true); }); // エラーレスポンスが返却されないこと $this->assertNull($response); }異常系
正常系と違って、異常系ではエラーレスポンスのテストが必要になります。
ここで問題となるのは、
$response->assertStatus()
のようなメソッドが 使えない ということです。通常、コントローラのテストコードで
get()
やpost()
を記載すると、自動的にTestResponse
クラスのインスタンスが返却されます。assertStatus()
はTestResponse
クラスのメソッドなので、特に意識することなく使うことができます。ところが、ミドルウェアの
handle()
はTestResponse
ではなくResponse
クラスのインスタンスを返却します。そのため、assertStatus()
を使うことはできません。もちろん、以下のように書くことはできます。
$this->assertSame(400, $response->getStatusCode()); $this->assertSame([ 'message' => 'このバージョンは利用できません。' ], json_decode($response->getContent(), true));しかし、これは、かなり冗長に感じられます。
そこで、
get()
やpost()
がどのように動いていくかを見ていきました。その結果、トレイト
Illuminate\Foundation\Testing\Concerns\MakesHttpRequests
のcreateTestResponse()
を呼び出して、Response
をTestResponse
に格納し直していることが分かりました。そして、テストクラスの親クラス
Illuminate\Foundation\Testing\TestCase
はMakesHttpRequests
をuse
しているので、テストクラスからもcreateTestResponse()
を呼び出せることが分かりました。こうして、テストコードはこのように書き直すことができました。
$middleware = new VersionCheck(); $originalResponse = $middleware->handle($request, function () { $this->assertTrue(true); }); $response = $this->createTestResponse($originalResponse); $this->assertNotNull($response); $response->assertStatus(400); $response->assertExactJson([ 'message' => 'このバージョンは利用できません。' ]);完成形
namespace Tests\Feature\Middleware; use Tests\TestCase; use App\Http\Middleware\VersionCheck; class VersionCheckTest extends TestCase { /** * @test */ public function 利用可能なバージョン(最低バージョンと等しい)である場合は正常終了すること() { $request = app()->make('request'); $request->merge(['version' => '1.0.1']); $middleware = new VersionCheck(); $response = $middleware->handle($request, function () { $this->assertTrue(true); }); // エラーレスポンスが返却されないこと $this->assertNull($response); } /** * @test */ public function 利用可能なバージョン(最低バージョンより大きい)である場合は正常終了すること() { $request = app()->make('request'); $request->merge(['version' => '1.0.2']); $middleware = new VersionCheck(); $response = $middleware->handle($request, function () { $this->assertTrue(true); }); // エラーレスポンスが返却されないこと $this->assertNull($response); } /** * @test */ public function バージョンが送信されない場合はエラーレスポンスが返却されること() { $request = app()->make('request'); $middleware = new VersionCheck(); $originalResponse = $middleware->handle($request, function () { $this->assertTrue(true); }); $response = $this->createTestResponse($originalResponse); // エラーレスポンスが返却されること $this->assertNotNull($response); // ステータスコードが400であること $response->assertStatus(400); // JSONレスポンスが期待どおりであること $response->assertExactJson([ 'message' => 'このバージョンは利用できません。' ]); } /** * @test */ public function 利用不可能なバージョンである場合はエラーレスポンスが返却されること() { $request = app()->make('request'); $request->merge(['version' => '1.0.0']); $middleware = new VersionCheck(); $originalResponse = $middleware->handle($request, function () { $this->assertTrue(true); }); $response = $this->createTestResponse($originalResponse); // エラーレスポンスが返却されること $this->assertNotNull($response); // ステータスコードが400であること $response->assertStatus(400); // JSONレスポンスが期待どおりであること $response->assertExactJson([ 'message' => 'このバージョンは利用できません。' ]); }感想
ミドルウェアは便利な反面、どのように書くべきかという「ベストプラクティス」を、あまり見つけることができませんでした。
「もっと良い方法があるよ!」という方は、こっそり教えていただければと思います。
参考
- Laravel 公式ドキュメント
- Testing Middleware in Laravel with PHPUnit
- 投稿日:2020-04-26T22:14:03+09:00
Pterodactyl Panelで、2FAコードが取得できなくなってログインできなくなった時の処置
パスワードを再発行しても、2FA認証は解除されない
2FA認証が出来ない状況でもパスワードの再発行はできましたが、新しいパスワードでログインを試行しても2FAコードが要求されてしまう。
コントロールパネルには2FA設定を解除する項目が無かった
解決策
職人(artisan)に頼み込んで、強制的に2FA認証を取り消す
php artisan p:user:disable2fa --email=user@example.com※チュートリアルページにあるように{}を付けると、
Too many arguments, expected arguments "command".と怒られてしまう。
環境
Version Information =================== Panel Version 0.7.17 Latest Version 0.7.17 Up-to-Date Yes Unique Identifier user@example.com参考リンク
- 投稿日:2020-04-26T21:42:58+09:00
MAMP環境にLarabelをインストールする方法
①MAMPのhtdocsにディレクトリ移動
ターミナルを開き、下記コマンドを入力。
$ cd /Applications/MAMP/htdocs②Larabelプロジェクト作成
htdocs配下において、下記コマンドを入力。
公文) $ composer create-project laravel/laravel プロジェクト名 --prefer-dist "6.0.*" 例) $ composer create-project laravel/laravel larabel_test --prefer-dist "6.0.*"③対象プロジェクトのディレクトリに移動
htdocs配下において、下記コマンドを入力。
公文) $ cd プロジェクト名 例) $ cd larabel_test④サーバ起動
③で作成したディレクトリ配下において、下記コマンドを入力。
$ php artisan serve⑤サーバ起動確認
ブラウザを開き、下記にアクセス。
http://127.0.0.1:8000⑥サーバ停止
「control」 + 「c」 を同時に押下
- 投稿日:2020-04-26T18:21:14+09:00
PHPの入力、送信フォームでファイルを送信するとPOSTが全部消える問題
PHPにて会員登録フォームを作り、ファイルも一緒に乗っけていざ送信をしようとすると、全てのPOSTデータが消えてしまう問題に直面しました。
具体的には、下記のようにinputタグで入力フォームを作ります。
その際にinput type="file" としてファイルも送信できるようにします。
またファイル以外の各inputタグのvalueには、POSTのデータをリダイレクトされた場合でも表示できるようプログラム記入しています。ここで画像の拡張子以外を弾くプログラムを記入し、Zipなどは入れないように、下記のエラーメッセージが出るようにするのが目標です。
しかしとあるZipファイルを入れてしまった場合、同じページにリダイレクトされるとPOSTデータが全て消えてしまいました。
初めは仮として上げたZipそのものが悪いと思っていました。
結論としてはそれは誤りで、ファイルの添付容量が大きすぎると、データを受け付けてくれず無効化されるようです。php.iniを確認してアップロードの最大容量を確認する
PHPにはアップロードの最大容量が決まっています。
その容量を超えるものは受け入れてくれません。
それを確認するには、php.iniを見に行き、そこに書かれている以下の文を探します。php.ini; Maximum allowed size for uploaded files. upload_max_filesize = 32Mここに書かれている容量を超えた場合、アップロードは無効化されてしまいます。
まとめ
php.iniに書かれている最大容量より下であれば、ファイルは受けることができて問題なく機能してくれます。
今回例として上げたZipファイルは150MBあったので、受け付けてくれなかったようです。
- 投稿日:2020-04-26T18:08:37+09:00
Laravel7 Bladeファイルのひな形を作成するmakeコマンドを自作する
概要
前回の記事Laravel7 makeコマンドのひな形をカスタマイズするでスタブのカスタマイズは行えましたが、makeコマンドを追加したい時の例をご紹介します。
環境
- Laravel 7.5.1
make:blade コマンド
今回は
make:blade
コマンドを自作します。
他のmake系コマンドはGeneratorCommand
クラスを継承しているのでそれに倣って作成します。https://laravel.com/api/7.x/Illuminate/Console/GeneratorCommand.html
app/Console/Commands/BladeMakeCommand.php
ファイルに以下の内容を記述します。app/Console/Commands/BladeMakeCommand.php<?php declare(strict_types=1); namespace App\Console\Commands; use Illuminate\Console\GeneratorCommand; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Symfony\Component\Console\Input\InputOption; class BladeMakeCommand extends GeneratorCommand { /** * The console command name. * * @var string */ protected $name = 'make:blade'; /** * The console command description. * * @var string */ protected $description = 'Create a new blade file'; /** * Execute the console command. * * @return mixed * @throws FileNotFoundException */ public function handle() { if (parent::handle() === false && ! $this->option('force')) { return false; } return true; } /** * Parse the class name and format according to the root namespace. * * @param string $name * @return string */ protected function qualifyClass($name): string { return $name; } /** * Get the destination class path. * * @param string $name * @return string */ protected function getPath($name): string { return $this->laravel->basePath('resources/views') . '/' . str_replace('\\', '/', $name).'.blade.php'; } /** * Get the stub file for the generator. * * @return string * @throws FileNotFoundException */ protected function getStub(): string { if (file_exists($customPath = $this->laravel->basePath('stubs/blade.stub'))) { return $customPath; } throw new FileNotFoundException('stubs/blade.stub file not found.'); } /** * Get the console command options. * * @return array */ protected function getOptions(): array { return [ ['force', null, InputOption::VALUE_NONE, 'Create the class even if the model already exists'], ]; } }
stubs/blade.stub
ファイルに以下の内容を記述します。stubs/blade.stub@extends('layouts.app') @section('content') @endsectionmake:blade コマンドの使い方
$ php artisan make:blade helloresources/views/hello.blade.php@extends('layouts.app') @section('content') @endsection
/
で区切るとディレクトリ切って作成できます。$ php artisan make:blade foo/barresources/views/foo/bar.blade.php@extends('layouts.app') @section('content') @endsection
- 投稿日:2020-04-26T17:54:10+09:00
Laravel7 makeコマンドのひな形をカスタマイズする
概要
Laravel7以降からStubのカスタマイズを簡単に行える機能が追加されました。
makeコマンドで使用される元のひな形ファイルを簡単にカスタマイズできて便利なのでご紹介します。環境
- Laravel 7.5.1
モデルのひな形を生成するコマンド
$ php artisan make:model Postapp/Post.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { // }makeコマンドを実行するとこのようなファイルが生成されます。
このひな形の内容を変えたい場合は次のようにします。スタブファイルの公開
Laravel7以降では
stub:publish
コマンドが新たに追加されました。$ php artisan stub:publish Stubs published successfully.実行すると次のファイル群が生成されます。
stubs/console.stub stubs/controller.api.stub stubs/controller.invokable.stub stubs/controller.model.api.stub stubs/controller.model.stub stubs/controller.nested.api.stub stubs/controller.nested.stub stubs/controller.plain.stub stubs/controller.stub stubs/factory.stub stubs/job.queued.stub stubs/job.stub stubs/middleware.stub stubs/migration.create.stub stubs/migration.stub stubs/migration.update.stub stubs/model.pivot.stub stubs/model.stub stubs/policy.plain.stub stubs/policy.stub stubs/request.stub stubs/rule.stub stubs/seeder.stub stubs/test.stub stubs/test.unit.stubmake:model のカスタマイズ
stubs/model.stub
ファイルを見ると次のようにスタブファイルが定義されています。stubs/model.stub<?php namespace {{ namespace }}; use Illuminate\Database\Eloquent\Model; class {{ class }} extends Model { // }このファイルを書き換えてあげると
make:model
コマンドに反映されます。参考
- 投稿日:2020-04-26T17:54:10+09:00
Laravel7 makeコマンドのひな形ファイルをカスタマイズする
概要
Laravel7以降からStubのカスタマイズを簡単に行える機能が追加されました。
makeコマンドで使用される元のひな形ファイルを簡単にカスタマイズできて便利なのでご紹介します。環境
- Laravel 7.5.1
モデルのひな形を生成するコマンド
$ php artisan make:model Postapp/Post.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { // }makeコマンドを実行するとこのようなファイルが生成されます。
このひな形の内容を変えたい場合は次のようにします。スタブファイルの公開
Laravel7以降では
stub:publish
コマンドが新たに追加されました。$ php artisan stub:publish Stubs published successfully.実行すると次のファイル群が生成されます。
stubs/console.stub stubs/controller.api.stub stubs/controller.invokable.stub stubs/controller.model.api.stub stubs/controller.model.stub stubs/controller.nested.api.stub stubs/controller.nested.stub stubs/controller.plain.stub stubs/controller.stub stubs/factory.stub stubs/job.queued.stub stubs/job.stub stubs/middleware.stub stubs/migration.create.stub stubs/migration.stub stubs/migration.update.stub stubs/model.pivot.stub stubs/model.stub stubs/policy.plain.stub stubs/policy.stub stubs/request.stub stubs/rule.stub stubs/seeder.stub stubs/test.stub stubs/test.unit.stubmake:model のカスタマイズ
stubs/model.stub
ファイルを見ると次のようにスタブファイルが定義されています。stubs/model.stub<?php namespace {{ namespace }}; use Illuminate\Database\Eloquent\Model; class {{ class }} extends Model { // }このファイルを書き換えてあげると
make:model
コマンドに反映されます。参考
- 投稿日:2020-04-26T17:40:45+09:00
phpアプリをherokuにデプロイ→エラー 私がハマったポイント。
「おいおいにもデプロイできないのかよ、、、」と自分に幻滅していましたが、粘ってエラー解決しました。
自分用で書き残します。sqlファイルの読み込み
docker環境で開発していた際に使っていたDBをsqlファイルとしてエクスポートし、これをherokuでも使いたいなーと思っていました。
herokuのDBはpostgreSQLしかないもんだと思っていたので、postgreSQLを使おうとしていたのですが、これではMySQLが使えないのですね。
DBに関する知識に疎く、ここで時間を食ってしまいました。どうやらherokuにおいてMySQLを使うならclearDBらしい。というわけでこれを使いました。
clearDBをアドオンに追加したらば、
http://hhmmm.hateblo.jp/entry/2016/02/15/204638
を参考にSequel Proと連携。sqlファイルもカンタンに読み込めましたし、DBの操作・閲覧がカンタンにできるようになりました。ログイン機能が使えない
ログイン機能を付したアプリケーションでしたが、ログインをするとエラーに。
????とおもってずっと苦戦していましたが、どうやらherokuではmb_strlen()
が使えないらしい。
バリデーションチェックのときにこの関数を使っていたため、エラーになったのだとか。
やはり、開発中はini_set('display_errors', 0);
にしておくべきですね;;最初はDBへの接続がうまくいかないものだと勝手に思い込んでしまい、環境変数が間違ってるんじゃないかーとかそもそもDBと連携できてないんじゃないかーとかいらんことばっかりやっていました。
これで解決しました。↓
https://qiita.com/taro-hida/items/f677abe2bc3b689002b3
- 投稿日:2020-04-26T17:33:33+09:00
Laravelで ErrorException Creating default object from empty valueというエラー
エラーの原因
色々なケースでこのエラーは起こり得ますが、Laravelでこのエラーが出た時に多いパターンのひとつに「Soft Delete = 論理削除したデータ」を取ってこようとしてnullを取ってきてしまい、エラーになっているというものがあります。
Product.php// 実際のコード // Productモデルにはソフトデリートが定義されている namespace App\Models; use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { use SoftDeletes; } // 加えてproductsテーブルにはdeleted_atのカラムに値が入っているProductController.php// id=1 のプロダクトが論理削除されている場合 // コントローラなどで該当のデータを取って来ようとすると... $product = Product::find(1); $product->name; // 実行結果 ErrorException Creating default object from empty valueつまり
$product->name
や大元の$product
がnullになっているのに、それらに対して何かしらの操作をしようとして上記のエラーになる場合があります。ということでDBからデータを引っ張ってくる時は、
- 該当のテーブルは論理削除を設定していないか?
- 該当のデータが論理削除されていないか?
を確認してみて下さい。論理削除
テーブルに
deleted_at
というカラムを持たせ、ここに日時が入ってるレコードは削除されたものとみなす。違う名前で削除フラグ
のようなカラムを持たせる場合もあるが、Laravelのデフォルトではdeleted_at
のカラムがその役割を担う。
このようなカラムがなく、データベースから完全にデータを削除する場合は物理削除
と表現する。
(ご存知とは思いますが念の為。)対策
このようなエラーを回避するには「論理削除のデータも含めて取得する」という操作が必要なので下記のように記載すれば大丈夫です。
$product = Product::withTrashed()->find(1);データにアクセスする際
withTrashed()
を一緒に書けば、論理削除されたデータも検索して引っ張ってきてくれ、ErrorException : Creating default object from empty value
というエラーにはなりません。参考URL
【Laravel5.8】Creating default object from empty valueを解決したい
stdClassをnewしたらPHP Warning: Creating default object from empty value inとなったときの対応方法
PHP Warning: Creating default object from empty valueの原因と対応
【php】 Creating default object from empty value
- 投稿日:2020-04-26T16:56:08+09:00
Mac Laravel 500 server errorが出た話
目的
- 環境構築直後にローカル開発環境で500 server errorが出た話をまとめる
エラー概要
$ php artisan serve
を実行してブラウザでhttp://127.0.0.1:8000/を確認したところ500 server error(下記のもの)が出力された。解決法
- アプリ名ディレクトリを確認すると.envが存在していないことがわかった。
下記コマンドを実行して.envを作成する。
$ cd アプリ名ディレクトリ $ cp .env.example .envlaravelアプリを再起動してブラウザを確認したところ下記の画面が表示されていた。
- アプリケーションを暗号化するためのキーがない、もしくは設定されていないらしい。
下記コマンドを実行して鍵を作成した。
$ php artisan key:generate >Application key set successfully.laravelアプリを再起動してブラウザを確認したところ下記の画面が表示されており、問題は解決した。
- 投稿日:2020-04-26T16:40:50+09:00
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その9「Kernel::handle()」
INDEX
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!
- その1「マイグレーションファイルを見てみよう」
- その2「$app['db']って何者?」
- その3「Repositoryクラス」
- その4「Larabelアプリケーションの初期化の流れ」
- その5「リポジトリの読込」
- その6「データベース接続」
- その7「スキーマビルダー」
- その8「migrate コマンドの実行」
- その9「Kernel::handle()」
Kernel::handle()
前回は
Kernel::handle()
がコールされた際に渡される引数
Symfony\Component\Console\Input\ArgvInput
Symfony\Component\Console\Output\ConsoleOutput
を軽く見てみました。
では、handle()
メソッドを見てみましょう。Illuminate\Foundation\Console\Kernel::handle()/** * Run the console application. * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface|null $output * @return int */ public function handle($input, $output = null) { try { $this->bootstrap(); return $this->getArtisan()->run($input, $output); } catch (Throwable $e) { $this->reportException($e); $this->renderException($output, $e); return 1; } } /** * Get the Artisan application instance. * * @return \Illuminate\Console\Application */ protected function getArtisan() { if (is_null($this->artisan)) { return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version())) ->resolveCommands($this->commands); } return $this->artisan; }Illuminate\Foundation\Console\Kernel::handle()
第一引数は
Symfony\Component\Console\Input\ArgvInput
インスタンスです。
第二引数はSymfony\Component\Console\Output\ConsoleOutput
インスタンスです。
戻り値は整数型です。まず
bootstrap()
メソッドがコールされます。ここは以前読みましたのでもう理解できています。
次にgetArtisan()
メソッドがコールされます。
getArtisan()
メソッドの戻り値はIlluminate\Console\Application
インスタンスです。
getArtisan()
メソッドは$this->artisan
がnull
かどうか検証します。null
でなければそれを返します。null
ならば、アプリケーション、イベント、バージョンを渡しArtisan
インスタンスを生成し、resolveCommands()
を実行した戻り値を返します。
Artisan
の実体はIlluminate\Console\Application
です。見てみましょう。Illuminate\Console\Application::__construct()/** * Create a new Artisan console application. * * @param \Illuminate\Contracts\Container\Container $laravel * @param \Illuminate\Contracts\Events\Dispatcher $events * @param string $version * @return void */ public function __construct(Container $laravel, Dispatcher $events, $version) { parent::__construct('Laravel Framework', $version); $this->laravel = $laravel; $this->events = $events; $this->setAutoExit(false); $this->setCatchExceptions(false); $this->events->dispatch(new ArtisanStarting($this)); $this->bootstrap(); } /** * Bootstrap the console application. * * @return void */ protected function bootstrap() { foreach (static::$bootstrappers as $bootstrapper) { $bootstrapper($this); } }Symfony\Component\Console\Application::__construct()|関連メソッドpublic function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; $this->terminal = new Terminal(); $this->defaultCommand = 'list'; } /** * Sets whether to automatically exit after a command execution or not. */ public function setAutoExit(bool $boolean) { $this->autoExit = $boolean; } /** * Sets whether to catch exceptions or not during commands execution. */ public function setCatchExceptions(bool $boolean) { $this->catchExceptions = $boolean; }Illuminate\Console\Application::__construct()
第一引数はアプリケーションコンテナです。
第二引数はイベントディスパッチャーです。
第三引数はストリング型でバージョン番号です。
戻り値はありません。まず、スーパークラス
Symfony\Component\Console\Application
のコンストラクタをアプリケーション名「Laravel Framework
」とバージョン情報を引数にコールします。
Symfony\Component\Console\Application
のコンストラクタは受け取ったアプリケーション名とバージョン名を変数に代入し、$this->terminal
にTerminal
インスタンスを生成したものを代入し、デフォルトコマンドをセットします。Terminal
にコンストラクタはありません。保持ファンクションの一覧を見てみましょう。Symfony\Component\Console\Terminal変数|ファンクション一覧private static $width; private static $height; private static $stty; public function getWidth() public function getHeight() private static function initDimensions() private static function hasVt100Support(): bool private static function initDimensionsUsingStty() private static function getConsoleMode(): ?array private static function getSttyColumns(): ?string private static function readFromProcess(string $command): ?stringSymfony\Component\Console\Terminal
縦幅横幅の取得、
Stty
が利用可能か、サイズの初期化、VT100
をサポートしているか、Stty
利用時のサイズ初期化、mode CON
が使用可能か(コマンドプロンプトウィンドウの変更)。Stty
の行数の取得、コマンドの実行、等のメソッドを実装しているようです。端末に関することを担うクラスのようです。
Illuminate\Console\Application::__construct()
に戻ります。
アプリケーションコンテナとイベントディスパッチャーを変数に代入します。
setAutoExit()
メソッドをfalse
を渡しコールします。
setAutoExit()
メソッドは$this->autoExit
をセットします。
コマンド実行後に自動的に終了するかどうかを設定するパラメータのようです。
setCatchExceptions()
メソッドをfalse
を渡しコールします。
setCatchExceptions()
メソッドは$this->catchExceptions
をセットします。
コマンドの実行中に例外をキャッチするかどうかを設定するパラメータのようです。
次にイベントディスパッチャーにArtisanStarting
を引数に$this
を渡して生成したものを登録します。
ArtisanStarting
クラスは非常に小さな定義のクラスです。Illuminate\Console\Events\ArtisanStarting<?php namespace Illuminate\Console\Events; class ArtisanStarting { /** * The Artisan application instance. * * @var \Illuminate\Console\Application */ public $artisan; /** * Create a new event instance. * * @param \Illuminate\Console\Application $artisan * @return void */ public function __construct($artisan) { $this->artisan = $artisan; } }コンストラクタでコンソールアプリケーションを受け取り
$this->artisan
に代入します。
Illuminate\Console\Application::__construct()
の続きです。
bootstrap()
メソッドをコールしています。
bootstrap()
メソッドは、foreach
でstatic::$bootstrappers
を回して$bootstrapper()
を引数に自身を渡してコールしています。static::$bootstrappers
は生成の過程で仕込まれている様子はありませんでした。おそらくKernel
が初期化される工程で準備されるのでしょう。
Kernel
初期化の流れの中でApplication::registerConfiguredProviders()
がコールされます。そこでは$this->config['app.providers']
つまり、PROJECT_ROOT/config/app.php
で定義されている配列のキーproviders
つまりプロバイダーのリストを引数にProviderRepository::load()
をコールしていました。その処理の中で、アプリケーションコンテナのregister()
に引数としてプロバイダーを一つずつ渡し、そこでプロバダー自体のregister()
がコールされます。
$this->config['app.providers']
の中にはIlluminate\Foundation\Providers\ConsoleSupportServiceProvider
が含まれます。
こちらを見てみましょう。Illuminate\Foundation\Providers\ConsoleSupportServiceProvider<?php namespace Illuminate\Foundation\Providers; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Database\MigrationServiceProvider; use Illuminate\Support\AggregateServiceProvider; class ConsoleSupportServiceProvider extends AggregateServiceProvider implements DeferrableProvider { /** * The provider class names. * * @var array */ protected $providers = [ ArtisanServiceProvider::class, MigrationServiceProvider::class, ComposerServiceProvider::class, ]; }コンストラクタも
register()
も見当たりません。
$providers
が定義されているだけです。その中にArtisanServiceProvider
とそれっぽいクラスが記述されています。
ConsoleSupportServiceProvider
クラスはIlluminate\Support\AggregateServiceProvider
クラスを継承しています。こちらを見てみましょう。Illuminate\Support\AggregateServiceProviderL::register()/** * Register the service provider. * * @return void */ public function register() { $this->instances = []; foreach ($this->providers as $provider) { $this->instances[] = $this->app->register($provider); } }
register()
メソッドがありました。
ConsoleSupportServiceProvider
でオーバーライドした$providers
をforeach
で回してアプリケーションコンテナのregister()
でプロバイダーを登録して$this->instances[]
に入れいていく処理です。
ということは、ArtisanServiceProvider
インスタンスがアプリケーションコンテナにプロバイダー登録され、自身のregister()
メソッドがコールされるはずです。
見てみましょう。Illuminate\Foundation\Providers\ArtisanServiceProvider::register()|関連メソッドprotected $commands = [ 'CacheClear' => 'command.cache.clear', 'CacheForget' => 'command.cache.forget', 'ClearCompiled' => 'command.clear-compiled', 'ClearResets' => 'command.auth.resets.clear', 'ConfigCache' => 'command.config.cache', 'ConfigClear' => 'command.config.clear', 'DbWipe' => 'command.db.wipe', 'Down' => 'command.down', 'Environment' => 'command.environment', 'EventCache' => 'command.event.cache', 'EventClear' => 'command.event.clear', 'EventList' => 'command.event.list', 'KeyGenerate' => 'command.key.generate', 'Optimize' => 'command.optimize', 'OptimizeClear' => 'command.optimize.clear', 'PackageDiscover' => 'command.package.discover', 'QueueFailed' => 'command.queue.failed', 'QueueFlush' => 'command.queue.flush', 'QueueForget' => 'command.queue.forget', 'QueueListen' => 'command.queue.listen', 'QueueRestart' => 'command.queue.restart', 'QueueRetry' => 'command.queue.retry', 'QueueWork' => 'command.queue.work', 'RouteCache' => 'command.route.cache', 'RouteClear' => 'command.route.clear', 'RouteList' => 'command.route.list', 'Seed' => 'command.seed', 'ScheduleFinish' => ScheduleFinishCommand::class, 'ScheduleRun' => ScheduleRunCommand::class, 'StorageLink' => 'command.storage.link', 'Up' => 'command.up', 'ViewCache' => 'command.view.cache', 'ViewClear' => 'command.view.clear', ]; /** * The commands to be registered. * * @var array */ protected $devCommands = [ 'CacheTable' => 'command.cache.table', 'ChannelMake' => 'command.channel.make', 'ComponentMake' => 'command.component.make', 'ConsoleMake' => 'command.console.make', 'ControllerMake' => 'command.controller.make', 'EventGenerate' => 'command.event.generate', 'EventMake' => 'command.event.make', 'ExceptionMake' => 'command.exception.make', 'FactoryMake' => 'command.factory.make', 'JobMake' => 'command.job.make', 'ListenerMake' => 'command.listener.make', 'MailMake' => 'command.mail.make', 'MiddlewareMake' => 'command.middleware.make', 'ModelMake' => 'command.model.make', 'NotificationMake' => 'command.notification.make', 'NotificationTable' => 'command.notification.table', 'ObserverMake' => 'command.observer.make', 'PolicyMake' => 'command.policy.make', 'ProviderMake' => 'command.provider.make', 'QueueFailedTable' => 'command.queue.failed-table', 'QueueTable' => 'command.queue.table', 'RequestMake' => 'command.request.make', 'ResourceMake' => 'command.resource.make', 'RuleMake' => 'command.rule.make', 'SeederMake' => 'command.seeder.make', 'SessionTable' => 'command.session.table', 'Serve' => 'command.serve', 'StubPublish' => 'command.stub.publish', 'TestMake' => 'command.test.make', 'VendorPublish' => 'command.vendor.publish', ]; /** * Register the service provider. * * @return void */ public function register() { $this->registerCommands(array_merge( $this->commands, $this->devCommands )); } /** * Register the given commands. * * @param array $commands * @return void */ protected function registerCommands(array $commands) { foreach (array_keys($commands) as $command) { call_user_func_array([$this, "register{$command}Command"], []); } $this->commands(array_values($commands)); }Illuminate\Support\ServiceProvider::commands()/** * Register the package's custom Artisan commands. * * @param array|mixed $commands * @return void */ public function commands($commands) { $commands = is_array($commands) ? $commands : func_get_args(); Artisan::starting(function ($artisan) use ($commands) { $artisan->resolveCommands($commands); }); }
$commands
と$devCommands
に連想配列が沢山代入されています。
register()
メソッドはregisterCommands()
に$commands
と$devCommands
をarray_merge
したものを渡してコールしています。
registerCommands()
メソッドは受け取った配列をarray_keys
でキーのみ配列で取り出し、それをforeach
で回し、call_user_func_array
でコールバック関数を実行します。実行するコールバックは自身に定義してある関数で、関数名はregister{$command}Command
で生成されたものです。引数は空の配列となっています。
このクラスにはクロージャーをアプリケーションコンテナにシングルトンで結合するメソッドが沢山定義されてます。
以下は例です。定義例/** * Register the command. * * @return void */ protected function registerUpCommand() { $this->app->singleton('command.up', function () { return new UpCommand; }); }アプリケーションコンテナに
command.up
という名前でUpCommand
インスタンスを生成して返すクロージャーをシングルトン結合しています。
$commands
と$devCommands
に代入されている沢山のコマンドがシングルトン結合されます。
次にcommands()
メソッドを$commands
をarray_values
で値のみの配列にしたものを引数にコールします。
Artisan::starting()
をクロージャーを引数に渡してコールしています。Artisan
はIlluminate\Console\Application
のことです。見てみましょう。Illuminate\Console\Application::starting()/** * Register a console "starting" bootstrapper. * * @param \Closure $callback * @return void */ public static function starting(Closure $callback) { static::$bootstrappers[] = $callback; }Illuminate\Console\Application::starting()
第一引数はクロージャーです。
戻り値はありません。
static::$bootstrappers[]
配列に受け取ったクロージャーを登録しています。
先程疑問だった、static::$bootstrappers
に格納されている中身の正体がわかりました。
static::$bootstrappers[]
に登録されたクロージャー全てに$this
を渡したものをアプリケーションコンテナにシングルトン結合するという手順になります。せっかくここまで読んだので、
Artisan::starting()
メソッドに引数として渡されているクロージャーも読んでみましょう。Artisan::starting(function ($artisan) use ($commands) { $artisan->resolveCommands($commands); });このクロージャーをコールする
Artisan::bootstrap()
では引数として$this
を渡しています。つまり、$this->resolveCommands(コマンド名)
が実行されるクロージャーです。resolveCommands()
を見てみましょう。Illuminate\Console\Application::resolveCommands()|関連メソッド/** * Resolve an array of commands through the application. * * @param array|mixed $commands * @return $this */ public function resolveCommands($commands) { $commands = is_array($commands) ? $commands : func_get_args(); foreach ($commands as $command) { $this->resolve($command); } return $this; } /** * Add a command, resolving through the application. * * @param string $command * @return \Symfony\Component\Console\Command\Command */ public function resolve($command) { return $this->add($this->laravel->make($command)); } /** * Add a command to the console. * * @param \Symfony\Component\Console\Command\Command $command * @return \Symfony\Component\Console\Command\Command */ public function add(SymfonyCommand $command) { if ($command instanceof Command) { $command->setLaravel($this->laravel); } return $this->addToParent($command); } /** * Set the Laravel application instance. * * @param \Illuminate\Contracts\Container\Container $laravel * @return void */ public function setLaravel($laravel) { $this->laravel = $laravel; } /** * Add the command to the parent instance. * * @param \Symfony\Component\Console\Command\Command $command * @return \Symfony\Component\Console\Command\Command */ protected function addToParent(SymfonyCommand $command) { return parent::add($command); }Illuminate\Console\Application::resolveCommands()
第一引数は コマンドです。
戻り値は $this です。受け取ったコマンドを
foreach
で回し、resolve()
メソッドを引数にコマンド名を渡してコールし、自身を返しています。
resolve()
はコマンド名を受け取りそれをアプリケーションコンテナでmake()
した戻り値をadd()
しています。
add()
メソッドでは、引数として渡されたコマンドがCommand
インスタンス か検証し、そうであった場合はsetLaravel()
メソッドにアプリケションコンテナを引数に渡しコールします。
そうでない場合はaddToParent()
メソッドを通し、スーパークラスのadd()
メソッドをコールします。
setLaravel()
メソッドはコマンドの変数$laravel
にアプリケーションコンテナを代入します。
スーパークラスのadd()
メソッドとはSymfony\Component\Console\Application::add()
の事です。
Symfony\Component\Console\Application::add()
が引数として受け取るのはSymfony\Component\Console\Command\Command
インスタンスで、これはIlluminate\Console\Command
のスーパークラスです。つまりIlluminate\Console\Command
はSymfony\Component\Console\Command\Command
をLaravel用に拡張したコマンドクラスなのでしょう。
Symfony\Component\Console\Application
を読みたいところですが、少しボリュームがあるので、まずは本筋に戻ります。
Illuminate\Foundation\Console\Kernel::handle()
から$this->getArtisan()
がコールされその処理の途中でした。Illuminate\Foundation\Console\Kernel::getArtisan()/** * Get the Artisan application instance. * * @return \Illuminate\Console\Application */ protected function getArtisan() { if (is_null($this->artisan)) { return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version())) ->resolveCommands($this->commands); } return $this->artisan; }生成した
Artisan
インスタンスのresolveCommands
に$this->commands
を引数に渡しコールします。おそらく$this->commands
は空の配列なのでただ戻り値に自身が返ってくるだけでしょう。
そのまま生成されたArtisan
インスタンスが戻されます。
Illuminate\Foundation\Console\Kernel::handle()
に戻ります。Illuminate\Foundation\Console\Kernel::handle()public function handle($input, $output = null) { try { $this->bootstrap(); return $this->getArtisan()->run($input, $output); } catch (Throwable $e) { $this->reportException($e); $this->renderException($output, $e); return 1; }
$this->getArtisan()
の戻り値にrun()
しています。引数に入力と出力を渡しています。
次はこちらを見ていきましょう。次回
今回は
artisan
インスタンスの生成の流れを読んでみました。次回はConsole\Application::run()
を追ってみたいと思います!続く☆
- 投稿日:2020-04-26T16:38:21+09:00
PHPUnit で標準入力のテストをしたい(STDIN を使った関数のテストのサンプル)
以下のような標準入力を取得する関数を作りたい場合のテストをしたい。
function getContentsFromStdin() { $contents = \file_get_contents('php://stdin'); if ($contents === false) { throw new \RuntimeException('Failed to read contents from STDIN.'); } return $contents; }ここ1年以内に作成 or 更新された Qiita 記事で「phpunit 標準入力 テスト」に絞ってググっても、ピンポイントに欲しい記事がなかったので自分のググラビリティとして。
TL; DR
php
ストリーム にラッパーを登録し、php://STDIN
にテストデータを書き込んでテストする。
- PHP 7.0.33 〜 7.4.5 + PHPUnit ^9 | ^7.5 | ^6.5 で動作確認したサンプルを GitHub にあげました。(Dockerfile あり)
TS; DR
テストしたい対象の関数
<?php declare(strict_types=1); namespace KEINOS\Sample; function getContentsFromStdin() { $contents = \file_get_contents('php://stdin'); if ($contents === false) { throw new \RuntimeException('Failed to read contents from STDIN.'); } return $contents; }テストの内容
getContentsFromStdinFunctionTest.php<?php declare(strict_types=1); namespace KEINOS\Tests; final class FunctionGetContentsFromStdinTest extends \PHPUnit\Framework\TestCase { public function testRegularInput() { $result_expect = 'World!'; // ラッパー "MockPhpStream" を "php://" ストリームに登録する $existed = in_array('php', \stream_get_wrappers()); if ($existed) { \stream_wrapper_unregister("php"); } \stream_wrapper_register("php", '\\KEINOS\Tests\MockPhpStream'); // "STDIN" にデータを書き込む \file_put_contents('php://stdin', $result_expect); // テストしたい関数("getContentsFromStdin()")のデータを取得 $result_actual = \KEINOS\Sample\getContentsFromStdin(); // ラッパーの解放 \stream_wrapper_restore("php"); // アサーションの実行 $this->assertSame($result_expect, $result_actual); } }テストに使うラッパー・スクリプト "MockPhpStream"
MockPhpStream.php<?php declare(strict_types=1); namespace KEINOS\Tests; /** * REF: http://news-from-the-basement.blogspot.com/2011/07/mocking-phpinput.html */ class MockPhpStream { protected $index = 0; protected $length = null; protected $data = 'hello world'; public $context; public function __construct() { if (file_exists($this->buffer_filename())) { $this->data = file_get_contents($this->buffer_filename()); } else { $this->data = ''; } $this->index = 0; $this->length = strlen($this->data); } protected function buffer_filename() { return sys_get_temp_dir(). DIRECTORY_SEPARATOR . 'php_input.txt'; } public function stream_open($path, $mode, $options, &$opened_path) { return true; } public function stream_close() { } public function stream_stat() { return array(); } public function stream_flush() { return true; } public function stream_read($count) { if (is_null($this->length) === true) { $this->length = strlen($this->data); } $length = min($count, $this->length - $this->index); $data = substr($this->data, $this->index); $this->index = $this->index + $length; return $data; } public function stream_eof() { return ($this->index >= $this->length ? true : false); } public function stream_write($data) { return file_put_contents($this->buffer_filename(), $data); } public function unlink() { if (file_exists($this->buffer_filename())) { unlink($this->buffer_filename()); } $this->data = ''; $this->index = 0; $this->length = 0; } }参考文献
- Mocking php://input @ News from the basement
- コメント | PHPで標準入出力をテストする(改め) @ Qiita
- 投稿日:2020-04-26T16:38:21+09:00
PHPUnit で標準入力のテストをしたい(STDIN を使った関数のテスト・サンプル)
以下のような標準入力を取得する関数を作りたい場合のテストをしたい。
function getContentsFromStdin() { $contents = \file_get_contents('php://stdin'); if ($contents === false) { throw new \RuntimeException('Failed to read contents from STDIN.'); } return $contents; }ここ1年以内に作成 or 更新された Qiita 記事で「phpunit 標準入力 テスト」に絞ってググっても、ピンポイントに欲しい記事がなかったので自分のググラビリティとして。
TL; DR
php
ストリーム にラッパーを登録し、php://STDIN
にテストデータを書き込んでテストする。
- PHP 7.0.33 〜 7.4.5 + PHPUnit ^9 | ^7.5 | ^6.5 で動作確認したサンプルを GitHub にあげました。(Dockerfile あり)
TS; DR
テストしたい対象の関数
<?php declare(strict_types=1); namespace KEINOS\Sample; function getContentsFromStdin() { $contents = \file_get_contents('php://stdin'); if ($contents === false) { throw new \RuntimeException('Failed to read contents from STDIN.'); } return $contents; }テストの内容
getContentsFromStdinFunctionTest.php<?php declare(strict_types=1); namespace KEINOS\Tests; final class FunctionGetContentsFromStdinTest extends \PHPUnit\Framework\TestCase { public function testRegularInput() { $result_expect = 'World!'; // ラッパー "MockPhpStream" を "php://" ストリームに登録する $existed = in_array('php', \stream_get_wrappers()); if ($existed) { \stream_wrapper_unregister("php"); } \stream_wrapper_register("php", '\\KEINOS\Tests\MockPhpStream'); // "STDIN" にデータを書き込む \file_put_contents('php://stdin', $result_expect); // テストしたい関数(getContentsFromStdin())のデータを取得 $result_actual = \KEINOS\Sample\getContentsFromStdin(); // ラッパーの解放 \stream_wrapper_restore("php"); // アサーションの実行 $this->assertSame($result_expect, $result_actual); } }テストに使うラッパー・スクリプト "MockPhpStream"
MockPhpStream.php<?php declare(strict_types=1); namespace KEINOS\Tests; /** * REF: http://news-from-the-basement.blogspot.com/2011/07/mocking-phpinput.html */ class MockPhpStream { protected $index = 0; protected $length = null; protected $data = 'hello world'; public $context; public function __construct() { if (file_exists($this->buffer_filename())) { $this->data = file_get_contents($this->buffer_filename()); } else { $this->data = ''; } $this->index = 0; $this->length = strlen($this->data); } protected function buffer_filename() { return sys_get_temp_dir(). DIRECTORY_SEPARATOR . 'php_input.txt'; } public function stream_open($path, $mode, $options, &$opened_path) { return true; } public function stream_close() { } public function stream_stat() { return array(); } public function stream_flush() { return true; } public function stream_read($count) { if (is_null($this->length) === true) { $this->length = strlen($this->data); } $length = min($count, $this->length - $this->index); $data = substr($this->data, $this->index); $this->index = $this->index + $length; return $data; } public function stream_eof() { return ($this->index >= $this->length ? true : false); } public function stream_write($data) { return file_put_contents($this->buffer_filename(), $data); } public function unlink() { if (file_exists($this->buffer_filename())) { unlink($this->buffer_filename()); } $this->data = ''; $this->index = 0; $this->length = 0; } }参考文献
- Mocking php://input @ News from the basement
- コメント | PHPで標準入出力をテストする(改め) @ Qiita
- 投稿日:2020-04-26T16:05:09+09:00
tymon/jwt-auth を拡張するパッケージを公開して不満が解消した
はじめに
Laravel
でAPIサーバーを開発する際、認証にJWT
を使うことが増えてきました。
ライブラリには決まって、tymon/jwt-authを使うのですが、いくつか不満があって、毎回カスタマイズしていたので、拡張ライブラリを作って公開しました。
tymon/jwt-auth
に対する不満クエリパラメータ、リクエストボディにある
token
もJWTトークンだと認識してしまう「Laravelで作ったらすべて解決ではないよ?」という記事でも書いたのですが、
tymon/jwt-auth
は、クエリパラメータ、リクエストボディにあるtoken
もJWTトークンだと認識してしまいます。
正直、QueryString
、InputSource
、RouteParams
は使うことないのでは?と考えてます。vendor/tymon/jwt-auth/src/Providers/AbstractServiceProvider.phpprotected function registerTokenParser() { $this->app->singleton('tymon.jwt.parser', function ($app) { $parser = new Parser( $app['request'], [ new AuthHeaders, new QueryString, new InputSource, new RouteParams, new Cookies($this->config('decrypt_cookies')), ] ); $app->refresh('request', $parser, 'setRequest'); return $parser; }); }
'token'
という名前を設定ファイルから変更できない各
Parser
のsetKey
関数で名前を変更することはできますが、できれば、設定ファイルで環境ごとに変えられるようにしたいですよね?
Add ability to provide custom cookie name from config by SiebeVE · Pull Request #1933でも、setKey
する方法が紹介されていますが、設定ファイルに切り出す気はなさそうです。vendor/tymon/jwt-auth/src/Http/Parser/KeyTrait.phpnamespace Tymon\JWTAuth\Http\Parser; trait KeyTrait { /** * The key. * * @var string */ protected $key = 'token'; /** * Set the key. * * @param string $key * * @return $this */ public function setKey($key) { $this->key = $key; return $this; }
'tymon.jwt.parser'
を拡張以上を踏まえて、imunew/tymon-jwt-authでは、下記のように、拡張しました。
QueryString
、InputSource
、RouteParams
は削除。
Cookies
だけにして、AuthHeaders
は任意で追加可能にしました。src/Providers/ServiceProvider.phpprivate function resetParserChain() { $this->app->extend('tymon.jwt.parser', function (Parser $parser) { $chain = [ (new Cookies(config('jwt.decrypt_cookies'))) ->setKey(config('jwt-auth.cookie.key')) ]; if (config('jwt-auth.auth-headers.enabled')) { $chain[] = new AuthHeaders(); } return $parser->setChain($chain); }); }config/jwt-auth.phpreturn [ "cookie" => [ 'key' => env('JWT_AUTH_COOKIE_KEY', 'auth-token') ], "auth-header" => [ 'enabled' => env('JWT_AUTH_AUTH_HEADER_ENABLED', false) ] ];追加機能
今回、
'tymon.jwt.parser'
を拡張しただけではなく、新たに2つの機能を追加しました。
AuthResource
ログインAPIが返すリソースとして使えそうな、API Resourceを実装しました。
JWT
トークンをCookie
にセットします。src/Resources/AuthResource.phpclass AuthResource extends JsonResource { /** @var string|null */ public static $wrap = null; /** * {@inheritDoc} */ public function toArray($request) { return []; } /** * @param Request $request * @return JsonResponse */ public function toResponse($request) { $response = parent::toResponse($request); return HttpHelper::respondWithCookie($request, $response, $this->resource->accessToken); } }
RefreshJwtToken
今回の目玉機能だと思っているんですが、JWTトークンの有効期限が切れたら自動でリフレッシュしてくれるミドルウェアを実装しました。
尚、このミドルウェアのアイデアは、Laravel での tymon/jwt-auth による JWT トークンの自動更新から得ました。
@yh1224 さん、ありがとうございました。src/Middleware/RefreshJwtToken.phpclass RefreshJwtToken extends BaseMiddleware { /** * @param Request $request * @param Closure $next * @return mixed */ public function handle(Request $request, Closure $next) { $token = null; try { $token = $this->auth->parseToken(); $token->authenticate(); } catch (TokenExpiredException $e) { // Token expired: try refresh try { return $this->refreshJwtToken($token, $request, $next); } catch (JWTException $e) { // Refresh failed (refresh expired) } } catch (JWTException $e) { // Invalid token } return $next($request); } /** * @param JWT $jwt * @param Request $request * @param Closure $next * @return JsonResponse|Response */ private function refreshJwtToken(JWT $jwt, Request $request, Closure $next) { $newToken = $jwt->refresh(); $request->cookies->set(config('jwt-auth.cookie.key'), $newToken); $response = $next($request); return HttpHelper::respondWithCookie($request, $response, $newToken); } }
RefreshJwtToken
を有効にするには、以下のように、Kernel.php
に設定を追加します。app/Http/Kernel.phpprotected $middlewareGroups = [ 'web' => [ // ... ], 'api' => [ 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加 ] ]; protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加 \App\Http\Middleware\Authenticate::class, // この行よりも上に追加する \Illuminate\Routing\Middleware\ThrottleRequests::class, \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Auth\Middleware\Authorize::class, ];
$middlewarePriority
の\App\Http\Middleware\Authenticate::class
よりも前に、RefreshJwtToken
を追加しないと、先にAuthenticate
が実行されてしまい、401
になってしまうので注意してください。おわりに
2020-03-05
に、ついに、1.0.0がリリースされたtymon/jwt-auth
ですが、github
のissues
など見ていても、Parser
周りは特に不満があるかと思います。
そんなときは、imunew/tymon-jwt-authも合わせて、composer require
していただければ幸いです。
pull-requests
またはissues
お待ちしております。
ではでは。
- 投稿日:2020-04-26T09:59:23+09:00
学習日記 #5
クロスサイトスクリプティング
エンドユーザーからの入力などによって生成されるページで、不備によって不正な(悪意のある)スクリプトが混入/実行されてしまう脆弱性のことをクロスサイトスクリプティング(XSS:Cross Site Scripting)脆弱性という。
SQLインジェクション
SQLインジェクションとは、SQL命令に不正なパラメータを引き渡すことで、本来、開発者が意図していなかったSQLが生成/実行されてしまう脆弱性のことである。その結果、公開されるはずではなかった機密情報が漏洩していたり、重要なデータが削除されてしまったりする可能性がある。
OSコマンドインジェクション
OSコマンドに対して、不正な命令を注入する脆弱性である。
クロスサイトリクエストフォージェリ
クロスサイトリクエストフォージェリ(CSRF:Cross-Site Request Forgeries)とは、サイトに攻撃用のスクリプトを仕込んでおくことで、アクセスしてきたユーザに意図しない操作を強制することである。CSRF攻撃を受けることで、あるサービスに勝手に登録させられてしまったりしてしまう。
パストラバーサル
パストラバーサル(Path Traversal)脆弱性とは、本来想定したパスを遡って自由にファイルを読み書きされてしまう脆弱性のことである。
ホワイトリストを作成し、ファイル名をチェックする。ホワイトリストとは、操作を許可する対象のリストである。メールヘッダインジェクション
メールヘッダに不正なヘッダ情報を混入させることで、スパムメールを送信させる攻撃のことである。
- 投稿日:2020-04-26T00:23:44+09:00
Laravelの初心に還る旅【サービスコンテナ&サービスプロバイダ】
1. サービスコンテナの概要
サービスコンテナとは
オブジェクトのインスタンス化を管理するもの。
(〜が要求されれば〜のインスタンスを提供するといったようなイメージ)公式(Laravel7.x)
Laravelサービスコンテナは、クラスの依存関係を管理し、依存関係の注入を実行するための強力なツールです。依存性注入は、本質的にこれを意味する派手なフレーズです。クラスの依存関係は、コンストラクタまたは場合によっては「セッター」メソッドを介してクラスに「注入」されます。
依存性注入(Dependency injection、略称DI)とは
あるオブジェクトが他のサービスオブジェクトに依存している際、そのサービスオブジェクトの生成は自身で担わず、外部から生成されたオブジェクトを受け取り、使用する仕組み。
生成されたオブジェクトに対しての、「使用」と「構築」の責務を分離する。
クライアント側はオブジェクトの生成方法は知る必要がなく、使用するのみ。
これにより、コードの可読性と再利用のしやすさが向上する。例)
依存性注入を行わない場合のコンストラクタ
Foo.phpclass Foo{ private $bar; public function __construct() { $this->bar = new Bar(); } }Fooクラス自身がBarクラスのインスタンス化を担っており、単一責任の原則に基づいていない。
さらに、コンストラクタ内にインスタンス生成のロジックがハードコードされているため、密結合になっている。依存性注入を行う場合のコンストラクタ
Foo.phpclass Foo{ private $bar; public function __construct(Bar $bar) { $this->bar = $bar; } }FooクラスはBarクラスのインスタンス化は担っておらず、どこかで生成されたインスタンスをプロパティに格納するだけ。
Laravelのサービスコンテナはこの依存性注入を便利に行ってくれる。
2. サービスプロバイダ
サービスプロバイダとは
Laravelアプリケーションの全体処理において、初期起動処理を担う。
サービスコンテナの結合や、イベントリスナ、フィルター、それにルートなど、諸々の登録処理を行う。公式(Laravel7.x)
サービスプロバイダは、Laravelアプリケーション全体の起動処理における、初めの心臓部です。皆さんのアプリケーションと同じく、Laravelのコアサービス全部もサービスプロバイダを利用し、初期起動処理を行っています。
ところで「初期起動処理」とは何を意味しているのでしょうか? サービスコンテナの結合や、イベントリスナ、フィルター、それにルートなどを登録することを一般的に意味しています。サービスプロバイダはアプリケーション設定の中心部です。3. サービスコンテナとサービスプロバイダの活用例
自動的な解決
デフォルトでLaravelプロジェクト内に作成されているUserモデルをもとに解説。
app/User.php<?php namespace App; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; }以下、Userモデルに合わせた形でコントローラクラスを作成。
app/Http/Controllers/UserController.php<?php namespace App\Http\Controllers; use App\User; use Illuminate\Http\Request; class UserController extends Controller { /** * ユーザーモデル * * @var User */ protected $user; public function __construct(User $user) { $this->user = $user; } public function getName(int $id): string { return $this->user->where('id', $id)->get('name');; } }コンストラクタに注目。
UserController.php/** * ユーザーモデル * * @var User */ protected $user; public function __construct(User $user) { $this->user = $user; }コンストラクタでUserクラスをタイプヒンティングしている。
通常サービスコンテナを利用せずにUserControllerをインスタンス化しようとすると以下のようになる。
$user_controller = new UserController(new User());しかし、Laravelのサービスコンテナを利用する場合は以下のように書くことができる。
$user_controller = app()->make(UserController);なぜかというとLaravelがタイプヒンティングをもとにリフレクションで自動解決するからだ。
(引数にわざわざUserモデルのインスタンスを渡さなくても要求されているオブジェクトを自分で探してインスタンス化する。)これでUserControllerクラスのプロパティ$usersにはUserモデルが格納される。
上記の例だと自動的に解決するが、明示的にサービスコンテナの結合を定義しておくこともできる。
明示的な解決
前述のUserモデルとコントローラを使用し、Repositoryパターンを用いて解説。
Repositoryパターンに必要な諸々のファイルを作成。
app/Repositories/Interfaces/UserRepositoryInterface.php<?php namespace App\Repositories\Interfaces; interface UserRepositoryInterface { public function all(); public function getUserById(int $id); }app/Repositories/UserRepository.php<?php namespace App\Repositories; use App\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Collection; use App\Repositories\Interfaces\UserRepositoryInterface; class UserRepository implements UserRepositoryInterface { private $user; public function __construct(User $user) { $this->user = $user; } public function all(): Collection { return User::all(); } public function getUserById(int $id): Model { return User::where('id', $id)->first(); } }コントローラは以下のように書き換えておく。
app/Http/Controllers/UserController.php<?php namespace App\Http\Controllers; use App\User; use Illuminate\Http\Request; use Illuminate\Database\Eloquent\Model; use App\Repositories\Interfaces\UserRepositoryInterface; class UserController extends Controller { /** * リポジトリクラス * * @var UserRepositoryInterface */ protected $repository; public function __construct(UserRepositoryInterface $repository) { $this->repository = $repository; } public function index() { return $this->repository->all(); } public function getUserName(int $id): string { $model = $this->repository->getUserById($id); return $model->name; } }サービスプロバイダクラスの作成。
root@582455531b89:/var/www/hogehoge# php artisan make:provider RepositoryServiceProvider Provider created successfully.以下のサービスプロバイダクラスが出来上がる。
app/Providers/RepositoryServiceProvider.php<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { // } /** * Bootstrap services. * * @return void */ public function boot() { // } }register()内を以下のように書き換える。
app/Providers/RepositoryServiceProvider.php<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { /** * Register services. * * @return void */ public function register() { $this->app->bind(UserRepositoryInterface::class, UserRepository::class); } /** * Bootstrap services. * * @return void */ public function boot() { // } }これで、「UserRepositoryInterfaceクラスが要求された際にUserRepositoryクラスを返す」という意味になる。
最後にconfig/app.phpに作成したサービスプロバイダを追加しておく。
config/app.php...省略 App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\RepositoryServiceProvider::class, ], ...省略tinkerで実行してみる。
root@582455531b89:/var/www/hogehoge# php artisan tinker Psy Shell v0.10.3 (PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman >>> $controller = app()->make('UserController'); [!] Aliasing 'UserController' to 'App\Http\Controllers\UserController' for this Tinker session. => App\Http\Controllers\UserController {#3009} >>> $controller->index(); => Illuminate\Database\Eloquent\Collection {#3054 all: [ App\User {#3055 id: 1, name: "test1", email: "test1@test.com", email_verified_at: null, created_at: "2020-04-18 06:21:24", updated_at: "2020-04-18 06:21:24", }, App\User {#3056 id: 2, name: "Kaci Kunze", email: "acarroll@example.net", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3057 id: 3, name: "August Paucek", email: "oreilly.jadyn@example.org", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3058 id: 4, name: "Lizeth Reichel", email: "boyle.heath@example.com", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3059 id: 5, name: "Aurelio Gorczany", email: "polly72@example.org", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3060 id: 6, name: "Derek Boehm II", email: "fritz19@example.org", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3061 id: 7, name: "Nat Mertz", email: "amira.wisozk@example.net", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3062 id: 8, name: "Dominic Ritchie", email: "ybartell@example.com", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3063 id: 9, name: "Princess Cronin", email: "zion.weissnat@example.com", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, App\User {#3064 id: 10, name: "Willie Becker", email: "leta.medhurst@example.org", email_verified_at: "2020-04-18 06:43:29", created_at: "2020-04-18 06:43:29", updated_at: "2020-04-18 06:43:29", }, ...省略 >>> $controller->getUserName(2); => "Kaci Kunze"UserControllerをインスタンス化しただけで、UserRepositoryクラスがコントローラクラス内のプロパティDIされ、UserRepositoryのメソッドを使用できている。
まとめ
Laravelは偉大。