- 投稿日:2021-06-20T22:41:51+09:00
PHP、CakePHP、LaravelのそれぞれのXSS対策
XSSとは XSS(クロスサイトスクリプティング)は、脆弱性のあるWebサイトに対して、悪意のある不正なスクリプトを挿入する攻撃のことを示します。 挿入したスクリプトを、サイトに訪れたユーザーに実行させる攻撃のため、ウェブサイトそのものではなく、サイトを利用するユーザーに被害が発生します。 XSS対策を行っていないサイトでは、どのように攻撃されるのか例を見てみます。 XSS対策をしていないサイトの例とXSS攻撃方法 「投稿する」ボタンを押下すると、入力データが保存された後、次の投稿一覧画面に表示されるサイトとします(今回テーマから逸れてしまうので、データベースに保存する処理を省略しています)。 例:投稿フォームと一覧画面 comment_index.php <h1>投稿フォーム</h1> <form action="comment_index.php" method="post"> <p>コメント</p> <input type="text" name="comment"> <input type="submit" value="投稿する"> </form> <h2>投稿一覧</h2> <?php $sql = 'SELECT * FROM comment; $comments = $dbh->query($sql)->fetchAll(PDO::FETCH_ASSOC); foreach ($comments as $comment)){ echo '<p>'.$comment['comment'].'</p>'.PHP_EOL; } ?> 図:「投稿する」ボタンをクリックしたらコメント一覧の下に表示される このようなXSS対策を行っていないサイトでは、入力フォームにJSコードを書き込まれると、そのままコードが出力されるため、読み込み時に動作してしまいます。 以下のようなコードを書き込むと、当該ページへアクセスした利用者全員に、alertメッセージが強制的に表示されてしまいます。 <script>window.alert('このメッセージを表示したユーザー全員にアラートメッセージを表示');</script> 図:サイトのユーザー全員にアラートメッセージが表示されてしまう このような脆弱性のある環境では、次のような3つの攻撃が予想されます。 攻撃①:サイトの改ざん JSのコードの差し込みができると、画面の表示内容を変更することができます。 例のようなコメント一覧サイトに、次のようなコードを送信することで、全く別の内容を表示させる事ができます。 改ざんすることで、他にもECサイトで表示価格を変更させたり、会員サイトで再度ログイン画面などを出すことで個人情報を盗み出すといった被害が考えられます。 <script>document.getElementsByTagName("h1")[0].innerHTML = "ウイルスに感染しました!";document.getElementsByTagName("h2")[0].innerHTML = "ウイルスに感染しました!以下のサイトで、大至急セキュリティーソフトをダウンロードしてください";document.getElementsByTagName("p")[0].innerHTML = "";document.getElementsByTagName('h2')[0].insertAdjacentHTML('afterend','<a href=”http://罠サイト”>ダウンロードリンク</div>');</script> 図:コメント一覧サイトの書き換え 攻撃②:フィッシング詐欺 また、以下のようなにJSのコードを送信することで、アクセスと同時に偽サイトに転送させることが可能です。 <script>window.open('http://罠サイト');</script> 本物そっくりのサイトを作成してそこに転移させることで、個人情報を入力させて盗み出すといった被害が考えられます。 攻撃③:セッションハイジャック コンピューター通信で利用する「セッション」を奪い、「なりすまし」で不正アクセスをする行為です。 方法に関してはこちらの方が詳しく書かれていたので、リンクを載せておきます。 対策:サニタイズ(無効化)する XSSの対策は、サニタイズが有効です。 サニタイズとは、ユーザからフォームの値を取得した際に、htmlで特別な意味を持つ以下の記号を別の記号に置換することです。 置換することで、先程のようなスクリプトが実行できなくなるため、被害を防ぐことができます。 記号 サニタイズ < < > > & & " " ' ' PHPの場合:htmlspecialchars()を用いる <?php foreach ($comments as $comment)){ $comment = htmlspecialchars($comment['comment'], ENT_QUOTES, 'UTF-8'); echo '<p>'.$comment.'</p>'.PHP_EOL; } CakePHPの場合:h()メソッドを使用する htmlspecialchars()と同義となっています。 <?php foreach ($comments as $comment)){ echo '<p>'.h($comment['comment']).'</p>'.PHP_EOL; } また入力フォームでFormHelperを使用することでも、自動でサニタイズしてくれます。 <?php echo $this->Form->control('comment'); Laravel(blade)の場合: {{ }}で囲む これによって、自動的にhtmlspecialchars関数を通してくれます <?php {{ $comment['comment'] }} XSS対策まとめ PHP CakePHP Laravel htmlspecialchars() h() {{ }} 参考文献 https://www.php.net/manual/ja/function.htmlspecialchars.php https://book.cakephp.org/3/ja/core-libraries/global-constants-and-functions.html https://readouble.com/laravel/7.x/ja/blade.html 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 徳丸 浩 (著)
- 投稿日:2021-06-20T22:04:30+09:00
Laravel8でHTTPテスト
はじめに 初めまして。ヤマウチです。 今回は私が業務で実装しているLaravelのHTTPテストに関してPHPUnitのセットアップからテスト実行までの手順をチュートリアル風に書いてみました。 技術面がまだまだ乏しい弱々エンジニアですが、一人でも多くの方に参考になると嬉しいです。 誤った記載がありましたらコメントにてご指摘くださいm(__)m やること 今回はHTTPテストを実施することを目的としますので、すでに実装済みのコードがあることを前提に テストの準備 データの作成 使用するトレイトやメソッド を中心に紹介していきます。 マイグレーションや機能の実装などの紹介は行いません。 また、今回はコントローラの機能テスト(HTTPテスト)を行うていで進めていきます。 テストの準備 Laravelでは、ユニットテストも考慮して作られており、最初からPHPUnitが標準で備わっています。 以下のコマンドを打つことでテスト用のファイルがtests/Featureディレクトリへ配置されます。 $ php artisan make:test UserTest Test created successfully. また、tests/Unitディレクトリ内にテストを作成したい場合は、make:testコマンドを実行するときに--unitオプションを使用することで作成することが可能です。 .env.testingの設定に関して テストを行う際、開発用のDBを汚さずにテストを行いたいこともあるかと思います。そういった場合に.env.testingを別で作成しておくことが必要があります。 バージョンは異なりますが、こちらの記事が参考になったので紹介します。【Laravel】.env.testingの使用方法と注意点 テストを実行してみる 先程のコマンドでテストファイルを作成すると、tests/Featureディレクトリに以下の内容のテストが作成されます。 <?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; class UserTes extends TestCase { /** * A basic feature test example. * * @return void */ public function testExample() { $response = $this->get('/'); $response->assertStatus(200); } } 内容としては、「get('/')にアクセスすると、レスポンスステータスコード200が返されること。」が書かれています。 早速こちらを試してみます。 基本的には$ ./vendor/bin/phpunitコマンドで全てのテストを実行することができますが、ディレクトリやファイルを指定してテストを実行することも可能です。 $ ./vendor/bin/phpunit ./tests/Feature/ PHPUnit 8.5.8 by Sebastian Bergmann and contributors. ..... 5 / 5 (100%) Time: 1.87 seconds, Memory: 26.00 MB OK (5 tests, 8 assertions) 上記のように出力されれば無事テストが通ったことになり、イメージ通りのプログラムが書けていることの証明になります。(ちゃんとしたテストが書けていれば) しかし、上記のような出力だと、どのテストを実施したかいまいち分かり難いかと思います。 そこで、PHPUnitにはテストをより分かりやすくするためのオプションも備えていますので、ここでいくつか紹介します。 --list-testsオプション どのファイルのどのテストが実行されたかを表示してくれます。 $ ./vendor/bin/phpunit --list-tests ./tests/Feature PHPUnit 8.5.8 by Sebastian Bergmann and contributors. Available test(s): - Tests\Feature\Controllers\AuthTest::test_ユーザーが作成されること - Tests\Feature\Controllers\AuthTest::test_ユーザーが登録されること - Tests\Feature\Controllers\PostTest::test_投稿のテスト - Tests\Feature\ExampleTest::testBasicTest - Tests\Feature\UserTest::testExample --testdoxオプション テストを実行しながら、どのテストが成功し、あるいは失敗したかをリアルタイムで確認することができます。 ついつい--testboxにしがちですが、--testdoxなのでお間違いなく。 $ ./vendor/bin/phpunit --testdox ./tests/Feature PHPUnit 8.5.8 by Sebastian Bergmann and contributors. Auth (Tests\Feature\Controllers\Auth) ✔ ユーザーが作成されること ✔ ユーザーが登録されること Post (Tests\Feature\Controllers\Post) ✔ 投稿のテスト Example (Tests\Feature\Example) ✔ Basic test User (Tests\Feature\User) ✔ Example Time: 1.8 seconds, Memory: 26.00 MB OK (5 tests, 8 assertions) --filterオプション 実行したいテストを、パターンで指定することが可能。 以下の例では、Exampleから始まるファイルのみのテストを実行しています。 ./vendor/bin/phpunit --filter Example --testdox ./tests/Feature PHPUnit 8.5.8 by Sebastian Bergmann and contributors. Example (Tests\Feature\Example) ✔ Basic test User (Tests\Feature\User) ✔ Example Time: 1.46 seconds, Memory: 24.00 MB OK (2 tests, 2 assertions) また、--filterオプションでは、テストファイルだけではなく、特定のテストを指定することも可能です。 その他オプション PHPUnitのコマンドラインオプションに関してはphpunit --helpコマンドにて確認することができます。 コマンドラインオプション コントローラのテスト それでは本題のコントローラのテストを行います。 ですがその前に準備として、テストデータを作成する必要があるので、作成方法を記載します。 モデルファクトリの定義 テストを行う際、データベースにあらかじめいくつかのレコードを準備しておく必要があります。このテストデータを作成するときに各カラムの値を自分でいちいち指定する代わりに、Laravelではモデルファクトリを使用し、各Eloquentモデルのデフォルト属性を定義できます。 モデルファクトリの定義 モデルファクトリの作成 モデルファクトリを作成するにはまず、以下のコマンドを実行します。 php artisan make:factory UserFactory --model=User --modelオプションは、ファクトリにより作成するモデルの名前を指定するために使用します。 上記コマンドを実行すると、database/factoriesディレクトリに以下のファイルが作成されます。 ※return以下は、必要なレコードを作成するために各々で定義します。 UserFactory.php database/factories/UserFactory.php <?php namespace Database\Factories; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; use Illuminate\Support\Facades\Hash; class UserFactory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = User::class; // モデルとの紐付け /** * Define the model's default state. * * @return array */ public function definition() { return [ // 以下は適宜定義 'name' => $this->faker->name, 'email' => $this->faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => Hash::make(12345678), 'remember_token' => Str::random(10), ]; } } User.php app/Models/User.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; class User extends Authenticatable { use HasFactory; // 必要 } 以上でデータ作成のための前準備は完了です。 あとは必要な時にモデルからfactory()メソッドを呼び出すことでデータを作成することができます。 ※Laravel8から、モデルファクトリを使用したデータの作成方法がこれまでと異なります。 以下の記事が非常に参考になります。 Laravel8で完成されたModelFactoryの使い方 テスト作成(GETリクエスト) ここからは実際にコントローラのテストを書いていきます。 すでにログイン済みの画面から、別の画面へ遷移できることをテストで確認していきたいと思います。 tests/Feature/Controllers/ControllerTest.php <?php namespace Tests\Feature\Controllers; use Tests\TestCase; use App\Models\User; use Database\Seeders\GenreSeeder; class ControllerTest extends TestCase { use WithoutMiddleware; public function test_画面遷移のテスト() { // 認証済みユーザーの作成 $loginUser = User::factory()->create(); // Userモデルからfactory()を呼び出すことでユーザーを作成 $request = $this->actingAs($loginUser) ->get('main/display'); $request->assertOk(); } } // ------------------------------------------------------ Controller (Tests\Feature\Controllers\Controller) ✔ 画面遷移のテスト Time: 1.9 seconds, Memory: 28.00 MB OK (1 tests, 1 assertions) 上記テストのポイントとして気をつけなければいけないことは、「すでにログイン済みの画面から、別の画面へ遷移」することです。 つまり、認証済みのユーザーからリクエストを投げなければなりません。 そこで必要になるのが、actingAs()メソッドです。公式ドキュメントに詳しい説明があるのでぜひ読んでみてください。 もし、actingAs()を忘れていた場合、ステータスコード302が返されるので、そこも同時に覚えておくと良いでしょう。 HTTP 302 Post (Tests\Feature\Controllers\ControllerTest) ✘ 画面遷移のテスト ┐ ├ Response status code [302] does not match expected 200 status code. ├ Failed asserting that false is true. テスト作成(POSTリクエスト) 次は、POSTリクエストのテストを書いていきます。 よくある投稿機能のテストを想定とし、 タイトル ジャンル 本文 をリクエストパラメータとします。 tests/Feature/Controllers/PostTest.php <?php namespace Tests\Feature\Controllers; use Illuminate\Foundation\Testing\WithoutMiddleware; use Tests\TestCase; use App\Models\User; use App\Models\Post; use Database\Seeders\GenreSeeder; class PostTest extends TestCase { use WithoutMiddleware; // ミドルウェアを無効化 public function test_投稿のテスト() { $this->withoutMiddleware([VerifyCsrfToken::class]); // 特定のミドルウェアを無効化 $loginUser = User::factory()->create(); $request = $this->actingAs($loginUser) ->post('/post/insert', [ 'title' => 'テスト', 'comment' => 'これはテストです。', 'genres' => [3, 4, 5] ]); $newPost = Post::where('title', 'テスト')->first(); self::assertSame('これはテストです。', $newPost->comment); $request->assertRedirect('main/display'); } } // -------------------------------------------------------- Post (Tests\Feature\Controllers\Post) ✔ 投稿のテスト Time: 1.9 seconds, Memory: 28.00 MB OK (1 tests, 2 assertions) ここでもいくつかポイントを紹介します。 $this->withoutMiddleware([VerifyCsrfToken::class]); ここではテストの際に無効化したいミドルウェアを記載しています。 今回の例で説明しますと、POSTリクエストの際に必要となるCSRF(クロス・サイト・リクエスト・フォージェリ)を無効化しています。 use WithoutMiddleware;ここの記載のみでもミドルウェアを無効化することができますが、$this->withoutMiddleware();によって、特定のミドルウェアに絞らないと全てのミドルウェアを無効化してしまうことになるので、必要な時に必要なミドルウェアのみ無効化しましょう。 Laravel 8.x ミドルウェア 参考にした記事 また、上記の記載がなければステータスコード419(Laravelの非公式HTTPコード)が返されますので、同時に覚えておくと良いでしょう。 ステータスコード419 ✘ 投稿のテスト ┐ ├ Response status code [419] is not a redirect status code. ├ Failed asserting that false is true. まとめ 今回はPHPUnitセットアップ → データ作成 → テスト実施と、かなり大雑把な説明となってしまいました。 また、本記事では、2つのHTTPテストしか実施していませんが、実際にテストを書く際にはもっと様々なテストパターンを作成しなければなりませんし、PHPUnitには他にもたくさんの便利な機能が備わっています。 次回以降は、さらに焦点を絞った内容でテストに関する記事を書いていけたらいいなと考えています。
- 投稿日:2021-06-20T19:18:23+09:00
Laravel 製アプリケーションに対する自動テストでなにをどうテストすればいいか
この記事について 自分の所属するチームに、自動テストを書いたことがないメンバーが加わったとき、ガイドラインとなるようなドキュメントがほしいなと思っていたので、書きました。どうせなら他のチームでも使いたいので、ドメインはぼかして汎用的になるようにして公開します。独自のベストプラクティス的なものですが、これまでのチームではわりとどこもこれに近くうまくいっていたので、汎用的に使えるんじゃないかと想像します。 はじめに 環境 Laravel: 5.8 以上 PHP: 7.0 以上 PHPUnit: 7.0 以上 いちおうバージョンは書きましたが、あまり関係はないです。 基本方針 基本方針は、自分の経験上これがいちばんコストパフォーマンスがいいと思っているガイドラインですが、自分はベンチャーやスタートアップ企業で、スピード重視の文化に身を置くことが多いため、テストは最低限にして、素早くリリースするのに適した形になっています。プロダクトの性質や扱うドメインによってはもう少し厳格にやったほうがいい場合もあると思うので、そこは適宜補ってください。 フィーチャーテストの方針 ウェブ API ごとのフィーチャーテストは必ず書く(最低限正常系) ユニットテストの方針 以下のいずれかに該当する場合はユニットテストを書く(ただし、フィーチャーテストでカバーできれば省略も可) 金額計算などプログラムミス発生時のリスクが高いモジュール 入出力パラメータのパターンが多いモジュール 頻繁に仕様が変わるモジュール フィーチャーテストのガイドライン 「ウェブ API ごとのフィーチャーテストは必ず書く(最低限正常系)」 フィーチャーテストで検証する対象は以下の3つです: ステータスコード レスポンスデータ API 実行後に変化する状態 命名に関するガイドライン 原則的に、テストクラスの名前空間はテスト対象のクラスと対応するようにする 1つのクラスに対して複数のテストクラスが対応する場合は、テストクラス名を名前空間として、その下にテストクラスを配置する 例)Tests\Feature\Http\Controllers\SomeController\IndexTest 原則的に、テストメソッド名はテスト対象のメソッド名と対応するようにする 1つのメソッドに対して複数のテストメソッドが対応する場合は、サフィックスでわけるようにする 例)testInvoke_Successfull, testInvoke_FailedNotAuthorized 構造に関するガイドライン すべてのテストクラスは Tests\TestCase を継承してください テストメソッドの中身は以下の構成にしてください(小項目の順番は多少前後してもいいです) 準備 フェイク準備 データ準備 モック準備 実行 リクエストパラメータ構築 API コール 検証 ステータスコード検証 レスポンスデータ検証 状態変化検証 状態変化は、データベースや通知、ファイルなど、場合によって様々ですが、基本的には PHPUnit のアサーション、あるいは Laravel が拡張したアサーションで対応できるはずです。 モックに関するガイドライン 必ずモックしなければならないのは以下のパターンです: 外部 API 呼び出し ファイルI/O 通知系 Facade::fake() で対応できる部分は積極的に使っていきましょう。 フィーチャーテストのサンプルコード <?php // 原則的に名前空間の Http 以下は、プロダクションコードと同じ階層にする(対になるように) namespace Tests\Feature\Http\Controllers; // 独自で拡張してない場合を除いて Laravel が提供している基底クラスを継承する use Tests\TestCase; // 原則的にクラス名は、SUT(テスト対象)のクラス名 + Test となるようにする(対になるように) class HelloControllerTest extends TestCase { // テストメソッド名は test で始め、テスト対象のメソッド名と対になるようにする(この場合は __invoke がテスト対象) public function testInvoke(): void { // 1. 準備 // 1.1. フェイク準備 Notification::fake(); // 1.2. データ準備 $user = User::factory()->create(); $message = 'hello'; // 1.3. モック準備 $this->mock(ExternalApi::class, function ($mock) use ($message) { $mock->shouldReceive('send') ->once() ->with($message) ->andReturn(['message' => $message]); }) // 2. 実行 // 2.1. リクエストデータ構築 $payload = ['message' => $message]; // 2.2. API コール実行 $response = $this->actingAs($user) ->postJson('/hello', $payload) // 3. 検証 // 3.1. ステータスコード検証 $resuponse->assertOk(); // 3.2. レスポンス検証 $response->assertJsonExact(['message' => $payload['message']]); // 3.3. 状態変化検証 $this->assertDatabaseHas('messages', [ 'user_id' => $user->id, 'message' => $payload['message'], ]); Notification::assertSentTo( $user, HelloNotification::class, function ($notification, $channels, $notifiable) use ($payload) { return $notifiable->message->body === $payload['message']; } ); } } ユニットテストのガイドライン 「以下のいずれかに該当する場合はユニットテストを書く(ただし、フィーチャーテストでカバーできれば省略も可)」 金額計算などプログラムミス発生時のリスクが高いモジュール 入出力パラメータのパターンが多いモジュール 頻繁に仕様が変わるモジュール ユニットテストで検証する対象は以下の2つです: 関数の返却値(または例外) 実行後に変化する状態 命名に関するガイドライン フィーチャーテストと同じ 構造に関するガイドライン 以下の点以外はフィーチャーテストと同じ すべてのテストクラスは PHPUnit\Framework\TestCase を継承してください ユニットテストの基底クラスは原則的に PHPUnit が提供している TestCase クラスを使いますが、テスト対象がデータベースアクセスやファサードなど、Laravel に依存する部分と密結合している場合は、Laravel が提供している Tests\TestCase を継承してもかまいません。 モックに関するガイドライン フィーチャーテストと同じ 基本的に、ユニットテストのテスト対象となるようなクラスはプレーンなクラスになるよう、データベースへのアクセスが発生しないようなつくりにしてください。それでもやむを得ずデータベースへのアクセスが発生するようなケースでも、データベースアクセスを担当するクラスのモックはつくらない方針ですが、パターンが多くテストケースの実行に時間がかかる場合は、リファクタリングを行い、データベースアクセスを行うクラスをモックできるような形に変え、依存を排除してください。 例)ユーザーが持つクーポンから値引きして料金計算を行う テスト対象にデータベース依存がある構造 PriceCalculator.php public function calculate(int $price, User $user): int { $coupon = UserCoupon::where('user_id', $user->id)->first(); if ($coupon) { $price = max($price - $coupon->value, 0); } // 他の計算 return $price; } テスト対象にデータベース依存がない構造 PriceCalculator.php public function calculate(int $price, int $coupon): int { $price = max($price - $coupon->value, 0); // 他の計算 return $price; } // 呼び出し時 $coupon = UserCoupon::where('user_id', $user->id)->first(); $calculator = new PriceCalculator(); $price = $calculator->calculate($amount, $coupon ? $coupon->value : 0); Eloquent な Model クラスはデータベースアクセスを発生させないようにできるので、上記の形で排除できない場合は、次の形式を検討してください。 PriceCalculator.php public function calculate(int $price, User $user): int { $coupon = $user->coupon; if ($coupon) { $price = max($price - $coupon->value, 0); } // 他の計算 return $price; } // 呼び出し時 $user->coupon = new UserCoupon(['value' => 500]); $calculator = new PriceCalculator(); $price = $this->calculatePrice($amount, $user); ユニットテストのサンプルコード <?php // 原則的に名前空間の Unit 以下は、プロダクションコードと同じ階層にする(対になるように) namespace Tests\Unit\Models; // 独自で拡張してない場合を除いて PHPUnit が提供している基底クラスを継承する use PHPUnit\Framework\TestCase; // 原則的にクラス名は、SUT(テスト対象)のクラス名 + Test となるようにする(対になるように) class PriceCalculatorTest extends TestCase { /** * @param int $amount * @param ?UserCoupon $coupon * @param int $expected * @dataProvider dataCalculate */ // テストメソッド名は test で始め、テスト対象のメソッド名と対になるようにする(この場合は __invoke がテスト対象) public function testCalculate(int $amount, ?UserCoupon $coupon, int $expected): void { $user = User::factory()->make(); // 保存はしない $user->coupon = $coupon; $calculator = new PriceCalculator(); $this->assertSame($expected, $calculator->calculate($amount, $user)); } // データプロバイダメソッド名は data で始め、テストメソッド名と対になるようにする public function dataCalculate(): array { // 各テストパターンの配列は連想配列にし、テストメソッドの引数と対応するようにする return [ 'クーポンあり' => [ 'amount' => 5000, 'coupon' => new UserCoupon(['value' => 500]), // 保存はしない /* * 8.x 未満のモデルファクトリを使う場合はクロージャにする * 'coupon' => fn() => factory(UserCoupon::class)->make(['value' => 500]), */ 'expected' => 4500, ], 'クーポンなし' => [ 'amount' => 5000, 'coupon' => null 'expected' => 5000, ], ]; } } その他のガイドライン パターンが複数ある場合はデータプロバイダを使ってください 比較には assertSame を使ってください パターンが複数ある場合はデータプロバイダを使ってください 例)フィーチャーテスト /** * @param Closure $prepare * @param Closure $assert * @dataProvider dataInvoke */ public function testInvoke(Closure $prepare, Closure $assert): void { // 1. 準備 $prepare(); // 2. 実行 // 省略 // 3. 検証 $assert($this, $response); } // メソッド名はテストメソッドと対になるように(testInvoke なら dataInvoke) public function dataInvoke(): array { return [ 'パターンがわかるようなキーにしてください' => [ 'prepare' => function () { // データの構築やらなにやら }, 'assert' => function (TestCase $testCase, TestResponse $response) { // レスポンスの検証やらなにやら }, ], ]; } データプロバイダが返す配列は、連想配列のほうがいいと思います(キーが変数名になります)。データプロバイダ内ではファクトリもファサードも使えないので、なるべくプリミティブな値を返すようなつくりにしておいたほうがいいんですが、フィーチャーテストの場合は、モデルファクトリーでデータをつくったり、Facade::fake() を呼んだりするので上記例のように、クロージャを渡すのがいいでしょう。 例)ユニットテスト public function testCalculate(int $amount, int $coupon, int $expected): void { $calculator = new PriceCalculator(); $actual = $calculator->calculate($amount, $coupon); $this->assertSame($expected, $actual); } public function dataCalculate(): array { return [ 'クーポンあり' => [ 'amount' => 4000, 'coupon' => 500, 'expected' => 3850, ], ]; } ユニットテストの場合も、フィーチャーテストでの使い方と同じです。 比較には assertSame を使ってください assertEquals は緩い比較なので、原則 assertSame を使ってください。 $this->assertEquals(10, 10.0); // passes $this->assertSame(10, 10.0); // fails おわりに ざっと思いついたガイドラインを列挙してみました。またなにか(思いついた|質問された)ら追加します。
- 投稿日:2021-06-20T15:54:55+09:00
[Laravel] XSS,CSRF,SQLインジェクションまとめ
はじめに 初学者が学ぶべきセキュリティ攻撃である、XSS,CSRF,SQLインジェクションについて、Laravelではどのように対策がされているのか、今一度おさらいしてみた。 検証環境 macOS Catalina ver 10.15.7 Docker ver 20.10.5 docker-compose ver 1.29.0 Laravel ver 7.30.4 MySQL ver 8.20.3 XSS(クロスサイトスクリプティング) XSSとは何か 3分でわかるXSSとCSRFの違いに書いてあるように「ユーザーがWebページにアクセスすることで不正なスクリプトが実行されてしまう脆弱性または攻撃手法」です。こちらの記事は大変わかりやすいので、ぜひ一読してください。 XSS対策のポイントは「特殊文字(< や > や " など)をエスケープする」ことです。そうすることでスクリプトではなく、単なる文字列として扱うことになるため、意図しない処理を防ぐことができます。 エスケープするためにはhtmlspecialchars関数を用います。 index.php $script = "<script>window.alert('hello world')</script>"; $script = htmlspecialchars($script,ENT_QUOTES,"UTF-8"); echo $script; // エスケープされるためalertは実行されない Laravelではどのように対策しているのか PostController.php public function create() { $script = "<script>window.alert('hello world')</script>"; return view('posts.create',compact( 'script' )); } posts/create.blade.php {{ $script }} // エスケープされるためalertは実行されない {{!! $script !!}} // ちなみに両端「!!」の場合は、alertが実行されますので注意です ドキュメントにも書いてある通り、{{ 変数 }}とすることでhtmlspecialchars関数が適用されます。 CSRF(Cross Site Request Forgeries) 脆弱なサイトと罠サイトを実際に作って学ぶ『CSRF』とその対策に書いてあるように「『罠サイト』から『標的サイト』へ HTTP リクエストを送信することで『標的サイト』を操作してしまおうという攻撃手法」です。 どのような被害が生まれるのかについて、上記記事でご確認ください。 CSRF対策のポイントは「リクエストが送信先が想定しているリクエストかどうか」です。 そのためリクエストを送信する側とそのリクエストを受信する側で合鍵のようなものを持っておけば、正しいリクエストかどうかわかるわけです。 Laravelではどのように対策しているのか input.blade.php <form action="" method="POST"> @csrf </form> この@csrfが <input type="hidden" name="_token" value="ランダムな文字列"> に置換され、このvalueを用いて正しいリクエストであるか否かを確認しているというわけですね。 まさに合鍵の役割を果たしています。 SQLインジェクション 【 Laravel 】クエリビルダ と Eloquent ORM の違いを整理!に書いてあるように、SQLインジェクションとは悪意のあるユーザーが不正なクエリ( データベースへの命令文 )を書き、データベースへアクセスしてデータの漏洩・改ざんを行う攻撃のことです。 SQLインジェクションのポイントは XSSと同様にエスケープ処理をする ユーザによる入力値を必要とするクエリはプリペアードステートメント(静的プレースホルダ)を用いる PDO::ATTR_EMULATE_PREPARES => false(Laravelではデフォルト) エミュレーションに関するまとめはPHPでデータベースに接続するときのまとめをご覧ください。 です。 プリペアードステートメント?ってなられた方は安全なSQLの呼び出し方をご覧ください。 Laravelではどのように対策しているのか 先ほど紹介させていただいた【 Laravel 】クエリビルダ と Eloquent ORM の違いを整理!に書いてあるように、SQLインジェクション対策は基本的になされているが、DB::rawなどの素のSQL文を使用するときは注意が必要です。 この点について、ドキュメントにもきちんと書かれています。 例えば以下のコードを見てみましょう。 PostController.php public function store(Request $request) { // 悪意のあるユーザによって送られてきたid $id = '5; delete from posts'; $posts = DB::table('posts')->whereRaw("id = ${id}")->get(); } こんなコードを書くことはまずありませんですが、この場合はpostsテーブルのデータが全て削除されてしまいます。 続けて、 PostController.php public function search(Request $request) { // ユーザによって検索がかけられた値 $word = "果汁100%"; // 正しくエスケープされないためエラー $results = DB::table('posts')->whereRaw("content like %${word}%")->get(); // 正しくエスケープされる $results = Post::where("content","like","%${word}%")->get(); return view('posts.search',compact( 'results' )); } このようにエスケープ処理を行っていないためSQLインジェクションの脆弱性を生む原因となってしまいます。 よってDB::rawなどの素のSQL文を使用するときは、エスケープ処理をして、しっかり対策を取らなければならないことがわかります。 rawメソッドをどうしても使わなければならない時は注意して使いましょう。 さいごに いかがだったでしょうか。 初学者が押さえておくべきセキュリティについて、Laravelではどのように対策を行っているのかを見てきましたが、普段それらを特に意識することなく、開発することができる点はやはりLaravelのすばらしい点だと思います。 一方で、「Laravelを使っていても、セキュリティ対策は不完全である」こともお分かりいただけたと思います。 Laravelでできることはフル活用して、できないことは補うようにしてセキュリティ対策をしていきたいですね。 参考サイト 3分でわかるXSSとCSRFの違い https://qiita.com/wanko5296/items/142b5b82485b0196a2da 脆弱なサイトと罠サイトを実際に作って学ぶ『CSRF』とその対策 https://www.hypertextcandy.com/csrf-hands-on-tutorial 【 Laravel 】クエリビルダ と Eloquent ORM の違いを整理! https://qiita.com/Tomo_rrow86/items/a0426ce17b978e3b05d5 PHPでデータベースに接続するときのまとめ https://qiita.com/mpyw/items/b00b72c5c95aac573b71#%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E3%81%BE%E3%81%A8%E3%82%81 安全なSQLの呼び出し方 https://qiita.com/Morinikiz/items/dfdb33f25df4df0f672c 駆け出しエンジニアの皆さんに知ってほしい脆弱性のこと。 https://zenn.dev/ad5/articles/5e4e67c9663e4e0d0cb0 【Laravel】敢えて試すSQLインジェクション【仕組みを知るには実装】 https://kimamacode.com/laravel-sql-injection/ PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能 https://blog.tokumaru.org/2013/12/pdo-and-mysql-allow-multiple-statements.html
- 投稿日:2021-06-20T15:27:51+09:00
初学者がlaravel 8.xのBreezeでログイン機能を実装してみた
初学者が簡単なCRUDアプリにログイン機能を実装したいなと思ったとき、 公式ドキュメントより「まず、Breezeより始めよ」とのお告げがあったので Breezeでログイン機能を実装してみました。 環境 OS: Windows 10 home CPU: AMD Ryzen 2700X GPU: NVIDIA GTX 1060 RAM: 16GB 2666Mhz PHP: ver 8.0.3 Laravel: ver 8.45.1 MySQL(MariaDB): ver 15.1 1.laravelの認証機能 laravel 8.xでは認証機能構築のスターターキットとして、 jetstreamとbreeze、が提供されています。 jetstreamはログイン、ユーザー登録、メール検証、2要素認証、セッション管理等を含む 多機能な認証パッケージであるのに対して、breezeはログイン、ユーザー登録、パスワードのリセット、 メールの検証、パスワードの確認等の最小限の認証パッケージとなっています。 そのため、公式ドキュメントは「まず、Breezeより始めよ」とbreezeから始めることを推奨しています。 また、jetstreamとbreezeはCSSフレームワークがbootstrapからTailwind CSSに変わっていることに留意。 なんでTailwind CSS推しに...bootstrapでいいじゃないですか。 2.laravel Breezeのインストール 事前にlaravelプロジェクトを作成しておきます。 筆者はlaravel/installerで作成しているので以下のコマンドになります。 laravel new example-app ターミナルで作成したlaravelプロジェクトのディレクトリまで移動し、 ComposerでLaravel Breezeパッケージをインストールします。 composer require laravel/breeze --dev Laravel Breezeパッケージのインストールが完了したら Artisanコマンドでプロジェクトにインストールします。 php artisan breeze:install 最期にNode.jsモジュールのインストールとアセットのコンパイルを行いますが、 そのためにはNode.jsとnpm(Node Package Manager)が必要となります。 npm install npm run dev これでLaravel Breezeの実装は完了しました。 php artisan serveコマンドで簡易サーバーを立ち上げて確認すると、 画面右上赤丸内にloginとregisterが表示されます。
- 投稿日:2021-06-20T14:44:57+09:00
Symfony4 ユーザー認証機能作成(後編)
こちらは前編の続きになります。 先に前編のほうを完了させてから進めてください。 Symfony4 ユーザー認証機能作成(前編) ログインページを作成する ログイン処理のSecurityControllerを作成する $ php bin/console make:controller src/Controller/SecurityController.php <?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class SecurityController extends AbstractController { /** * @Route("/login", name="login") */ public function login(AuthenticationUtils $authenticationUtils): Response { $error = $authenticationUtils ->getLastAuthenticationError(); if ($this->getUser() == null) { $user = 'not logined...'; } else { $user = 'logined: ' . $this->getUser()->getUsername(); } return $this->render('security/login.html.twig', [ 'error' => $error, 'user' => $user, ]); } } ログイン用のテンプレートを作成する。 templates/security/login.html.twig {% extends 'base.html.twig' %} {% block title %}Hello SecurityController!{% endblock %} {% block body %} <style> .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } </style> <div class="example-wrapper"> <p>login</p> <p>{{user}}</p> {% if error %} <p>{{ error.messageKey }}</p> {% endif %} <form action="{{ path('login') }}" method="post"> <table> <tr> <th><label for="username">Username:</th> <td><input type="text" id="username" name="_username" /></td> </tr> <tr> <th><label for="password">Password:</th> <td><input type="password" id="password" name="_password" /></td> </tr> <tr><th></th><td><button type="submit">login</button></td></tr> </table> </form> </div> {% endblock %} 管理者ページを作成する AdminControllerを作成する。 $ php bin/console make:controller src/Controller/AdminController.php <?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class AdminController extends AbstractController { /** * @Route("/admin", name="admin") */ public function admin(): Response { return $this->render('admin/admin.html.twig', [ 'title' => 'Admin', 'message' => 'message', 'user' => $this->getUSer(), ]); } } templates/admin/admin.html.twig {% extends 'base.html.twig' %} {% block title %}Hello AdminController!{% endblock %} {% block body %} <style> .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } </style> <div class="example-wrapper"> <h2>{{ message }}</h2> <p>user: {{ user.username }}</p> <p>mail: {{ user.email }}</p> </div> {% endblock %} 一般ユーザー用のページを作成する HelloControllerを作成する。 $ php bin/console make:controller src/Controller/HelloController.php <?php namespace App\Controller; use App\Entity\Person; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; class HelloController extends AbstractController { /** * @Route("/hello", name="hello") */ public function index(Request $request): Response { if (!$this->getUser()->getIsActivated()) { throw new AccessDeniedException('Unable to access!'); } return $this->render('hello/index.html.twig', [ 'title' => 'Admin', 'message' => 'message', 'user' => $this->getUser(), ]); } } templates/hello/index.html.twig {% extends 'base.html.twig' %} {% block title %}Hello HelloController!{% endblock %} {% block body %} <style> .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } </style> <div class="example-wrapper"> <h2>{{ message }}</h2> <table> <tr><th>id</th><td>{{ user.id }}</td></tr> <tr><th>name</th><td>{{ user.username }}</td></tr> <tr><th>mail</th><td>{{ user.email }}</td></tr> </table> </div> {% endblock %}
- 投稿日:2021-06-20T14:43:56+09:00
Symfony4 ユーザー認証機能作成(前編)
今回作成する予定のテーブル User(ユーザー)テーブル 項目名 日本語名 型 必須 id ID(連番) integer 〇 username ユーザー名 string 〇 password パスワード string 〇 email メールアドレス string 〇 isActiveted 活動フラグ boolean 〇 エンティティの作成 $ php bin/console make:entity Formの作成 $ php bin/console make:form User マイグレーションの作成 $ php bin/console make:migration マイグレーション実行 $ php bin/console doctrine:migrations:migrate User.phpに以下の記述を追記。 src/Entity/User.php <?php namespace App\Entity; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** * @ORM\Entity(repositoryClass=UserRepository::class) * @UniqueEntity("username") * @UniqueEntity("email") */ class User implements UserInterface, \Serializable { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=255 unique=true) */ private $username; /** * @ORM\Column(type="string", length=255) */ private $password; /** * @ORM\Column(type="string", length=255 unique=true) */ private $email; /** * @ORM\Column(type="boolean") */ private $isActivated; /* 省略 */ public function __construct() { $this->isActivated = true; } public function getSalt() { return null; } public function getRoles() { if ($this->username == 'admin') { return array('ROLE_ADMIN'); } else { return array('ROLE_USER'); } } public function eraseCredentials() { } public function serialize() { return serialize(array( $this->id, $this->username, $this->password, $this->isActivated, )); } public function unserialize($serialized) { list( $this->id, $this->username, $this->password, $this->isActivated, ) = unserialize($serialized, array('allowed_classes' => false)); } UserTypeクラスを編集 src/Form/UserType.php <?php namespace App\Form; use App\Entity\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('username', TextType::class) ->add('password', PasswordType::class) ->add('email', EmailType::class) ->add('register', SubmitType::class) ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => User::class, ]); } } RegisterControllerの作成 $ php bin/console make:controller src/Conteoller/RegisterController.php <?php namespace App\Controller; use App\Form\UserType; use App\Entity\User; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; class RegisterController extends AbstractController { /** * @Route("/register", name="register") */ public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response { $user = new User; $form = $this->createForm(UserType::class, $user); $form->handleRequest($request); if ($request->getMethod() == 'POST') { if ($form->isValid()) { $password = $passwordEncoder->encodePassword($user, $user->getPassword()); $user->setPassword($password); $manager = $this->getDoctrine()->getManager(); $manager->persist($user); $manager->flush(); return $this->redirectToRoute('ルート先'); } } else { return $this->render('register/register.html.twig', [ 'form' => $form->createView(), ]); } } } security.yamlを修正する config/package/security.yaml security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: our_db_provider: entity: class: App\Entity\User property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: anonymous: ~ pattern: ^/ provider: our_db_provider form_login: login_path: login check_path: login logout: path: logout target: login invalidate_session: false # activate different ways to authenticate # https://symfony.com/doc/current/security.html#firewalls-authentication # https://symfony.com/doc/current/security/impersonating_user.html # switch_user: true # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } encoders: App\Entity\User: algorithm: bcrypt 登録できることを確認する。 http://127.0.0.1:8000/register 後編に続く Symfony4 ユーザー認証機能作成(後編)
- 投稿日:2021-06-20T13:52:10+09:00
SQLのCRUDについて再確認してみる
CRUDとは? プログラミングでアプリを作る際の4大機能の頭文字を取ったもの。 CRUDはクラッドと読めば良いらいしい! C・・・create(新規作成) R・・・read(読み込む) U・・・update(更新する) D・・・delete(削除する) の頭文字を取ったものになる。 SQL文でのCRUDの使い方 ① CREATE SQL文では新規作成のCREATEはINSERTという言葉で書く。(insertの直訳は差し込む) INSERT INTO テーブル名(カラム名) VALUES(値); //実際の使用例 INSERT INTO contacts(id,name,created_at)VALUES(1,"三宅",now()); 使用例を実行した結果、contactテーブルには以下のデータが新規作成(追加)される。 id name created_at 1 三宅 2021/06/20 0:00 ② READ SQL文では読み込み(データ取得)のREADはSELECTという言葉で書く。(SELECTの直訳は選択する) SELECT カラム名 FROM テーブル名 WHERE カラム名=値; //実際の使用例 SELECT * FROM contacts WHERE id=2; 使用例を実行した結果、contacts(任意の名前のテーブル名)テーブルのid=2のデータが取得できる。 ※WHEREを記述しない場合は、contactテーブル全てのデータを取得できる id name created_at 2 山田 2021/06/20 01:00 ③ UPDATE SQL文では更新のUPDATEはUPDATEという言葉で書く。(UPDATEの直訳は更新する) UPDATE テーブル名 SET カラム名="更新する値"; //実際の使用例 SELECT contacts SET age="20代"; 使用例を実行した結果、contactsのテーブルのageカラムの値は全て20代になる。 id name age created_at 1 三宅 20代 2021/06/20 0:00 2 山田 20代 2021/06/20 01:00 3 佐藤 20代 2021/06/20 12:00 ・・・特定の箇所だけ更新したい場合はWHEREを使う UPDATE テーブル名 SET カラム名="更新する値" WHERE カラム名="更新したいデータ"; //実際の使用例 SELECT contacts SET age="30代" WHERE id=3; 実行結果 id=3の箇所だけがageが30代に更新されました! id name age created_at 1 三宅 20代 2021/06/20 0:00 2 山田 20代 2021/06/20 01:00 3 佐藤 30代 2021/06/20 12:00 ④ DELETE SQL文では削除のDELETEはDELETEという言葉で書く。(deleteの直訳は削除する) DELETE FROM テーブル名 WHERE カラム名=値; //実際の使用例 DELETE FROM contacts WHERE id=2; 使用例を実行した結果、contactテーブルにはid=2のデータが削除される。 ※WHEREを記述しなければ、テーブルごと削除することができる id name created_at 1 三宅 2021/06/20 0:00 3 佐藤 2021/06/20 12:00
- 投稿日:2021-06-20T11:22:21+09:00
【PHP】.env の使用方法
はじめに プロジェクトや個人開発で .env ファイルを使用しないことはほぼないと思います。 .env ファイルとは開発や本番など環境ごとに変わるDB情報やGitで管理したくないパスワードなどプロジェクトの設定を書いたファイルです。 今回は簡単に .env を読み込み出力するというプログラムを作成しました。( Laravel (フレームワーク)を使用するときは直接 .env を読み込まず config ファイルから読み込むことにご注意ください) 目次 はじめに 目次 導入 composer .gitignore .env メインファイル 参考文献 導入 composer ライブラリを導入します。 GitHub(Installation) ターミナル composer require vlucas/phpdotenv .gitignore .gitignore を作成して Git で管理しないようにします。 対象は vendor ディレクトリと まだ作成していませんが .env です。 # すでにファイルがある場合は飛ばしてください touch .gitignore .gitignore vendor .env .env .env を作成していきます。 ターミナル # ルート直下 touch .env .env GREETING="こんにちは" メインファイル GitHub(Usage) .php <?php // 自動で読み込み require './vendor/autoload.php'; // .envを使用する // アプリケーションのロード Dotenv\Dotenv::createImmutable(__DIR__)->load(); // .envファイルで定義したGREETINGを変数に代入 $greeting = $_ENV['GREETING']; // 出力 print($greeting) . PHP_EOL; // 出力結果 // => こんにちは 参考文献 GitHub (PHP dotenv) PHPマニュアル($_ENV) PHPのオートロード(autoload) envファイルとはなんだろう。 Laravel では env() を config 系ファイル以外の場所に書いてはいけない Laravel .envとconfigの役割の違い【初心者のLaravel講座】
- 投稿日:2021-06-20T11:22:21+09:00
【PHP】.env は絶対に使おう
はじめに プロジェクトや個人開発で .env ファイルを使用しないことはほぼないと思います。 .env ファイルとは開発や本番など環境ごとに変わるDB情報やGitで管理したくないパスワードなどプロジェクトの設定を書いたファイルです。 今回は簡単に .env を読み込み出力するというプログラムを作成しました。( Laravel (フレームワーク)を使用するときは直接 .env を読み込まず config ファイルから読み込むことにご注意ください) この記事でわかること .env の使用方法 目次 はじめに この記事でわかること 目次 導入 composer .gitignore .env メインファイル 参考文献 導入 composer ライブラリを導入します。 GitHub(Installation) ターミナル composer require vlucas/phpdotenv .gitignore .gitignore を作成して Git で管理しないようにします。 対象は vendor ディレクトリと まだ作成していませんが .env です。 # すでにファイルがある場合は飛ばしてください touch .gitignore .gitignore vendor .env .env .env を作成していきます。 ターミナル # ルート直下 touch .env .env GREETING="こんにちは" メインファイル GitHub(Usage) .php <?php // 自動で読み込み require './vendor/autoload.php'; // .envを使用する Dotenv\Dotenv::createImmutable(__DIR__)->load(); // .envファイルで定義したGREETINGを変数に代入 $greeting = $_ENV['GREETING']; // 出力 print($greeting) . PHP_EOL; // 出力結果 // => こんにちは 参考文献 GitHub (PHP dotenv) PHPマニュアル($_ENV) PHPのオートロード(autoload) envファイルとはなんだろう。 Laravel では env() を config 系ファイル以外の場所に書いてはいけない Laravel .envとconfigの役割の違い【初心者のLaravel講座】
- 投稿日:2021-06-20T10:28:14+09:00
PHP クロージャー(無名関数)をアロー関数を使って定義してみる
目的 PHP クロージャー(無名関数)の定義をアロー関数を用いて行ってみる ご注意 アロー関数を用いたクロージャーの定義はPHP7.4からのみサポートされている 情報 下記のWebサイトをつかって筆者はクロージャーの振る舞いを確認した。 https://paiza.io/ja 下記のようなクロージャーを用いた簡単な処理があるとする。 <?php $str_2 = 'japan'; $add_str = function ($str_1) use ($str_2) { return $str_1 . $str_2; }; echo $add_str('hello'); 方法 「情報」に記載したクロージャーの定義部分をアロー関数を用いて書き直してみた。 <?php $str_2 = 'japan'; $add_str = fn($str_1) => $str_1 . $str_2; echo $add_str('hello'); 参考文献 https://www.php.net/manual/ja/functions.arrow.php
- 投稿日:2021-06-20T08:53:45+09:00
Heroku(PHP)無料枠で日替わり猫画像を定期的にメール送信する
完成形 こんな感じの猫画像メールを毎日特定の時間に配信する。猫画像は毎日日替わり。 システム環境 名称 説明 URL Heroku アプリケーションの実行環境(PaaS) https://jp.heroku.com/ Twilio SendGrid Herokuアドオン。メール送信サービス https://elements.heroku.com/addons/sendgrid Heroku Scheduler Herokuアドオン。定期実行などのスケジューラーサービス https://elements.heroku.com/addons/scheduler The Cat API 猫画像を取得できたりするサービス https://thecatapi.com/ 構築手順 1. Heroku環境構築 Heroku スターターガイド (PHP)を参考にPHP環境を構築する。 アプリの準備 コマンドプロンプト >mkdir nekoproject >cd nekoproject >git clone https://github.com/heroku/php-getting-started.git >cd php-getting-started アプリのデプロイ コマンドプロンプト >heroku create >git push heroku main >heroku open heorku open で以下のような画面が表示されればデプロイ成功。 2. The Cat API のAPIキーを取得 https://thecatapi.com/ にアクセスして「SIGN UP FOR FREE」をクリックしてメールアドレスを入力すると、しばらくするとAPIキーが送られてくる。 取得したAPIキーをURLに追加してアクセス。結果のJSONファイルをデコードし以下のコードで猫画像のURLを取得できる。 PHP // Get a cat image $json = file_get_contents('https://api.thecatapi.com/v1/images/search?api_key=***********'); $arr = json_decode($json,true); $url = $arr[0]['url']; 3. Twilio SendGrid(メールサービス)の設定 アドオンの追加 SendGridのアドオンをプロビジョニングする。(Herokuではアドオンのプロビジョニングにはクレジットカードの登録が必要です) コマンドプロンプト heroku addons:create sendgrid:starter SendGrid API Key の取得と設定 SendGridであらかじめアカウントを登録する。(審査に数日程度かかります) SendGridのコントロールパネルを開き、Settings→API Keysを開く。そして、「Create API Key」を押し、Full AccessのAPI Keyを作成する。 以下のコマンドで環境変数に作成したAPI Keyを設定する。 コマンドプロンプト heroku config:set SENDGRID_API_KEY=***************************** SendGridをPHPから使用するためのライブラリのインストール composer.jsonを編集し、"sendgrid/sendgrid": "~7" の行を追加する。 composer.json { "require" : { "silex/silex": "^2.0.4", "monolog/monolog": "^1.22", "twig/twig": "^2.0", "symfony/twig-bridge": "^3", "sendgrid/sendgrid": "~7" }, "require-dev": { "heroku/heroku-buildpack-php": "*" } } 以下のコマンドでcomposerをアップデートし、ライブラリをインストールする。 コマンドプロンプト composer update 4. ソースコードのデプロイ 以下のコードをcatmail.phpとしてphp-getting-started直下に配置する。(Cat APIのKeyと、メールの送信元、送信先のアドレスは適宜書き換えてください) catmail.php <?php require 'vendor/autoload.php'; // Get a cat image $json = file_get_contents('https://api.thecatapi.com/v1/images/search?api_key=******'); $arr = json_decode($json,true); $url = $arr[0]['url']; // Set the email parameters $email = new \SendGrid\Mail\Mail(); $email->setFrom("***@gmail.com"); $email->setSubject("今日の猫です"); $email->addTo("****@gmail.com"); $email->addContent("text/html", "<img width='300' src='" . $url . "'/>"); $sendgrid = new \SendGrid(getenv('SENDGRID_API_KEY')); // Send the email try { $response = $sendgrid->send($email); print $response->statusCode() . "\n"; print_r($response->headers()); print $response->body() . "\n"; echo "email sent!\n"; } catch (Exception $e) { echo 'Caught exception: '. $e->getMessage() ."\n"; } デプロイする。 コマンドプロンプト git add . git commit -m "NEKO" git push heroku main 以下を実行してメールが届けば成功。 コマンドプロンプト heroku run "php catmail.php" 5. スケジューラ登録 アドオンの追加 スケジューラのアドオンをプロビジョニングする。 コマンドプロンプト heroku addons:create scheduler:standard スケジューラ登録 Herokuのコンソール画面から、Resorce→Heroku Schedulerを開き、Create Jobボタンをクリック。 今回は、以下の画面の様に毎日13時にcatmail.phpが実行されるように設定した。(協定世界時(UTC)で時刻を設定する必要があるため、9時間マイナスした時刻を設定) 参考サイト Heroku スターターガイド (PHP) 猫の画像が次々届くサーバーレスなシステム Heroku PHPでSendGridを使用してメール送信 添付ファイルも可 GitHub - sendgrid/sendgrid-php: The Official Twilio SendGrid Led, Community Driven PHP API Library Herokuでスケジューラ(cron)を設定する方法【Heroku Scheduler】
- 投稿日:2021-06-20T06:39:19+09:00
PHP基礎構文
PHP基礎構文 文字の表示 sample.php <?php echo "Hello,world!"; ?> 実行結果 Hello,world! 変数 変数名には、アルファベット、数字、アンダースコア( _ )が使えます。 ただし、変数名の最初の文字に数字は使えません。 sample.php <?php $num; $num = 1; $num2 = 2; echo $num; echo $num2; ?> 実行結果 1 2 演算子 算術演算子 sample.php <?php $num = 7; $num2 = 2; echo $num + $num2; echo $num - $num2; echo $num * $num2; echo $num / $num2; echo $num % $num2; ?> 実行結果 9 5 14 3.5 1 関係演算子 正しくないときは1返ってきます。 正しくないときは「 空 」が返ってきます。 sample.php <?php $x = 10; $y = 2; echo ($x > $y); echo ($x < $y); echo ($x >= $y); echo ($x <= $y); echo ($x == $y); echo ($x != $y); ?> 実行結果 1 0 0 0 0 1 論理演算子(AND) sample.php <?php $x = 8; $y = 3; echo ($x >= 5 && $x <= 10); echo ($y >= 5 && $y <= 10); ?> 実行結果 1 0 論理演算子(OR) sample.php <?php $x = 8; $y = 3; echo ($x == 3 || $y == 3); echo ($x == 1 || $y == 1); ?> 実行結果 1 0 配列 sample.php <?php $a = ["sato", "suzuki", "takahashi"]; echo ($a[0]); echo ($a[1]); echo ($a[2]); ?> 実行結果 sato suzuki takahashi 多次元配列 sample.php <?php $a = [ ["sato","suzuki"],["takahashi","tanaka"] ]; echo ($a[0][0]); echo ($a[0][1]); echo ($a[1][0]); echo ($a[1][1]); ?> 実行結果 sato suzuki takahashi tanaka if文 sample.php <?php $age = 0; if ($age >= 20) { echo ("adult"); }else if ($age==0) { echo ("baby"); }else { echo ("child"); } ?> 実行結果 baby for文 sample.php <?php for($i = 0; $i <= 4; $i ++) { echo $i."\n"; } ?> 実行結果 0 1 2 3 4 関数 sample.php <?php function say_hello() { echo "Hello World"; } say_hello(); ?> 実行結果 Hello World 引数がある関数 sample.php <?php function say_hello($greeting) { echo $greeting."\n"; }; say_hello("Good Morning"); function cal($x) { echo ($x * 3); }; cal(6); ?> 実行結果 Good Morning 18 複数の引数がある関数 sample.php <?php function cal($x, $y) { echo ($x / $y); }; cal(6, 3); ?> 実行結果 2 戻り値がある関数 sample.php <?php function cal($x, $y) { return $x / $y; }; $result = cal(6, 3); echo $result; ?> 実行結果 2 クラス アクセス修飾子には、public(パブリック)、protected(プロテクテッド)、private(プライベート)があります。 publicは、どこからでもアクセス可能です。 protectedは、クラス自身、継承したクラス、親クラスからのみアクセスできます。 privateは、同じクラスの中でのみアクセス可能です。 sample.php <?php function cal($x, $y) { return $x / $y; }; $result = cal(6, 3); echo $result; ?> 実行結果 2
- 投稿日:2021-06-20T03:17:50+09:00
【Laravel】個人的によく使うEloquentのWhere句まとめ
Laravelは自由かつ何でも実現できるフレームワークなので (使いこなせているかは別として…)重宝しています。 今回は、個人的によく使うEloquentのWhere句を書き出してみました。 基本 $adminUsers = User::where('role', '=', 'admin')->get(); AND検索・OR検索 whereを付け足せばAND検索になります。 OR検索をするにはorWhereを使います。 AND検索 $users = User::where('role', '=', 'admin') ->Where('id', '>', 10); ->get(); OR検索 $adminers = User::where('role', '=', 'admin') ->orWhere('role', '=', 'maintainer'); ->get(); IN句で検索 IN句で検索します。 たとえば、Idが0か999のユーザを取得します。 IN句 $specialUsers = User::whereIn('in', [0,999])->get(); NULLのレコードを検索 NULLの値になっているレコードを取得します。 whereNullを使います。 NULL $invalidUsers = User::whereNull('nickname')->get(); NULLでないレコードを検索 NULLでないレコードを取得します。 whereNotNullを使います。便利ですね。 NOTNULL $validUsers = User::whereNotNull('nickname')->get(); ソート orderByを使います。 降順は'desc'、昇順は'asc'を指定します。 またorderByではなく、latest、oldestを使っても良いです。 ソート $users = User::orderBy('id','desc')->get(); $users = User::orderBy('id','asc')->get(); $users = User::latest('id')->get(); $users = User::oldest('id')->get(); 動的にWhereを追加する 条件によってクエリを変更したいとき。 $userQuery = User::query(); $userQuery = $userQuery->where('role', '=', 'member'); if ($condition1) { $userQuery = $userQuery->where('condition', '=', '1'); } if ($condition2) { $userQuery = $userQuery->where('condition', '=', '2'); } $users = $userQuery->get(); 他にも思い出したら追記していこうと思います。 「他にもこれよく使う!」 というWhere句がありましたら是非コメント頂ければと思います!
- 投稿日:2021-06-20T00:22:35+09:00
この記事ではCentOS 6.5にインストールしたRuby 2.2を使っています
この記事ではCentOS 6.5にインストールしたRuby 2.2を使っています
- 投稿日:2021-06-20T00:08:47+09:00
Laravelの2種類のテストの比較(FeatureテストとUnitテスト)
はじめに Laravelのtests/ディレクトリ以下にはFeature/とUnit/の二つのディレクトリが自動で作成されます。それぞれのディレクトリにはExampleTest.phpが最初からありますが、これら二つの違いは「素のPHPUnitのTestCaseクラスを継承している」か「Laravel用に拡張されたTestCaseクラスを継承しているか」です。 Feature/ExampleTest.phpはLaravel用に拡張されたTestCaseクラスを継承しているので、DBアクセスを含むLaravelの機能がテスト内で全て使えます。Unit/ExampleTest.phpは素のPHPUnitのTestCaseクラスを継承しているため、Laravelの機能が使えず、素のPHPコードとしてのロジックテストを書くものになっています。 Featureテストは便利なのですが、Laravel用に拡張されている分テストの実行に時間がかかります。なので、Laravelに依存しない形で書けるテストは極力Unitテストにするべきです。 FeatureテストとUnitテストの比較 次のようなUser Modelがあります。isAdmin()メソッドでは、 roleカラムの値が「ADMIN」の場合にtrueを返します。このメソッドについて、FeatureテストとUnitテストを書いて比較します。 <?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use HasFactory, Notifiable; protected $fillable = [ 'name', 'email', 'password', 'role', ]; protected $hidden = [ 'password', 'remember_token', ]; public function isAdmin(): bool { return $this->role === 'ADMIN'; } } Featureテストの場合 isAdmin()メソッドを呼び出すと、Userモデルは内部でDBアクセスを行い、roleカラムの値をDBから読み出そうとします。なので、Featureテストの場合、実際にDBへデータを登録してからisAdmin()メソッドを呼び出すことになります。 <?php namespace Tests\Feature; use App\Models\User; use Tests\TestCase; class UserTest extends TestCase { public function test_isAdmin() { $user = User::factory()->create(['role' => 'ADMIN']); self::assertTrue($user->isAdmin()); } } 実行結果はこちらです。実行時間は[0.38s]でした。 sail test tests/Feature/UserTest.php PASS Tests\Feature\UserTest ✓ is admin Tests: 1 passed Time: 0.38s Unitテストの場合 Unitテストの場合は、DBアクセスができません。では、どうするかというと、Userモデルをインスタンス化した後に、roleプロパティへ自分でデータをセットします。 <?php namespace Tests\Unit; use App\Models\User; use PHPUnit\Framework\TestCase; class UserTest extends TestCase { public function test_isAdmin() { $user = new User(); $user->role = 'ADMIN'; // これでもOK // $user = new User(['role' => 'ADMIN']); self::assertTrue($user->isAdmin()); } } 実行結果はこちらです。実行時間は[0.06s]でした。Featureテストの1/6程度の時間で終わっています。 sail test tests/Unit/UserTest.php PASS Tests\Unit\UserTest ✓ is admin Tests: 1 passed Time: 0.06s まとめ 今回はFeatureテストとUnitテストの実行時間の比較を行いました。テスト対象のコードはとてもシンプルなものでしたが、それでも6倍以上の差でUnitテストの方が高速ということがわかりました。プロジェクトの全てのテストをFeatureテストにしてしまうと、少なくともこれだけ実行時間に差が出てしまうということですね。 また、実際のFeatureテストでは、裏でLaravelがよしなにやってくれていることも含めると、更に多くのLaravelの便利機能を使うことになると思います。そう考えるとFeatureテストとUnitテストでは6枚以上の実行時間の差が出るのではと思います。みなさん、Unitテスト書いていきましょう。そしてUnitテストが書けるような設計をしましょう。
- 投稿日:2021-06-20T00:05:08+09:00
【Php】validationについて-3
初めに phpのvalidationについて学習した内容のoutput用記事です。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 ※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。 前回の記事: https://qiita.com/redrabbit1104/items/a6e57aa1fd1771ef90ff https://qiita.com/redrabbit1104/items/a3eaf2bba51fac0b3c51 https://qiita.com/redrabbit1104/items/6e9c85c7503ed9043f38 emailとurl入力フォームを追加 メールアドレスとホームページの入力欄を追加します。 メールアドレス <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {echo sp_chars($_POST['email']);} ?>"> <br> ホームページ <input type="url" name="url" value="<?php if (!empty($_POST['url'])) {echo sp_chars($_POST['email']);} ?>"> また、確認画面の方にも入力したメールアドレスとホームページが見れるようにします。 メールアドレス <?php echo sp_chars($_POST["email"]); ?> ホームページ <?php echo sp_chars($_POST["url"]); ?> そしてhiddenタイプで入力した値を渡すようにします。 <input type="hidden" name="email" value="<?php echo sp_chars($_POST['email']); ?>"> <input type="hidden" name="url" value="<?php echo sp_chars($_POST['url']); ?>"> これで準備完了です。 filter_var()関数 メールアドレスやemailなどの値が予め用意されているフィルター(入力規則)に合っているかどうかをチェックする関数。戻り値はtrueまたはfalseになります。 filter_var("対象の値", "適用するフィルタ", "オプション") フィルタにはたくさんの種類がありますが、emailとurlのvalidationで使うフィルタとしては FILTER_VALIDATE_EMAILとFILTER_VALIDATE_URLを使います。 メールアドレスは入力されていない場合と入力内容が正しくない場合にメッセージが表示されるようにします。 if (empty($filter_value['email']) || !filter_var($filter_value['email'], FILTER_VALIDATE_EMAIL)) { $messages[] = '正しい形式のメールアドレスを入力してください。'; } ホームページは入力されなかった場合を除いて、正しく入力されたかをチェックしたいのでif文の中にif文を用意してvalidationを掛けます。 if (!empty($filter_value['url'])) { if (!filter_var($filter_value['url'], FILTER_VALIDATE_URL)) { $messages[] = '正しい形式のホームページを入力してください。'; } } emailやurlはhtml側で独自のvalidationが掛かりますので、php側のvalidationを確認するためには入力画面のinput欄のtypeをtextに変更する必要があります。 メールアドレス <input type="text" name="email" //typeをemailからtextに変更 value="<?php if (!empty($_POST['email'])) {echo sp_chars($_POST['email']);} ?>"> <br> ホームページ <input type="text" name="url" //typeをurlからtextに変更 value="<?php if (!empty($_POST['url'])) {echo sp_chars($_POST['email']);} ?>"> 参考サイト https://www.php.net/manual/ja/function.filter-var.php https://www.php.net/manual/ja/filter.filters.validate.php
- 投稿日:2021-06-20T00:01:26+09:00
データベースの全テーブル情報をPHPでCSV出力する方法
はじめに データベースにあるテーブルの情報をCSV出力するためのベストな方法を模索し、速度を計測したので記事にまとめたいと思います。 動作確認環境 PHP 8.0 Laravel 8.0 ※下位環境でも動作する場合がございます 補足 Dockerを使っていて、PHPコンテナからMySQLコンテナにコマンドが通らない場合はこちらの記事に解決法が記載されています。 Laravel(PHP)でのCSV出力方法について 主に以下の3つの方法がありました fwrite fputcsv mysqlコマンドを直接実行する fwriteとfputcsvの違いについては、こちらの記事が勉強になりました。 fwriteとfputcsvではCSV化したものをExcelで開くことがある場合は、fputcsvの方が軍配が上がる印象です。 ということで、今回はfwriteは候補から除外し、fputcsvとmysqlとでどちらが便利かを調査しました。 計測条件 1つのテーブルに一千万のレコードが入っている テーブルから指定期間のデータをCSV出力する その際出力するデータ量は約100万レコードとする 計測データ 計測に使ったソースコード $sumCount = DB::table('samples') ->where('created_at', '>=', '2026-06-22 00:00:00') ->where('created_at', '<', '2026-12-22 00:00:00') ->count(); $fetchNeedCount = (int)ceil($sumCount / 50000); for ($i = 0; $i < $fetchNeedCount; $i++) { $records = DB::table('samples') ->where('created_at', '>=', '2026-06-22 00:00:00') ->where('created_at', '<', '2026-12-22 00:00:00') ->skip($i * 50000) ->take(50000) ->get(); $stream = fopen('php://temp', 'r+b'); foreach ($records as $record) { fputcsv($stream, (array)$record); } rewind($stream); $csv = str_replace(PHP_EOL, "\r\n", stream_get_contents($stream)); $csv = mb_convert_encoding($csv, 'SJIS-win', 'UTF-8'); file_put_contents('test.csv', $csv, FILE_APPEND); } $cmd = 'mysql -B -N -u phper -h db -psecret -D local_laravel -e "SELECT * FROM samples where created_at >= \'2026-06-22 00:00:00\' and created_at < \'2026-12-22 00:00:00\'" | sed -e "s/\t/,/g" > test.csv'; exec($cmd); 結果 項目 計測時間 fputcsv 2m45s mysqlコマンド 35s なお、下記SQLを生でDBに叩いた時の計測時刻は24sでした。 SELECT * FROM samples where created_at >= '2026-06-22 00:00:00' and created_at < '2026-12-22 00:00:00'; まとめ 取得件数が少ない場合は、fputcsvとmysqlコマンドの差はそれほどなかったのですが、CSV化の対象が多くなると、fputcsv側ではメモリオーバーの発生を避けるためにクエリー発行を複数回に分ける必要が出てきたため、より時間がかかってしまう結果となりました。 なお、今回はcreated_atの型はTIMESTAMP型としましたが、時刻をUNIXタイムスタンプでも管理し、そのカラムで絞り込むというチューニング方法を用いるとさらに処理が速くなりそうです。 [追記] 実際にint型でタイムスタンプを挿入し、その値基準でしぼりこみを行うと5秒程度速くなること確認できました。 完成版コード mysqlコマンドの方がベターであることが分かったので、これを基準としてLaravelで全テーブルの情報をCSV化するコマンドを作成しましたので合わせて記載したいと思います。 <?php namespace App\Console\Commands; use Carbon\Carbon; use DB; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; use LogicException; /** * Class ConvertCSV. */ class ConvertCSV extends Command { /** * The filesystem instance. * * @var \Illuminate\Filesystem\Filesystem */ protected $file; /** * The name and signature of the console command. * * @var string */ protected $signature = 'convert_csv {--day=} {--period=1}'; /** * The console command description. * * @var string */ protected $description = 'convert_csv'; /** * List of connection names to be output. * * @var array */ protected $targetConnectionNames = [ 'mysql', ]; /** * FileOperation constructor. */ public function __construct( Filesystem $file ) { $this->file = $file; parent::__construct(); } /** * Execute the console command. */ public function handle(): void { [$startDate, $endDate] = $this->getStartEndTime(); $this->createDirectory(storage_path(sprintf('dump_csv/%s', $startDate))); foreach ($this->targetConnectionNames as $connectionName) { $configConnection = config('database.connections.'.$connectionName); if (empty($configConnection)) { throw new LogicException(sprintf('The target was not found:%s', $connectionName)); } $databaseName = $configConnection['database']; $username = $configConnection['username']; $password = $configConnection['password']; if (empty($databaseName) || empty($username) || empty($password)) { throw new LogicException(sprintf( 'Incorrect config settings:databaseName[%s] username[%s] password[%s]', $connectionName, $username, $password)); } $schema = DB::connection($connectionName)->getDoctrineSchemaManager(); $tableNames = $schema->listTableNames(); foreach ($tableNames as $tableName) { $mysqlBaseCommand = sprintf('mysql -B -N -u %s -h db -p%s -D %s -e', $username, $password, $databaseName); $mysqlCommand = sprintf("SELECT * FROM %s where created_at >= '%s 00:00:00' and created_at < '%s 00:00:00'", $tableName, $startDate, $endDate ); $outputFilePath = storage_path(sprintf('dump_csv/%s/%s.csv', $startDate, $tableName)); $cmd = sprintf('%s "%s" | sed -e "s/\t/,/g" > %s', $mysqlBaseCommand, $mysqlCommand, $outputFilePath); exec($cmd); $this->comment(sprintf('Completed:%s', $tableName)); } } } /** * Directory creation. * * @param string $directoryPath */ protected function createDirectory(string $directoryPath): void { if (! is_dir($directoryPath)) { $result = $this->file->makeDirectory($directoryPath, 0777, true); if (! $result) { throw new LogicException(sprintf('Directory creation failure:%s', $directoryPath)); } } } /** * Get start date and time and end date and time. */ protected function getStartEndTime(): array { $period = $this->option('period'); if ($this->option('day')) { $startDate = $this->option('day'); $endDate = Carbon::parse($startDate)->addDays($period)->format('Y-m-d'); } else { $carbon = Carbon::now(); $startDate = $carbon->format('Y-m-d'); $endDate = $carbon->addDays($period)->format('Y-m-d'); } if ($startDate > $endDate) { return [$endDate, $startDate]; } return [$startDate, $endDate]; } } 解説 mysqlのコマンド部分を変数化してより汎用性ある形にしました。 出力先は日付単位でstorage直下に出力されるようにしてあります。 Laravelではconfigでデータベース関連の値を記述する仕組みとなっているため、mysqlコマンドに使う値はそこから取得しています。 また、テーブル名一覧においてもLaravelであれば下記記事の方法で取得することが可能です。 なお、CSVファイルの先頭にヘッダーを付けたい場合はmysqlコマンドの「-N」を外すだけで大丈夫です。 勉強に使った関連記事やサイト一覧