- 投稿日:2020-12-04T23:45:09+09:00
Laravel 8のexplainをLaravel7以下で使う
何がしたいか
Laravel8 でexplainメソッドが追加されました。
User::where('id',1)->explain()->dump(); DB::table('id',1)->where('id',1)->explain()->dump();https://www.amitmerchant.com/using-mysql-explain-for-queries-in-laravel-8/
Laravel8未満でも使いたいなーと思いやってみることに
環境
下記環境で動作を確認してます。
Laravel 7.xやり方
macroで拡張できるので、メソッドを追加して、マージされたプルリクの内容を
AppServiceProvider
にペタリAppServiceProvideruse Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Query\Builder as QueryBuilder; public function boot(): void { QueryBuilder::macro('explain', function () { $sql = $this->toSql(); $bindings = $this->getBindings(); $explanation = $this->getConnection()->select('EXPLAIN ' . $sql, $bindings); return collect($explanation); }); EloquentBuilder::macro('explain', function () { $sql = $this->toSql(); $bindings = $this->getBindings(); $explanation = $this->getConnection()->select('EXPLAIN ' . $sql, $bindings); return collect($explanation); }); }これでとれるようになった。
User::where('id',1)->explain()->dump(); DB::table('id',1)->where('id',1)->explain()->dump();ログに出す
そもそも、ログ出すのに使う必要ないので、explainメソッドいらない子かもしれない...
サービスプロバイダのboot
に下記を追加(参考)AppServiceProviderDB::listen(function ($query) { if (!preg_match("/^EXPLAIN/", $query->sql)) { $explain = DB::select("EXPLAIN {$query->sql}", $query->bindings); Log::info($explain); } });
- 投稿日:2020-12-04T22:01:41+09:00
unsignedBigIntegerについて調べてみた
unsignedBigIntegerは外部キーのデータ型によく使うやつ。
忘れやすいのでメモっておきます。
unsignedBigIntegerは「符号なしBIGINTカラム」
・符号なし・・・正の数のみ ・BIGINTカラム・・・格納サイズは8バイト(-9223372036854775808から9223372036854775807) ・8バイト・・・64ビット(1バイトは8ビット) ・64ビット・・・2の64乗(1ビットは2進数の1桁。つまり、0か1の2種類。なので、64ビットで2×64種類の数値が表せるということ。) ・2の64乗・・・18446744073709551616ということで、つまりは
unsignedBigIntegerは「0~18446744073709551615」までの値を格納できるデータ型のことで、言い換えると「符号なしBIGINTカラム」
- 投稿日:2020-12-04T21:18:10+09:00
Laravel でテストを書くことに慣れてきたあたりで知りたかったこと5選
はじめに
年始に入社してから Laravel でテストを書くことが多かったように思います。
その中でハマったものや、既存のコードを読んでいてこういう書き方ができるのかと思ったことを5つ挙げさせていただきました。
最近テストを書き始めた方の参考になれば幸いです。注入するクラスが例外を返すときの挙動をテストしたいとき
テスト対象へ注入しているクラスのインスタンスが下記の
$hoge
だとします。
以下のように例外発生時何か処理を行っているメソッドがテスト対象の場合、キャッチした中での処理結果を確認したいときがあると思います。
この場合、$hoge->execute()
の結果で、例外を返す必要があります。try { $hoge->execute(); } catch (\Exception $e) { // エラー通知のメールを送信・ログに出力などの処理 }このようなときは、以下のようにして例外を返すモックを作って注入してやれば例外を返してくれます。
$mock = $this->createMock(targetTestClass::class); $mock->method('execute') // メソッド名を指定 ->will($this->throwException(new \Exception)); // 例外を返すことを指定 $test = new testTargetClass($mock);注入するクラスがチェーンメソッドを使っている場合
テスト対象のクラスへ注入するクラス(下記の例では
HogeClass
)から、チェーンメソッドが呼ばれている場合です。
チェーンメソッドが最終的に返す値によってテスト対象のメソッドの結果が変わる場合、任意の値を返してもらう必要があります。class TestTargetClass { protected $hoge; public function __construct(HogeClass $hoge) { $this->hoge = $hoge; } public function TestTargetMethod() { $tmp = $this->hoge->fuga()->moge(); // ←こういうパターンで任意の値を返したい! // この後 $tmp によって分岐したりしてメソッドが返す値が変わる下記のようにチェーンメソッドで呼ばれているメソッド名を矢印
→
でつないだ文字列を引き渡してあげればモックが作成できますのでこれを注入してやればOKです。$hogeClassMock = \Mockery::mock(HogeClass::class); $hogeClassMock ->shouldReceive('fuga->moge') ->andReturn('返したい値');Log ファサードで出力した内容を確認したい場合(に便利なライブラリ)
「Log fake for Laravel」というライブラリがシンプルで便利だったので紹介させてください。
Laravel のFake
のようなメソッドがありテストがしやすいようになっていますが、Log
にはないためそれを補ってくれます。準備
下記のメソッドによって、実際のログファイルには出力されなくなるようになるため、テストのたびにログが積み重なっていくということも避けられます。(
Mail::fake()
に近い)use Illuminate\Support\Facades\Log; use TiMacDonald\Log\LogFake; protected function setUp(): void { parent::setUp(); Log::swap(new LogFake()); }アサーションの書き方の例
以下のように、ログ出力先のチャンネル、エラーレベル、ログの内容をクロージャで指定するのが基本となっているようです。
他にもアサーションがありますので、上記のリンクから公式のドキュメントを参照してみてください。Log::channel('hoge')->assertLogged( 'error', function ($message) use ($expectedMessage) { return $expectedMessage === $message; } );2つの配列の順序は無視できるが、内容が一致しているかを確認したい場合
順序は関係ないが2つの配列の値が一致していたらテストとしてOKとしたいパターンです。
このメソッドを知るまでは都度ソートしてから比較しており、冗長のような気がする… などと思っていたらassertEqualsCanonicalizing
というずばりなアサーションがありました。
以下のように配列を2つ引き渡してあげると、双方が持つ id の値が順序関係なく一致していればOKになります。$this->assertEqualsCanonicalizing( $newData->pluck('id'), $oldData->pluck('id') );特定の呼び出し回数で返す値を変えるモック
以下のように
andReturnUsing
というメソッドに、呼び出し回数で処理を変えるクロージャを引き渡してやればOKです。
例では、send
メソッドが呼ばれた初回のみ例外を返し、2回目以降はfalse
を返しています。
特定の値が引き渡された場合のみ異なる値を返すなど応用が効きそうです。$mail = Mail::shouldReceive('send') ->andReturnUsing(function () { static $counter = 0; switch ($counter++) { case 0: throw new \Exception(); break; default: return false; break; } });単純に呼び出し回数で返す値を変えたい場合
andReturn
に引き渡した値の順番と呼び出し回数は対応しているため、例えば、呼び出し1回目=>true、呼び出し2回目=>false
と返したい場合は下記のように書けます。Mail::shouldReceive('send')->andReturn(true, false);おわりに
モックでも例外を返せたりチェーンメソッドが呼び出せるということは知っていればなんてことはありませんが、初めて見たときはどうしたらいいのかわからず困ったりしました。
せっかく方法を調べたので未来の自分向けのメモも兼ねて今回記事にまとめさせていただきました。
基本的なテストの書き方はわかった方が、どんどんテストを書いていくうちにそのうちつまずきそうなものを選んだつもりなので、最近テストを書き始めた方の頭の片隅にでも残してもらえれば幸いです。
- 投稿日:2020-12-04T18:41:55+09:00
php artisan serveに任意のphp.iniを指定する方法
結論
php -c php.ini artisan serve背景
php artisan serveで動かしている開発用のサーバー上で大きなファイルをアップロードする必要があり、実際にアップロードしたところIlluminate\Http\Exceptions\PostTooLargeException とエラーが出た。
ここはphp.iniを変更すれば通るだろうと考え、upload_max_filesizeとpost_max_sizeを変更したものの、なぜか同じエラー。phpinfoで確認すると値はきちんと更新されている。
原因
いろいろ調べてみたところ、php artisan serveは別のphp.iniを読みに行っているということまでは判明。
解決
ただ、どのphp.iniを読みに行っているのか分からない。
php.iniと同じディレクトリにphp-cli.iniを作成するとそれを読みに行ってくれるとか、その他色々な情報があったものの結局、動いたのは冒頭で示したやり方。
元々読みに行っていたファイルを変更したほうがスマートな気もするが、Webサーバーで使用される設定ファイルと同じものを指定できるならそれはそれで分かりやすくて便利な気がする。
コマンド中での指定される"php.ini"は現在のWebサーバーで使用されているファイルを見に行くようなので、php -iで場所を確認してから編集する。
$ php -i | grep php.ini Configuration File (php.ini) Path => /etc/php/7.4/cli Loaded Configuration File => /etc/php/7.4/cli/php.ini編集後、php artisan serveを実行すると変更したパラメーターが反映された設定でサーバーが起動する。
- 投稿日:2020-12-04T17:43:39+09:00
10年戦士のレガシーPHPを改善するためにやってきたこと
ドワンゴで、ニコニコ動画とプレミアム課金の開発をしている @yoshikyoto です。
最近では、総合TOPのリニューアルや、↓の記事で書かれているプレミアム課金の移行をやっていました。
レガシーなPHPを改善してきた知見
ニコニコは古くからあるサービスで、レガシーなコードも多く存在しています。
僕は新卒でニコニコ動画に配属され、5年間ずっと PHP を触ってきました。その中で、多くの新機能の開発、古いコードのリファクタリング、いろいろやってきました。その知見をまとめます。
Composer, Laravel (フレームワーク)の導入
PHP を使っていて Composer を導入していないシステムは珍しい思いますが、10年もののシステムには、Composer が導入されていない場合もあります。導入されていないなら導入します。(Composer は JavaScript でいうところの npm 、 Ruby でいうところの Bundler です)
10年もののPHPには、フレームワークは導入されていません。URLのエンドポイントごとに PHP ファイルが存在しています。フレームワークを導入しましょう。基本的には Laravel を導入するのが良いです。
レガシーなプロダクトに Laravel を導入する記事も以前に書きました。
Slim Framework を導入する場合もありました。 DI、DB、Redis、Log といったライブラリが導入済みで、ルーティングだけなんとかしたい場合は Slim Framework でもよいです。
ユニットテスト(PHPUnit)
私にとって、レガシーコードとは、単にテストの無いコードです
『レガシーコード改善ガイド』にはこう書いてあります。
コードに変更を加えづらくなる一番の原因は、テストが無いことです。ユニットテストがあれば、コード修正時にある程度の動作が保証できます。コードが綺麗でも、テストがなければ、コードの修正がしづらくなるのです。
ユニットテスト導入時には同時に、以下の2点を考えると良いです。
- CIの導入
- CIを導入しないと、いつの間にかテストが落ちている事に気づかない
- IDE から phpunit コマンドを実行できるようにする
- CIでしかテストを実行できないと、開発効率が良くないので、当然手元のPCでもテストを実行できる必要があるのですが、IDE の設定をすることで、開発効率が格段によくなる
CI の導入
特に説明不要かと思います。僕のチームでは Jenkins を利用していますが、CircleCI を使っているチームもあります。
Jenkinsfile
という、CI の実行内容を定義できる仕組みがあるので(CircleCI でいう.circleci/config.yml
みたいなもん)、これを使って phpunit コマンドを実行し、結果を表示させます。pipeline { agent any // composer などのコマンドを叩くために必要 environment { PATH = "/usr/local/bin:$PATH" } stages { stage ('setup') { steps { checkout scm // phpunit 等をインストールするために --dev を付ける sh 'composer install --dev' // テスト結果を格納しているディレクトリをきれいにする sh 'rm -rf results' sh 'mkdir results' } } stage ('phpunit') { steps { sh './vendor/bin/phpunit --log-junit results/phpunit_junit.xml' } } } post { always { junit 'results/phpunit_junit.xml' } } }IDE(IntelliJ / PHPStorm)の設定
僕は開発に IntelliJ を使っているのですが、テスト関数の横のボタンを押すだけで、テストを実行できるようになります。この機能を知った時には衝撃的でした。
手元で毎回すべてのテストを実行するには時間がかかるので、自分が修正した部分のテストだけ実行し、全体のテストは、CIで保証しますが、「自分の実装したテストだけを実行するコマンド」を忘れがちなので、それを IDE におまかせできるのも良い点です。
これを出すためには
- PHP Interpreter の設定
- composer.json の設定
- Test Frameworks の設定
が必要です。
PHP Interpreter の設定 は、 Preferences > Languages & Frameworks > PHP の「CLI Interpreter」から設定できます。PCにインストールされているPHPのバージョンから選択できるので、適切なバージョンをインストールしてください。Docker上のPHPを設定するとかもできるはずです。
composer.json の設定 は、Languages & Frameworks > PHP > Composer から設定できます。
Test Frameworks の設定は、 Languages & Frameworks > PHP > Test Frameworks から設定できます。composer で PHPUnit を入れていれば、「User composer autoloader」を選択し、
autoload.php
のバージョンを指定すれば OK です。うまく設定できていれば PHPUnit のバージョンが表示されます。モック
PHPUnit にはモックの仕組みがありますが、コードとして読みづらいので、 Mockery というライブラリを使うのがおすすめです。
コーディング規約
括弧や演算子の前後のスペースの有無がぐちゃぐちゃなコードを見た経験はありますか?インデントにタブとスペースが混ざっていると、とても芸術的な見た目になります。しかし、コードにはそのような芸術性より、レビューしやすさが必要です。
PHP には PSR-12 というコーディング規約があるので、これをもとにコードを書きます。
コーディング規約についても PHPUnit と同じ理由で、CI と IDE の設定を両方設定しておくと良いです。
設定の方法については、僕が過去のブログで書いているので参考にしてください。
ログ
ログは、基本的に1つのファイルにすべて出力するのが良いかなと思います(ファイル数を増やしすぎないのが良いかなと思います)。ログファイルが増えると、そのたびに監視対象のファイルを追加する必要があります。また、もう出さなくなったログのファイルが残り、空のログファイルが無限にログローテートされていたりします。
ログを ElasticSearch などに突っ込めば、検索性については問題ありません。むしろ、複数ファイルを横断して検索する必要もなくなり便利です。
PHP の標準規約である PSR-3 ( https://www.php-fig.org/psr/psr-3/ )では、ログレベルは9段階が用意されています。Laravel で利用している Monolog はこれに従っていますが、全部を利用する必要は無いです。
ただ、最低でも、3段階くらいには分けておいたほうが良いです。
- 即時対応が必要なログ(アラートが上がって電話がかかってくるタイプのやつ)
- 翌営業日の対応でも問題ないが、検知しておく必要があるログ(Slackとかメールで流れるやつ)
- ユーザーや関連システムからの問い合わせがあった場合のみ参照するログ(プッシュ通知の必要は無い)
ログには、
- ログの日時(Laravel だと自動で付与される)
- どのサーバーで出たか
- どのエンドポイント(URL)で出たか
- アクセスしたユーザーのID
- スタックトレースや、スクリプトファイル名
- 必要に応じてその他の情報
などを出します。
ログには、「何のログなのか・対応が必要なログなのか」「対応が必要な場合どのような対応をすれば良いのか」も出しておきましょう。エラーっぽいログなのに、必要な対応が書かれていないと、どうしたら良いのかわかりません。
コードの設計について
ニコニコ動画のコードは、以前はレイヤードアーキテクチャでしたが、最近は「依存性の逆転」をかなり意識したコードになっています(レイヤードアーキテクチャより前は、スパゲッティアーキテクチャでした)。
一方で、プレミアム課金については、まだほとんどがスパゲッティアーキテクチャです。まずはレイヤードアーキテクチャを目指してコードをリファクタリングして行こうと思っています。
どういう設計を目指すかについては、現状のコードとメンバーのスキルを考えてやっていくことが重要です。プレミアム課金システムのことを考えると
- とりあえずテストを拡充していくことが重要だと思うので、シンプルでわかりやすいレイヤードアーキテクチャでよい
- テストが充実すれば、レイヤードアーキテクチャ→クリーンアーキテクチャの変更もしやすくなる
- メンバーのレベル感も考える
- プレミアム課金システムに詳しいメンバーが少ないので、適切な設計をしづらい(配属されて浅い人が多い)
- 教育コストとかもかかる
- レイヤードアーキテクチャとクリーンアーキテクチャを両方つかって初めて、それぞれの良さがわかる。
- この経験がメンバーの成長にもつながる
といった感じです。
PHPDoc
PHPDoc で、クラスの Doc を書くことは重要だと僕は思っています。
/** * 何をするクラスなのか、何をしないクラスなのか書くことで、 * 適切なメソッドを適切なクラスに実装できるようになる。 * @see 仕様ページへのリンクなど */ class Xxxxx { ... }また、
@param
や@return
についても、PHP の型宣言より詳細な情報を書け、IDE の補完による恩恵も受けられるので、書いておいたほうがいいです。/** * @return string[] <- array の中身の型も書ける */ public function get(): array; /** * @return int|false <- mixed の詳細まで書ける * @throw RuntimeExeption なんの時のエラーなのかちゃんと説明を書いてね */ public function get(): mixed;GuzzleHttp
レガシーなコードでは API Client の実装に
file_get_contents()
やcurl
を使っていたと思いますが、 Composer があるなら Guzzle を使うほうが良いです。Guzzle は PSR-18 のインタフェースに従って実装されています。
PSR-18 で重要なのが、エラーハンドリングの部分です。400エラーや、500エラーが返ってきた時には例外を投げるようになっており、Guzzle のエラーは以下のような構造になっています。
- \RuntimeException
- TransferException -- HTTPリクエストに関する例外
- ConnectException -- 接続がtimeoutした場合などの例外
- TooManyRedirectsException -- リダイレクトループの場合の例外
- BadResponseException -- 4XXまたは5XXエラー
- ClientException -- 4XXエラー
- ServerException -- 5XXエラー
エラーについて理解し、どのようなエラーの時にどのようなハンドリングをすればいいのか、注意しましょう。
まとめ
先日、ウィスキーの蒸留所に行ってきたのですが、できたばかりのウィスキーの色は、12年後のウィスキーの色と全然違いました。
このように、ものは10年経つと全然変わります。先日 PHP 8 がリリースされたり、去年には PSR-12 が発表されたり、PHP も進歩しているので、我々も、常に新しい技術を取り入れて、改善を続けています。
レガシーコードばっかりいじっていて大変そうに思えますが、このレベルのレガシーコード改善は、ほぼ新規アプリケーションの開発のようなものです。ニコニコ動画のサーバーの規模は大きく、その中でどのようにコードを組み上げて行くか考えるのは非常に面白いです。
こういうリファクタリングのための工数を取ってもらえるのは、ドワンゴの非常に良い文化ですね。
- 投稿日:2020-12-04T16:39:43+09:00
ExcelやcsvでもらったマスタデータをLaravelのseederでデータ登録する
区分値だったりデータベース上で管理するマスタデータをexcelやcsvで共有されることが多いです。
マスタデータなどはLaravelのSeeder機能を使ってレコード追加していきたいので少し工夫してみたという記録です
例えば都道府県の登録
以下のようなcsvがあるとします。
これをいちいち登録するのはうんざりする作業なのでcsvからLaravelのSeederを使って登録したいですtodofuken.csv
1,北海道 2,青森県 3,岩手県 4,宮城県 5,秋田県 6,山形県 7,福島県 8,茨城県 9,栃木県 10,群馬県 11,埼玉県 12,千葉県 13,東京都 14,神奈川県 15,新潟県 16,富山県 17,石川県 18,福井県 19,山梨県 20,長野県 21,岐阜県 22,静岡県 23,愛知県 24,三重県 25,滋賀県 26,京都府 27,大阪府 28,兵庫県 29,奈良県 30,和歌山県 31,鳥取県 32,島根県 33,岡山県 34,広島県 35,山口県 36,徳島県 37,香川県 38,愛媛県 39,高知県 40,福岡県 41,佐賀県 42,長崎県 43,熊本県 44,大分県 45,宮崎県 46,鹿児島県 47,沖縄県単純な話でただcsvを一行ずつ読んで
insertしていくスクリプトを書くだけです。<?php use Illuminate\Database\Seeder; class Area1TableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { if (($handle = fopen(__DIR__ . './todofuken.csv', 'r')) !== false) { while (($data = fgetcsv($handle))) { DB::table('m_todofuken')->insert([ 'id' => $data[0], 'name' => $data[1], ]); } fclose($handle); } } }csvファイルもリポジトリで管理しておけばマスタデータが変わったよなんて言うときにも
csvを編集してseedを実行してあげればいいので楽ちんですね終わり
- 投稿日:2020-12-04T14:33:08+09:00
Laravel ログインテスト
Laravel Unitテスト
ポートフォリオを作成して、ユニットテストを実行するまでの一連の流れを備忘録として記述します。
作成したポートフォリオでは初期画面からホーム画面に遷移するのにログインする必要があるシステムでした。コントローラーのテストで下記のように実行してもエラーが表示された為、ログインテストを別で作成してモデルのテストを実行しました。pubulic function testBasicTest() { $this->get('/home')->assertStatusOk(); /// /homeにはログインが必要な為アクセスできない }1.テスト用のDatabaseの準備
今回はMySQLを使用しました。使用するデータベースに応じてテスト用のデータベースを準備。
$ mysql -uroot -p $ ?(password入力) mysql> create database データベース名 mysql> show databases 今回作成したデータベースがあるか確認 mysql> exitConfig/database.phpの'connections'にテスト用のデータベースの設定を追加。
'testing' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => "作成したデータベース名", 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ],phpunit.xmlのタグ内に下記を追加。
<server name="DB_CONNECTION" value="mysql"/> <server name="DB_DATABASE" value="[データベース名]"/>2.ファクトリーを準備する
今回作ったポートフォリオはLaravelに標準で準備されているUserテーブルを使用した為、変更しませんが追加などしている場合は変更する。(database/factories/UserFactory.php)
3.テスト用ファイルの作成
下記コマンドを実行するとtests/Featureにテスト用ファイルが作成されます。
$ php artisan make:test LoginTest作成されたLoginTest.phpを下記に変更
<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithoutMiddleware; use Tests\TestCase; use App\User; use Auth; class LoginTest extends TestCase { /** * A basic feature test example. * * @return void */ public function testExample() { $user = factory(User::class)->create([ 'password' => bcrypt('laraveltest123') //パスワードは好きな言葉で大丈夫です ]); // 認証されないことを確認 $this->assertFalse(Auth::check()); // ログインを実行 $response = $this->post('login', [ 'email' => $user->email, 'password' => 'laraveltest123' //先ほど設定したパスワードを入力 ]); // 認証されていることを確認 $this->assertTrue(Auth::check()); // ログイン後にホームページにリダイレクトされるのを確認 $response->assertRedirect('home'); //作成したサイトでログイン後にリダイレクトされるルート情報を記述 } }4.マイグレーションの実行
下記コマンドを実行するとテスト用のデータベースにテーブルが作成されます。
--database==testingを忘れないでください。この記述によりdatabase.phpに用意したtesing設定で実行されます。$ php artisan migrate:refresh --database==testing5.テストの実行
$ vendor/bin/phpunit以上でテスト完了です!
作成したデータベースのUserテーブルには新規レコードが登録されていて正しくログイン出来ています。
- 投稿日:2020-12-04T12:25:50+09:00
Laravel(6.x)でのセキュアそうなAPI認証を実装する
こんにちは。M2です。
Laravel Advent Calendar 2020の4日目を担当させていただきます。概要
Laravelのtokenを使ったAPI認証でなるべくセキュアそうな認証方法を実装してみたので、備忘録がてらに共有します。
Laravelパスポートを使いたいところですが、使わない方向です。
環境
- Laravel 6.x
- フロント側は(Vue.js 3)だけでど何でも良い
方向性
- DBにapi_tokenを用意して、それをユーザ認証用のtokenとする
- DBに保存しているapi_tokenはHash化して保存する
- ブラウザ側ではapi_tokenを暗号化してCookieに保存する
- JavaScript側でapi_tokenにアクセスできないように、Laravel側でhttponlyに設定する
- ずっと同じapi_tokenも嫌なので、ログイン毎に更新する
api_tokenカラムの導入
$ php artisan make:migration add_api_token_to_users_table --table=usersdatabase/migrations/${date}_add_api_token_to_users_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class AddApiTokenToUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->string('api_token', 80)->after('password') ->unique() ->nullable() ->default(null); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('api_token'); }); } }$ php artisan migrateDBに保存しているapi_tokenはHash化して保存する
config/auth.php<?php return [ ~~ 省略 ~~ 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', 'hash' => true, // ここをtrueにする ], ], ~~ 省略 ~~ブラウザ側ではapi_tokenを暗号化してCookieに保存する
app/Http/Controllers/Auth/RegisterController.php/** * Create a new user instance after a valid registration. * * @param array $data * @return \App\User */ protected function create(array $data) { $token = $this->generateApiTokenAndSetCookie(); return User::create([ . . . 'api_token' => hash('sha256', $token), ]); } }//$this->generateApiTokenAndSetCookie();の中身 use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Str; use Illuminate\Support\Facades\Cookie; . . . public function generateApiTokenAndSetCookie() { $token = Str::random(80); $encryptToken = Crypt::encrypt($token); Cookie::queue(Cookie::forget('api_token')); Cookie::queue('api_token', $encryptToken, '10000000'); return $token; }api_tokenの暗号化は手動で設定しておく
app/Http/Middleware/EncryptCookies.php<?php namespace App\Http\Middleware; use Illuminate\Cookie\Middleware\EncryptCookies as Middleware; class EncryptCookies extends Middleware { /** * The names of the cookies that should not be encrypted. * * @var array */ protected $except = [ 'api_token' ]; }JavaScript側でapi_tokenにアクセスできないように、Laravel側でhttponlyに設定する
# .env . . . SESSION_SECURE_COOKIE=trueずっと同じapi_tokenも嫌なので、ログイン毎に更新する
app/Http/Controllers/Auth/LoginController.php. . . public function login(Request $request) { . . . if ($this->attemptLogin($request)) { // Cookieのapi_tokenの更新して、DBのapi_tokenも更新する $token = $this->generateApiTokenAndSetCookie(); User::where('email', $request->email) ->update(['api_token' => hash('sha256', $token)]); return $this->sendLoginResponse($request); } . . . }いざAPI認証
HogeService.php// Serviceクラスでも何でも好きなところで... <?php namespace App\Services; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Cookie; . . . public function getUser(string $url): ?string { $client = new \GuzzleHttp\Client(); if (Cookie::get('api_token') === null) { return null; } $decryptToken = Crypt::decrypt(Cookie::get('api_token'), true); $response = $client->request( 'GET', $url, ['verify' => true] ); $response = $response->getBody()->getContents(); $result = json_decode($response); if (json_last_error() === JSON_ERROR_NONE) { return json_encode($result); } else { return null; } } }Controllerから上記のAPIにアクセスする
HogeGetUserController.php<?php . . . . use App\Services\AuthApi; /** * @group UserInfo API * * APIs 会員登録系 */ class HogeGetUserController extends Controller { protected $authApi; public function __construct(AuthApi $authApi) { $this->authApi = $authApi; } . . . public function userInfo(Request $request) { $json = $this->authApi->getUser(route(('api.requestUser'))); return $json; } public function requestUser(Request $request) { return $request->user(); }ルーティングの設定
routes/api.php. . . Route::get('userInfo', 'HogeGetUserController@userInfo'); Route::group(['middleware' => 'auth:api'], function () { Route::get('requestUser', 'HogeGetUserController@requestUser')->name('api.requestUser'); });これで、ログイン後に/api/userInfoにアクセスすれば、Userの情報が取得できる。
Vue.jsから呼び出す
hoge.vue<template> . . . </template> <script lang="ts"> axios.get('/api/userInfo') </script>テストする
tests/Feature/UserTest.php<?php namespace Tests\Feature; use App\User; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Str; use Tests\TestCase; class UserTest extends TestCase { use DatabaseTransactions; /** * * @return void */ public function test_getユーザー取得() { $token = Str::random(80); $user = factory(User::class)->create([ 'api_token' => hash('sha256', $token), ]); $response = $this->actingAs($user)->json('GET', "/api/requestUser?api_token={$token}"); $json = json_decode($response->getContent()); $this->assertObjectHasAttribute('id', $json); $this->assertObjectHasAttribute('name', $json); $this->assertObjectHasAttribute('email', $json); }問題点とか
- JavaScriptからCookieにアクセスできなくても、正しいapi_tokenを叩けば、誰でも認証情報にアクセスできちゃう...
- ログイン毎にapi_tokenが更新されるとはいえ、誰か良い方法を教えてクレメンス
参考
公式:Laravel 6.x API認証
Laravel6.x + Vue.js2(TypeScript)のSPAでシンプルなAPI認証を実装する
- 投稿日:2020-12-04T09:58:05+09:00
php8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く
この記事はFusic Advent Calender の4日目の記事です。
昨日は@ryu022304さんのGo+serverlessでSQS→Lambda→SESの記事でした。
Serverless Framework便利ですよね。Goはまだあまりやったことないので、ちゃんと時間作ってやってみたいですね。
さて、この記事は、「php8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く」という記事です。
php8、Laravel8を触ってみたかったのと、Laravel-Generatorを案件で使ってみた知見の共有で、記事を書きます。
Laravel-Generatorはこちらですね。
InfyOmLabsというところが作っているみたいです。
一度案件で使ってみて、驚くほど一瞬でCRUD機能が作成できるので、早く動くものを作りたい時にはとても便利です。
最初の状態
Laravel8のプロジェクトをたてたところから始めます。ディレクトリ構成はこんな感じ。
├── README.md ├── app ├── artisan ├── bootstrap ├── composer.json ├── composer.lock ├── config ├── coverage ├── database ├── docker-compose.yml ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── server ├── server.php ├── storage ├── tests ├── vendor └── webpack.mix.jsLaravel8の最初の画面、初めてみました。
ちょっと暗い感じになりましたね。phpはphp:8.0-rc-fpmのDockerイメージを使いました。
$ php -v PHP 8.0.0RC5 (cli) (built: Nov 25 2020 01:10:25) ( NTS ) Copyright (c) The PHP Group Zend Engine v4.0.0-dev, Copyright (c) Zend TechnologiesLaravelのバージョンは
$ php artisan --version Laravel Framework 8.17.08.17.0です。
Laravel-Generatorをインストールする
早速はじめていきます。
ドキュメント通りにインストール。
"require": { "php": "^7.3|^8.0", "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", "laravel/framework": "^8.12", "laravel/tinker": "^2.5", ↓以下の三つを追加↓ "infyomlabs/laravel-generator": "8.0.x-dev", "laravelcollective/html": "^6.2", "infyomlabs/coreui-templates": "8.0.x-dev" },今回はAdminLTEではなく、CoreUIを使ってみます。
$ composer update
インストールに失敗。
Problem 1 - Root composer.json requires infyomlabs/laravel-generator 8.0.x-dev -> satisfiable by infyomlabs/laravel-generator[8.0.x-dev]. - infyomlabs/laravel-generator 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement. Problem 2 - Root composer.json requires infyomlabs/coreui-templates 8.0.x-dev -> satisfiable by infyomlabs/coreui-templates[8.0.x-dev]. - infyomlabs/coreui-templates 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement.Laravel-GeneratorがまたPHP8に対応していないみたいですねー。
きっと動作すると信じて、強引に入れてみますw$ composer update --ignore-platform-reqsインストール完了しました。
Laravel-Generatorの初期設定
app.phpにaliasを追記
こちらもドキュメント通り、app.phpにaliasを追加します。
'Str' => Illuminate\Support\Str::class, 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, // 追加 'Form' => Collective\Html\FormFacade::class, 'Html' => Collective\Html\HtmlFacade::class, 'Flash' => Laracasts\Flash\Flash::class,vendor:publishコマンドを実行
$ php artisan vendor:publish --provider="InfyOm\Generator\InfyOmGeneratorServiceProvider"config/infyom/laravel_generator.php に設定ファイルが生成されます。
デフォルト設定だと、テンプレートがAdminLTEになってしまっているので、CoreUIに修正します。
/* |-------------------------------------------------------------------------- | Templates |-------------------------------------------------------------------------- | */ 'templates' => 'coreui-templates',Laravel-GeneratorのPublishコマンドを実行
$ php artisan infyom:publish
Laravel UIで認証を作成する
CRUD作成の前に、Laravel6から使われているLaravelUIを用いて、簡単に認証機能を作成します。
ドキュメント通りコマンド実行。
$ composer require laravel/ui:^3.0 --ignore-platform-reqs $ php artisan ui bootstrap --authこの状態で、npm install && npm run devを実行すれば、ログイン画面が確認できますが、
今回は、Laravel-Genaratorのコマンドで上書きしてしまいます。$ php artisan infyom.publish:layout --localized上書きしますか?というメッセージが表示されますが、全て「y」を押して大丈夫です。
Login画面に行くと、CoreUIのログイン画面が表示されます。
多言語対応ができていないので、日本語ファイルを追加してあげましょう。
多言語対応
app.phpのlocaleの設定を日本語に変更。(ついでにTimezoneも変えときましょう)
'timezone' => 'Asia/Tokyo', 'locale' => 'ja',resources/lang/ja配下に日本語ファイルを設置します。
auth.php<?php return [ 'failed' => 'メールアドレス、もしくはパスワードが違います', 'throttle' => 'ログイン試行回数がしきい値を超えました。 時間をおいてログインしてください。', 'login' => 'ログインしました', 'logout' => 'ログアウトしました', 'notVerified' => 'メールが認証されていません', 'verified' => 'メールの認証に成功しました', 'alreadyVerified' => '既に認証済みのメールアドレスです', 'sendVerifyMail' => '認証メールを再送しました', 'noUser' => '入力されたメールアドレスのユーザーが存在しません', 'email' => 'メールアドレス', 'password' => 'パスワード', 'confirm_password' => 'パスワードを確認', 'sign_in' => 'ログイン', 'admin_sign_in' => '管理者ログイン', 'sign_out' => 'ログアウト', 'register' => 'ユーザー登録', 'full_name' => '氏名', 'login' => [ 'forgot_password' => 'パスワードを忘れた時', ], 'forgot_password' => [ 'send_pwd_reset' => 'パスワード変更リンクを送る', ], 'reset_password' => [ 'title' => 'パスワードを変更', 'enter_email' => 'メールアドレスを入力してください' ], 'registration' => [ 'have_membership' => '既にアカウントを持っている方はこちら' ] ];必要なさそうな文言はresources/views/auth/login.blade.phpやresources/views/auth/register.blade.phpを削って対応。
以下のように表示されました。
ユーザー登録・ログイン
マイグレーションを走らせれば、ユーザー登録、ログインができます。
$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (117.26ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (84.44ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (205.19ms)ログインした後はこんな感じの画面に。
まっさら何もないですね。
ここから本題のCRUDを作成していきます。
Laravel-GeneratorでCRUDを作成する
今回は簡単な投稿をつくるためのCRUDを作ってみます。
Laravel-Generatorのコマンドを実行。
Model名ArticleのCRUDを作成します。
$ php artisan infyom:scaffold Article Specify fields for the model (skip id & timestamp fields, we will add it automatically) Read docs carefully to specify field inputs) Enter "exit" to finish Field: (name db_type html_type options) []: >カラムのDBタイプとHTMLタイプを聞かれるので、答えます。その後Validationも聞かれます。
HTMLタイプの候補に関しては、Laravel-Generatorのサイトに書いてあります。
http://infyom.com/open-source/laravelgenerator/docs/8.0/fields-input-guideValidationの書き方についてはこちらに。
http://infyom.com/open-source/laravelgenerator/docs/8.0/getting-started#validations今回は以下のような感じでやってみました。
Field: (name db_type html_type options) []: > title string text Enter validations: []: > required|max:20 Field: (name db_type html_type options) []: > body text text Enter validations: []: > required Field: (name db_type html_type options) []: > user_id integer:unsigned:foreign,users,id select Enter validations: []: > required|numeric Field: (name db_type html_type options) []: > exit自動でMigrationファイルやRepository、Model、Controller、Viewなどが作成されます。
Migration created: 2020_12_03_180423_create_articles_table.php Model created: Article.php Repository created: ArticleRepository.php Factory created: ArticleFactory.php Create Request created: CreateArticleRequest.php Update Request created: UpdateArticleRequest.php Controller created: ArticleController.php Generating Views... table.blade.php created index.blade.php created field.blade.php created create.blade.php created edit.blade.php created show_fields.blade.php created show.blade.php createdMigrationの実行を聞かれて、実行してみましたが、失敗。。
UsersのMigrationファイルの問題みたいだったので、修正します。2014_10_12_000000_create_users_table<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }マイグレーションを再度実行
$ php artisan migrate:refresh Rolling back: 2020_12_03_180423_create_articles_table Rolled back: 2020_12_03_180423_create_articles_table (125.45ms) Rolling back: 2019_08_19_000000_create_failed_jobs_table Rolled back: 2019_08_19_000000_create_failed_jobs_table (34.11ms) Rolling back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_100000_create_password_resets_table (33.85ms) Rolling back: 2014_10_12_000000_create_users_table Rolled back: 2014_10_12_000000_create_users_table (33.50ms) Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (85.98ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (93.14ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (126.64ms) Migrating: 2020_12_03_180423_create_articles_table Migrated: 2020_12_03_180423_create_articles_table (198.16ms)うまくいきました。
ログインユーザーのみが記事確認、作成できるようにする
生成されたRouteのままだと、ArticleのResourceがAuth Middlewareの外にあるので、Routeをいじってあげます。
web.phpRoute::middleware('auth')->group(function () { Route::resource('articles', App\Http\Controllers\ArticleController::class); });画面を確認
Article一覧画面ができてます。
記事の作成、編集、削除もできますね。
コマンド一つでCRUDが作成できました。
ユニットテストを書く
最後にユニットテストを書いていきます。
まずは何もしない状態でテストを実行してみます。Laravelのデフォルトのテストが実行されます。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:01.291, Memory: 20.00 MB OK (2 tests, 2 assertions)ArticleRepositoryのテストを書く
記事のCRUDが動いていることを確認するテストを書きます。
せっかくPHP8を使っているので、Repositoryのメソッドの引数には、無駄に名前付き引数を使ってみました。ArticleRepositoryTest<?php namespace Tests\Unit\Repositories; use App\Models\Article; use App\Models\User; use App\Repositories\ArticleRepository; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ArticleRepositoryTest extends TestCase { use RefreshDatabase; private $articleRepository; public function setUp(): void { parent::setUp(); $this->articleRepository = app(ArticleRepository::class); } /** * 記事全取得のテスト * @return void */ public function testPaginate(): void { // Laravel8以降、Globalなfactory関数は廃止され、Modelから呼び出す形となりました Article::factory(10)->create(); $articles = $this->articleRepository->paginate(perPage: 10);; $this->assertInstanceOf(Collection::class, $articles->getCollection()); $this->assertCount(10, $articles); } /** * 記事一件取得のテスト * @return void */ public function testGetById(): void { $articleId = 1; Article::factory()->create([ 'id' => $articleId ]); $article = $this->articleRepository->find(id: $articleId); $this->assertInstanceOf(Article::class, $article); } /** * 記事作成編集削除のテスト * @return void */ public function testCreateAndUpdateAndDelete(): void { // Create $data = [ 'title' => 'タイトル', 'body' => '内容', 'user_id' => User::factory()->create()->id, ]; $createdArticle = $this->articleRepository->create(input: $data); $this->assertInstanceOf(Article::class, $createdArticle); $this->assertEquals($data['title'], $createdArticle->title); // Update $updated = [ 'title' => '更新されたタイトル', ]; $updatedArticle = $this->articleRepository->update(input: $updated, id: $createdArticle->id); $this->assertInstanceOf(Article::class, $updatedArticle); $this->assertEquals($updatedArticle['title'], $updatedArticle->title); // Delete $this->articleRepository->delete(id: $updatedArticle->id); $this->assertNull($this->articleRepository->find($updatedArticle->id)); } }factoryをModelから呼び出すところはLaravel8で変わった点ですね。
テスト実行
test/Repositories配下を実行するように、phpunit.xmlを修正してあげます。
phpunit.xml</testsuite> <testsuite name="Feature"> <directory suffix="Test.php">./tests/Feature</directory> </testsuite> ↓追加 <testsuite name="Repositories"> <directory suffix="Test.php">./tests/Repositories</directory> </testsuite>テストを実行します。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. ..... 5 / 5 (100%) Time: 00:02.388, Memory: 30.00 MB OK (5 tests, 10 assertions)通りましたね。
権限のテストを書く
ログインしたユーザーだけが記事一覧や、記事作成ができる形になっているかをテストします。
AuthTest.php<?php namespace Tests\Unit; use App\Models\Article; use App\Models\User; use Auth; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class AuthTest extends TestCase { use RefreshDatabase; private Article $article; public function setUp(): void { parent::setUp(); $this->article = Article::factory()->create(); } /** * 記事へのアクセステスト * * @return void */ public function testArticleAccess(): void { // ログイン前はログイン画面にリダイレクトされる $this->get(route('articles.index'))->assertRedirect(route('login')); $this->get(route('articles.create'))->assertRedirect(route('login')); $this->get(route('articles.show', $this->article))->assertRedirect(route('login')); $this->get(route('articles.edit', $this->article))->assertRedirect(route('login')); $user = $this->actingAs( User::factory()->create() ); // ログイン後は正常にアクセス可能 $user->get(route('articles.index'))->assertStatus(200); $user->get(route('articles.create'))->assertStatus(200); $user->get(route('articles.show', $this->article))->assertStatus(200); $user->get(route('articles.edit', $this->article))->assertStatus(200); Auth::logout(); // ログアウト後はログイン画面にリダイレクトされる $this->get(route('articles.index'))->assertRedirect(route('login')); $this->get(route('articles.create'))->assertRedirect(route('login')); $this->get(route('articles.show', $this->article))->assertRedirect(route('login')); $this->get(route('articles.edit', $this->article))->assertRedirect(route('login')); } }実行してみます。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. ...... 6 / 6 (100%) Time: 00:02.842, Memory: 30.00 MB OK (6 tests, 30 assertions)無事通りましたね!
実行対象のテストを絞りたい際は$ ./vendor/bin/phpunit --filter='AuthTest'のような形で絞ることも可能です。
長くなりましたが、これでLaravel-Generatorを使って簡単にCRUDを作成し、ユニットテストまで書くことができました。
PHP8やLaravel8は今回初めて使いましたが、もっといろんな機能があるので、早く色々試してみたいですね。
最後まで読んでいただき、ありがとうございました!!
明日は @gorogoroyasu の記事ですね。機械学習系の記事かな?楽しみです!
- 投稿日:2020-12-04T09:58:05+09:00
PHP8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く
この記事はFusic Advent Calenderの4日目の記事です。
昨日は@ryu022304さんのGo+serverlessでSQS→Lambda→SESの記事でした。
Serverless Framework便利ですよね。Goはまだあまりやったことないので、ちゃんと時間作ってやってみたいですね。
さて、この記事は、「PHP8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く」という記事です。
PHP8とLaravel8を触ってみたかったのと、Laravel-Generatorを案件で使ってみた知見の共有で、記事を書きます。
Laravel-Generatorはこちらですね。
InfyOmLabsというところが作っているみたいです。
一度案件で使ってみて、驚くほど一瞬でCRUD機能が作成できるので、早く動くものを作りたい時にはとても便利です。
最初の状態
Laravel8のプロジェクトをたてたところから始めます。ディレクトリ構成はこんな感じ。
├── README.md ├── app ├── artisan ├── bootstrap ├── composer.json ├── composer.lock ├── config ├── coverage ├── database ├── docker-compose.yml ├── package.json ├── phpunit.xml ├── public ├── resources ├── routes ├── server ├── server.php ├── storage ├── tests ├── vendor └── webpack.mix.jsLaravel8の最初の画面、初めてみました。
ちょっと暗い感じになりましたね。PHPはphp:8.0-rc-fpmのDockerイメージを使いました。
$ php -v PHP 8.0.0RC5 (cli) (built: Nov 25 2020 01:10:25) ( NTS ) Copyright (c) The PHP Group Zend Engine v4.0.0-dev, Copyright (c) Zend TechnologiesLaravelのバージョンは
$ php artisan --version Laravel Framework 8.17.08.17.0です。
Laravel-Generatorをインストールする
早速はじめていきます。
ドキュメント通りにインストール。
"require": { "php": "^7.3|^8.0", "fideloper/proxy": "^4.4", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", "laravel/framework": "^8.12", "laravel/tinker": "^2.5", ↓以下の三つを追加↓ "infyomlabs/laravel-generator": "8.0.x-dev", "laravelcollective/html": "^6.2", "infyomlabs/coreui-templates": "8.0.x-dev" },今回はAdminLTEではなく、CoreUIを使ってみます。
$ composer update
インストールに失敗。
Problem 1 - Root composer.json requires infyomlabs/laravel-generator 8.0.x-dev -> satisfiable by infyomlabs/laravel-generator[8.0.x-dev]. - infyomlabs/laravel-generator 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement. Problem 2 - Root composer.json requires infyomlabs/coreui-templates 8.0.x-dev -> satisfiable by infyomlabs/coreui-templates[8.0.x-dev]. - infyomlabs/coreui-templates 8.0.x-dev requires php ^7.3 -> your php version (8.0.0RC5) does not satisfy that requirement.Laravel-GeneratorがまたPHP8に対応していないみたいですねー。
きっと動作すると信じて、強引に入れてみますw$ composer update --ignore-platform-reqsインストール完了しました。
Laravel-Generatorの初期設定
app.phpにaliasを追記
こちらもドキュメント通り、app.phpにaliasを追加します。
'Str' => Illuminate\Support\Str::class, 'URL' => Illuminate\Support\Facades\URL::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, // 追加 'Form' => Collective\Html\FormFacade::class, 'Html' => Collective\Html\HtmlFacade::class, 'Flash' => Laracasts\Flash\Flash::class,vendor:publishコマンドを実行
$ php artisan vendor:publish --provider="InfyOm\Generator\InfyOmGeneratorServiceProvider"config/infyom/laravel_generator.php に設定ファイルが生成されます。
デフォルト設定だと、テンプレートがAdminLTEになってしまっているので、CoreUIに修正します。
/* |-------------------------------------------------------------------------- | Templates |-------------------------------------------------------------------------- | */ 'templates' => 'coreui-templates',Laravel-GeneratorのPublishコマンドを実行
$ php artisan infyom:publish
Laravel UIで認証を作成する
CRUD作成の前に、Laravel6から使われているLaravelUIを用いて、簡単に認証機能を作成します。
ドキュメント通りコマンド実行。
$ composer require laravel/ui:^3.0 --ignore-platform-reqs $ php artisan ui bootstrap --authこの状態で、npm install && npm run devを実行すれば、ログイン画面が確認できますが、
今回は、Laravel-Genaratorのコマンドで上書きしてしまいます。$ php artisan infyom.publish:layout --localized上書きしますか?というメッセージが表示されますが、全て「y」を押して大丈夫です。
Login画面に行くと、CoreUIのログイン画面が表示されます。
多言語対応ができていないので、日本語ファイルを追加してあげましょう。
多言語対応
app.phpのlocaleの設定を日本語に変更。(ついでにTimezoneも変えときましょう)
'timezone' => 'Asia/Tokyo', 'locale' => 'ja',resources/lang/ja配下に日本語ファイルを設置します。
auth.php<?php return [ 'failed' => 'メールアドレス、もしくはパスワードが違います', 'throttle' => 'ログイン試行回数がしきい値を超えました。 時間をおいてログインしてください。', 'login' => 'ログインしました', 'logout' => 'ログアウトしました', 'notVerified' => 'メールが認証されていません', 'verified' => 'メールの認証に成功しました', 'alreadyVerified' => '既に認証済みのメールアドレスです', 'sendVerifyMail' => '認証メールを再送しました', 'noUser' => '入力されたメールアドレスのユーザーが存在しません', 'email' => 'メールアドレス', 'password' => 'パスワード', 'confirm_password' => 'パスワードを確認', 'sign_in' => 'ログイン', 'admin_sign_in' => '管理者ログイン', 'sign_out' => 'ログアウト', 'register' => 'ユーザー登録', 'full_name' => '氏名', 'login' => [ 'forgot_password' => 'パスワードを忘れた時', ], 'forgot_password' => [ 'send_pwd_reset' => 'パスワード変更リンクを送る', ], 'reset_password' => [ 'title' => 'パスワードを変更', 'enter_email' => 'メールアドレスを入力してください' ], 'registration' => [ 'have_membership' => '既にアカウントを持っている方はこちら' ] ];必要なさそうな文言はresources/views/auth/login.blade.phpやresources/views/auth/register.blade.phpを削って対応。
以下のように表示されました。
ユーザー登録・ログイン
マイグレーションを走らせれば、ユーザー登録、ログインができます。
$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (117.26ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (84.44ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (205.19ms)ログインした後はこんな感じの画面に。
まっさら何もないですね。
ここから本題のCRUDを作成していきます。
Laravel-GeneratorでCRUDを作成する
今回は簡単な投稿をつくるためのCRUDを作ってみます。
Laravel-Generatorのコマンドを実行。
Model名ArticleのCRUDを作成します。
$ php artisan infyom:scaffold Article Specify fields for the model (skip id & timestamp fields, we will add it automatically) Read docs carefully to specify field inputs) Enter "exit" to finish Field: (name db_type html_type options) []: >カラムのDBタイプとHTMLタイプを聞かれるので、答えます。その後Validationも聞かれます。
HTMLタイプの候補に関しては、Laravel-Generatorのサイトに書いてあります。
http://infyom.com/open-source/laravelgenerator/docs/8.0/fields-input-guideValidationの書き方についてはこちらに。
http://infyom.com/open-source/laravelgenerator/docs/8.0/getting-started#validations今回は以下のような感じでやってみました。
Field: (name db_type html_type options) []: > title string text Enter validations: []: > required|max:20 Field: (name db_type html_type options) []: > body text text Enter validations: []: > required Field: (name db_type html_type options) []: > user_id integer:unsigned:foreign,users,id select Enter validations: []: > required|numeric Field: (name db_type html_type options) []: > exit自動でMigrationファイルやRepository、Model、Controller、Viewなどが作成されます。
Migration created: 2020_12_03_180423_create_articles_table.php Model created: Article.php Repository created: ArticleRepository.php Factory created: ArticleFactory.php Create Request created: CreateArticleRequest.php Update Request created: UpdateArticleRequest.php Controller created: ArticleController.php Generating Views... table.blade.php created index.blade.php created field.blade.php created create.blade.php created edit.blade.php created show_fields.blade.php created show.blade.php createdMigrationの実行を聞かれて、実行してみましたが、失敗。。
UsersのMigrationファイルの問題みたいだったので、修正します。2014_10_12_000000_create_users_table<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }マイグレーションを再度実行
$ php artisan migrate:refresh Rolling back: 2020_12_03_180423_create_articles_table Rolled back: 2020_12_03_180423_create_articles_table (125.45ms) Rolling back: 2019_08_19_000000_create_failed_jobs_table Rolled back: 2019_08_19_000000_create_failed_jobs_table (34.11ms) Rolling back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_100000_create_password_resets_table (33.85ms) Rolling back: 2014_10_12_000000_create_users_table Rolled back: 2014_10_12_000000_create_users_table (33.50ms) Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (85.98ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (93.14ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (126.64ms) Migrating: 2020_12_03_180423_create_articles_table Migrated: 2020_12_03_180423_create_articles_table (198.16ms)うまくいきました。
ログインユーザーのみが記事確認、作成できるようにする
生成されたRouteのままだと、ArticleのResourceがAuth Middlewareの外にあるので、Routeをいじってあげます。
web.phpRoute::middleware('auth')->group(function () { Route::resource('articles', App\Http\Controllers\ArticleController::class); });画面を確認
Article一覧画面ができてます。
記事の作成、編集、削除もできますね。
コマンド一つでCRUDが作成できました。
ユニットテストを書く
最後にユニットテストを書いていきます。
まずは何もしない状態でテストを実行してみます。Laravelのデフォルトのテストが実行されます。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:01.291, Memory: 20.00 MB OK (2 tests, 2 assertions)ArticleRepositoryのテストを書く
記事のCRUDが動いていることを確認するテストを書きます。
せっかくPHP8を使っているので、Repositoryのメソッドの引数には、無駄に名前付き引数を使ってみました。ArticleRepositoryTest<?php namespace Tests\Unit\Repositories; use App\Models\Article; use App\Models\User; use App\Repositories\ArticleRepository; use Illuminate\Database\Eloquent\Collection; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ArticleRepositoryTest extends TestCase { use RefreshDatabase; private $articleRepository; public function setUp(): void { parent::setUp(); $this->articleRepository = app(ArticleRepository::class); } /** * 記事全取得のテスト * @return void */ public function testPaginate(): void { // Laravel8以降、Globalなfactory関数は廃止され、Modelから呼び出す形となりました Article::factory(10)->create(); $articles = $this->articleRepository->paginate(perPage: 10);; $this->assertInstanceOf(Collection::class, $articles->getCollection()); $this->assertCount(10, $articles); } /** * 記事一件取得のテスト * @return void */ public function testGetById(): void { $articleId = 1; Article::factory()->create([ 'id' => $articleId ]); $article = $this->articleRepository->find(id: $articleId); $this->assertInstanceOf(Article::class, $article); } /** * 記事作成編集削除のテスト * @return void */ public function testCreateAndUpdateAndDelete(): void { // Create $data = [ 'title' => 'タイトル', 'body' => '内容', 'user_id' => User::factory()->create()->id, ]; $createdArticle = $this->articleRepository->create(input: $data); $this->assertInstanceOf(Article::class, $createdArticle); $this->assertEquals($data['title'], $createdArticle->title); // Update $updated = [ 'title' => '更新されたタイトル', ]; $updatedArticle = $this->articleRepository->update(input: $updated, id: $createdArticle->id); $this->assertInstanceOf(Article::class, $updatedArticle); $this->assertEquals($updatedArticle['title'], $updatedArticle->title); // Delete $this->articleRepository->delete(id: $updatedArticle->id); $this->assertNull($this->articleRepository->find($updatedArticle->id)); } }factoryをModelから呼び出すところはLaravel8で変わった点ですね。
テスト実行
test/Repositories配下を実行するように、phpunit.xmlを修正してあげます。
phpunit.xml</testsuite> <testsuite name="Feature"> <directory suffix="Test.php">./tests/Feature</directory> </testsuite> ↓追加 <testsuite name="Repositories"> <directory suffix="Test.php">./tests/Repositories</directory> </testsuite>テストを実行します。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. ..... 5 / 5 (100%) Time: 00:02.388, Memory: 30.00 MB OK (5 tests, 10 assertions)通りましたね。
権限のテストを書く
ログインしたユーザーだけが記事一覧や、記事作成ができる形になっているかをテストします。
AuthTest.php<?php namespace Tests\Unit; use App\Models\Article; use App\Models\User; use Auth; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class AuthTest extends TestCase { use RefreshDatabase; private Article $article; public function setUp(): void { parent::setUp(); $this->article = Article::factory()->create(); } /** * 記事へのアクセステスト * * @return void */ public function testArticleAccess(): void { // ログイン前はログイン画面にリダイレクトされる $this->get(route('articles.index'))->assertRedirect(route('login')); $this->get(route('articles.create'))->assertRedirect(route('login')); $this->get(route('articles.show', $this->article))->assertRedirect(route('login')); $this->get(route('articles.edit', $this->article))->assertRedirect(route('login')); $user = $this->actingAs( User::factory()->create() ); // ログイン後は正常にアクセス可能 $user->get(route('articles.index'))->assertStatus(200); $user->get(route('articles.create'))->assertStatus(200); $user->get(route('articles.show', $this->article))->assertStatus(200); $user->get(route('articles.edit', $this->article))->assertStatus(200); Auth::logout(); // ログアウト後はログイン画面にリダイレクトされる $this->get(route('articles.index'))->assertRedirect(route('login')); $this->get(route('articles.create'))->assertRedirect(route('login')); $this->get(route('articles.show', $this->article))->assertRedirect(route('login')); $this->get(route('articles.edit', $this->article))->assertRedirect(route('login')); } }実行してみます。
$ ./vendor/bin/phpunit PHPUnit 9.4.4 by Sebastian Bergmann and contributors. ...... 6 / 6 (100%) Time: 00:02.842, Memory: 30.00 MB OK (6 tests, 30 assertions)無事通りましたね!
実行対象のテストを絞りたい際は$ ./vendor/bin/phpunit --filter='AuthTest'のような形で絞ることも可能です。
長くなりましたが、これでLaravel-Generatorを使って簡単にCRUDを作成し、ユニットテストまで書くことができました。
PHP8やLaravel8は今回初めて使いましたが、もっといろんな機能があるので、早く色々試してみたいですね。
最後まで読んでいただき、ありがとうございました!!
明日は @gorogoroyasu の記事ですね。機械学習系の記事かな?楽しみです!
- 投稿日:2020-12-04T01:39:18+09:00
HerokuでS3を使うアプリで500エラーが出た時の設定
heroku側のconfigにS3のキーをセット
Laravelで作ったアプリをHerokuにデプロイしたら500Errorが出た話ですが、
AWS S3を使用している場合には、AWSのLaravelプロジェクト内の「.env」だけではなく、
Herokuのconfig側も設定が必要heroku config:set AWS_ACCESS_KEY_ID=AAAAAAAAAAAAAA AWS_SECRET_ACCESS_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AWS_DEFAULT_REGION=ap-southeast-1 AWS_BUCKET=AAAAAAAA -a アプリ名的な感じで設定が可能