- 投稿日:2020-01-10T19:26:23+09:00
PHPで他URLのファイルを転送してダウンロードさせる
やっていることはトンネル。
function forwardBin($url) { // ウェブブラウザが独自にMIMEタイプを判断する処理を抑止する header('X-Content-Type-Options: nosniff'); // ダウンロードするべきファイル情報を先に取得して、その内容をそのまま設定 $filename = basename($url); $headers = get_headers($url, 1); header('Content-Type: ').$headers['Content-Type']; header('Content-Length: '.$headers['Content-Length']); header("Content-Disposition: attachment; filename=${filename}"); // readfile()の前に出力バッファリングを無効化する while (ob_get_level()) { ob_end_clean(); } // ダウンロード readfile($url); exit; }【PHP】正しいダウンロード処理の書き方 を参考にしました。
- 投稿日:2020-01-10T18:02:48+09:00
一時間に一回だけAPIを実行するPHPのオブジェクトファイル
一時間に一回だけAPIを実行するPHPのオブジェクトファイル
あるユーザーが公開しているプログラミングを参考にして
オブジェクト化してみた。尚、動作環境はPHP5.6以上になります、と言いつつ
動作テストは行っていないので、もしかしたらエラーで動かないかも?
動作内容はJSONファイルの更新時間( hour )と
サーバの 時間 ( hour ) を比べ差異があれば
APIを呼び出し結果をJSONファイルとして上書き保存します。
そのため、一時間に一回だけ更新処理が走ります。
(※CRONで設定していれば)結果がJSONで返ってこない場合などは可変して頂いて構いません。
もともと自分の案でもないので…。PHPファイルのダウンロードはこちらから
https://zip358.com/tool/timeKeeper/timeKeeper.zipソースコードはこちらになります( ̄(エ) ̄)
<?php class timeKeeper{ public static $json_filename = "abc.json"; public static $json_api_url = "https://example.com/api/?v=1.333"; public static function judge(){ $server_timestamp = time(); $server_time = date('Y/m/d H',$server_timestamp); $json_timestamp = filemtime(self::$json_filename); $json_time = date('Y/m/d H',$json_timestamp); return $server_time === $json_time ? true : false; } public static function api_run($opts=null){ if(is_null($opts))return false; $context = stream_context_create($opts); $json = file_get_contents(self::$json_api_url, false, $context); $fp = fopen(self::$json_filename, "w"); fwrite($fp,$json); fclose($fp); return self::json_load(); } public static function json_load(){ $json = file_get_contents(self::$json_filename); return json_decode($json, true); } public static function check(){ if(file_exists(self::$json_filename)){ return self::judge(); } return false; } } ///使用例 if(timeKeeper::check()){ $json = timeKeeper::json_load(); }else{ $opts = array( "http"=>array( "method" => "POST", "header" => "User-Agent: php" ) ); $json = timeKeeper::api_run($opts); }
- 投稿日:2020-01-10T18:01:16+09:00
LaravelにあるEloquentのfindとfirstOrFailにハマったけど、解決した話
はじめに
以下のような使い方をしてしまい、想定した結果にならずハマりました
// $idには1が入っている $user = User::find($id)->firstOrFail();知ってしまった今、なんてこと無い話になりますが、未来の自分に向けて備忘録を残しておきます
Laravelのバージョン
$ php artisan -V Laravel Framework 5.8.29前提
- 以下のモデルは作成済み
- App/User.php
結論
// $idには1が入っている $user = User::find($id)->firstOrFail();上記のような使い方をすると、以下のように2回SQLが実行されていました
select * from users where id = 1; select * from users limit 1;そのため、変数には2回目のSQLの実行結果が入っており、想定した動作になっていませんでした
対策
以下の書き方であれば、同じ動きになるようです
その①
// $idには1が入っている $user = User::findOrFail($id);その② firstOrFailを使用する場合(where句のあとはOKでした)
// $idには1が入っている $user = User::where('id', $id)->firstOrFail();参考
- 投稿日:2020-01-10T16:19:53+09:00
Laravel × PHPUnit & Postman でのテストコード(Passport にも対応)
Laravel で開発されているプロダクトにおいてテストコードを書く方法を、 PHPUnit および Postman を主体にしてまとめます。
テスト作成の背景
弊社(株式会社 NoSchool)では、Web フロントエンドに Nuxt を利用しており、また、iOS ネイティブアプリも開発しているため、Laravel を原則 API ベースで開発することが多いです。
API のテストを作成することは、バグのリスクを低減したり、アプリエンジニアとの仕様の共有のために必須となります。2020 年 1 月現在 NoSchool では
PHPUnitおよびPostmanを利用していますので、ドキュメントを兼ねてまとめました。この記事を読むとわかること
- PHPUnit を使って API 単位でのテストを書く方法
- PHPUnit を使って Class 単位での単体テストを書く方法
- Postman を使って API のレスポンスをテストする方法
- それぞれの比較と、実運用する際に共存する方法
PHPUnit
概要
PHPUnit は、PHP のコードベースでテストを記述できるフレームワークです。
PHPUnit でテストを書く目的
PHP のコードベースのため、データベースのサンプルデータを柔軟に生成し、テスト後に破棄すると言ったことが可能になります。そのため、細かいテストケースを実装するのに向いています。
書き方
ディレクトリ構造
弊社では
testsディレクトリ以下に下記のディレクトリ構造でテストを書いていますので、参考までに。
./Feature結合テスト(API テスト)を書きます。HTTP のエンドポイント単位でのテストを書きます。API を開発したときは原則必須で対応するテストコードを実装しましょう。
./Unit単体テストを書きます。クラス単位でのテストを書きますが、工数が多くかかること、設計変更に追従するコスト、および Final Class をモック化できない Mockery の制限などの理由から、あまり実装されることはありません。Rendering まで Laravel で行っている一部ページや、クラス単位でのテストをどうしても書きたい場合はこちらに書きます。
./Fixtureテスト用のデータを書きます。アプリ経由の課金で Apple から送信されるレシートなど、細かいデータを表現するときに使います。
./Lib複数のテストケースの基底となるクラスなどを格納します。各テストコードの基本
各テストコードは、それぞれのディレクトリ以下にドメインごとに namespace を切って作成してください。また、命名は ◯◯Test.php で作成してください。
ex. ./Feature/Chat/RoomTest.php
また、各テストケースは
Tests\TestCaseクラスを継承してください。final class RoomTest extends TestCase各テストケースの基本
各テストケースは、test◯◯ という名称の関数で作成します。
use Tests\TestCase; final class RoomTest extends TestCase { protected function setUp() // 後述 { parent::setUp(); } public function testSendMessage() { // hogehoge }API テスト
一つのテストケースの中で、下記のようなフェーズを順に実行します。
- テストデータの準備
- API リクエストの実行
- レスポンスのステータス、Body のテスト
- 必要に応じて、変化したデータベースの中身のテスト
テストデータの準備
一般に、各テストケースにおけるテストデータの準備は、
setUp()メソッドにて実装します。setUp()は各テストケースの実行直前に呼ばれます。反対に、終わったあとに呼ばれるのはtearDown()です。こちらはデータの後始末などに利用します。protected function setUp() { parent::setUp(); $this->room = factory(Room::class)->create([ 'user_id' => 1111 ]); }ここでのポイントは大きく 2 点あります。
まず、
parent::setUp()を実行することです。これを実行することで、Laravel アプリケーションを動かすための初期設定が終わります。実行しない場合、例えばconfig()を使った実装等が動かないので必ず実行してください。詳しい実装は
/Illuminate/Foundation/Testing/TestCase.phpに書いてあります。2 つめのポイントは
factory()グローバルヘルパを利用してテストデータを生成することです。ファクトリは
database/factories以下に作成します。詳しい書き方は実際のコードを読んだり、公式ドキュメントを参照してください。ここで重要なのは以下の 2 点です。
- factory を create するタイミングでテストデータを Array で渡すことで上書きできる
- factory 自体に名前をつけることができるため、例えば同じ User Model に対する factory でも違う初期データを持ったインスタンスを作り分けることができる
ファクトリ生成後は、実データがデータベースにインサートされているため、それがある前提で以降のテストを書くことができます。
【補足】テストデータの削除
テストデータを毎回テストのたびに生成していると、データベースがテストデータで溢れかえってしまいます。ここで、
DatabaseTransactionsを利用することで当該テストにて作成されたデータは全てテスト終了後に削除されます。use Illuminate\Foundation\Testing\DatabaseTransactions; class RoomTest extends TestCase { use DatabaseTransactions;API リクエストの実行
API リクエストは下記のように実行します。リクエストするメソッドには複数ありますが、個人的には
jsonメソッドが好きです。$response = $this->json( 'PUT', "/api/chat/{$this->room->id}", [ 'body' => 'message ], $headers )【補足】Laravel Passport を利用している場合
Passport を使っていると、都度都度 API リクエストに特定のヘッダを指定しなければなりません。具体的には、
Bearerトークンが必要です。これを解消するための手段を公開してくれている Web ページが有りましたので、ご参照ください。大まかに言えば、
setUp()を拡張してそこでUserに対応したアクセストークンを都度都度発行しています。https://www.whizz-tech.co.jp/1442/
こうすることで、
json()実行時の第 4 引数にヘッダーでBearerを渡せば認証済みユーザーとしてテストが実行できます。また、弊社のネイティブアプリ向けの API の場合、ログアウトユーザーでもクライアント単位でのグラントトークンを Bearer に載せることを必須としているのですが、そのトークンも
setUp()を使って実装できます。/oauth/tokenへ POST を飛ばしてトークンを都度発行すればいいです。【補足】Cookie 認証の API を利用している場合
Cookie 認証の API を利用していて、かつ認証済みのユーザーで API をリクエストしたいときは
actingAsメソッドを利用します。$response = $this->actingAs($user) ->json('PUT', '/api/room', [ ]);
factory(User::class)->create()でテストユーザーを作成後、そのユーザーで認証された状態で API を叩いた場合のテストなどをするときに有効です。レスポンスのステータス、Body のテスト
レスポンスのステータスは、下記のようにテストします。
$response->assertStatus(204);レスポンスの Body は、下記のようにテストします。
$response->assertJson([ 'message_id' => 100 ]);この場合のテストは一例で、具体的な値をテストできるものから、JSON の構造のみテストするものまで色々あります。
assertJsonはかなりゆるく、指定したキーを持ってさえいれば、余分なキーが含まれていてもエラーになりません(第 2 引数でより strict にできるようです)。ここでどうテストするかはテストケースで何をしたいかによるでしょう。ただ、特にネイティブアプリへのレスポンスなど、型にまで気を使わなければいけない場合において、下記のように構造とそれぞれの持っている型を見ることになると思います。
$response->assertJsonStructure([ 'id', 'user' => [ 'id', 'name', ], 'created_at', ]); $this->assertIsInt($response->json('id')); $this->assertIsInt($response->json('created_at')); $this->assertIsInt($response->json('user.id')); $this->assertIsString($response->json('user.name'));ネストしているキーに、Laravel にあるあるのドット記法でアクセスできるのは便利ですね。
詳しくは公式情報をどうぞ。
https://laravel.com/api/5.7/Illuminate/Foundation/Testing/TestResponse.html
https://phpunit.readthedocs.io/ja/latest/assertions.html#assertisint必要に応じて、変化したデータベースの中身のテスト
副作用のある API であれば、データベースに変更をもたらすでしょう。
データベースの中身をテストする場合は下記のように記述します。
$this->assertEquals( 3, Room::where('body', 'hogehoge')->count() );https://readouble.com/laravel/5.7/ja/database-testing.html を見る限り、
assertDatabaseHasというメソッドも用意されています。しかし、この場合はカウントで 2 つ以上のものがあること、というのは確認できませんので、用途に応じて使い分けてください。このように Eloquent Model を使ってクエリを発行して結果をチェックというのは少々我ながら筋が悪いため、より上手に書ける方法を知っている方は教えていただけると嬉しいです。
単体テスト
単体テストは、前述したとおり結構モック化を頑張らないと厳密な単体テストの実現が難しいです。
もし実装したい場合は、下記のようなコツを念頭に実装してみてください。
ここではユースケース層のテストを例に話します。NoSchool の場合、ユースケース層からは Repository の Interface を呼んでおり、
AppServiceProviderで実装クラスにbindingしているという特徴を持っているため、その前提での解説となります。DI を使っている場合は
resolveヘルパでインスタンスを取得DI している場合は普通にコンストラクタを書けないはずなので、
resolveヘルパでインスタンスを取得し、そこからメソッドを実行してテストします。$sendChatMessageUseCase = resolve(SendChatMessageUseCase::class);テストしたいクラス内で使っている外部クラスを Mock にしたい場合、DI するのが一番確実っぽい
以下の例はユースケースで使っている Repository をモック化した例です。
$repository = Mockery::mock(new MemberRepository($this->user)); $repository->shouldReceive('findMember')->once()->andReturn( $member ); $repository->shouldReceive('createMember')->once()->andReturn($member); $this->app->instance( 'App\Infrastructure\MemberRepository', $repository );final クラスをモック化するときは
alias:記法を使うfinal なクラスをモックにするときは
alias:を先頭につけて namespace を記述します。なんじゃこりゃ。まあ、そもそも final なクラスをモックにする意味あるのかって話なのですが。$requestStub = \Mockery::mock('alias:App\Helpers\HttpClient');といったややこしい仕様が散見されるので、特に
finalなクラスを使うのをやめるか、\Mockeryを使った単体テストはあまり書かないようにするか、というのが工数的なデメリットを鑑みると妥当かなというのが感想です。より良いライブラリや、Mockery でももっと楽に書けるぞ、という情報提供お待ちしております。テストの実行
テストの実行は、
./vendor/bin/phpunitコマンドで実行します。ディレクトリをオペランドに指定することも、ファイルを指定することも、何も指定せずに全てをテストすることも可能です。[mejileben]$ ./vendor/bin/phpunit ./tests/Feature/Room上記のコマンドでは、
Roomディレクトリ以下のテストケースを全て実行します。コマンドを覚えたくない方は、
composerやartisanコマンドとしてエイリアスを貼ってもいいでしょう。Postman
概要
Postman は、API のドキュメントを書いたり、テストケースを書いてまとめて複数の API をテストできるサービスです。
Swagger のテスト実行機能がついたようなイメージです。Swagger を使ったことがないのでイメージですが。
Postman を導入する目的
Postman を導入することで、API ドキュメントをネイティブアプリエンジニアに Web 経由で共有することができます。NoSchool では API 定義を JSON でエクスポートし、Git 管理しています。これを各自が Import することで API 定義を確認できます。有料プランに加入することで複数人が Web 上で共有を完結できるそうですが、ケチっているのでまだ無料プランです。
Postman 環境設定の概要
詳しくは割愛しますが、主に以下の点に注意が必要です。
- 秘匿情報は Environment として管理し、GitHub には上げない。または暗号化する
- ローカル環境で HTTPS を利用しているとき、設定画面から SSL のチェックを無効化しないとオレオレ証明書でエラーを吐いてしまう
Postman を使った API テストのしかた
Postman で API 定義を作成し、試しに「Send」ボタンを押すと、レスポンスが返ってきます。
ここまで終わったら、「Tests」タブを開き、下記のようにテストを書いていきます。
var jsonData = pm.response.json(); pm.test("id is number", function() { pm.expect(jsonData.id).to.be.a("number"); }); pm.test("room.id is 1540", function() { pm.expect(jsonData.room.id).to.eq(1540); });Postman のテストケースは
JavaScriptのライブラリであるChaiを利用しているため、JavaScript ベースで書くことができます。まず
json()メソッドでレスポンスの JSON を取得します。次に
pm.test()でテストケースを作成します。expect()でテストしたい値を JSON から抜き取り、その後は英文を書くかのようにメソッドチェーンで.to.be.aなどとつないでいき、最後に型名か具体的な値を書くことでテストが実行されます。ここでは
.toや.beをつけることは必須ではないのですが、そのほうが英文っぽく書けるので好ましいと思っています。それだけです。Rspec 等と思想が似ていると思います。Postman のテストでは具体的な値等までテストすることは難しいので、基本的にレスポンスの型のみテストすることが大半です。
Collection を作る
さて、API テストをいくつか作成すると、Collection というものにまとめることができます。Collection は、例えば Q&A サイトの場合、「質問したあとに回答がつき、それをベストアンサーにする」といった複数の連続した API のテストをすることが可能です。
テストを実行する
テストの実行は、クライアントアプリケーション上からもできるし、CLI から Node.js 等のコマンドベースでも実行可能です。
Postman を利用するメリットは?
API テストの表現力という意味では PHPUnit で頑張るほうが身の丈に合ってそうですが、例えば検証環境など、HTTP 越しでアクセスしたい場合は Postman でリクエストの向き先を Localhost から検証環境に向けて実行することができるなど、手軽さという意味では Postman に軍配があがるイメージです。
ただ、CI を組み込んだ場合などは結局 PHPUnit を検証環境で実行することなど簡単でしょうから、Postman でテストを頑張るメリットは薄れ、Swagger 等と同様で API の定義を複数の社員間で共有することがメイン目的となるでしょう。
個人的には Passport を使っている環境でも PHPUnit ベースでテストできることが衝撃でした。先の記事を公開した方に感謝です。
まとめ
PHPUnit も Postman も両方 API ベースでのテストの記述が可能ですが、PHPUnit のほうがより細かい制御が可能です。Postman は API 定義をまとめるのに特化して、PHPUnit でできる限りテストを書いていくのが 1 つの良い共存方法かと思います。
- 投稿日:2020-01-10T16:19:53+09:00
Laravel × PHPUnit & Postman でのテストコードまとめ(Passport にも対応)
Laravel で開発されているプロダクトにおいてテストコードを書く方法を、 PHPUnit および Postman を主体にしてまとめます。
テスト作成の背景
弊社(株式会社 NoSchool)では、Web フロントエンドに Nuxt を利用しており、また、iOS ネイティブアプリも開発しているため、Laravel を原則 API ベースで開発することが多いです。
API のテストを作成することは、バグのリスクを低減したり、アプリエンジニアとの仕様の共有のために必須となります。2020 年 1 月現在 NoSchool では
PHPUnitおよびPostmanを利用していますので、ドキュメントを兼ねてまとめました。この記事を読むとわかること
- PHPUnit を使って API 単位でのテストを書く方法
- PHPUnit を使って Class 単位での単体テストを書く方法
- Postman を使って API のレスポンスをテストする方法
- それぞれの比較と、実運用する際に共存する方法
環境
- Laravel 5.7(古い...)
- PHPUnit 7.0
- Mockery 1.2
PHPUnit
概要
PHPUnit は、PHP のコードベースでテストを記述できるフレームワークです。
PHPUnit でテストを書く目的
PHP のコードベースのため、データベースのサンプルデータを柔軟に生成し、テスト後に破棄すると言ったことが可能になります。そのため、細かいテストケースを実装するのに向いています。
書き方
ディレクトリ構造
弊社では
testsディレクトリ以下に下記のディレクトリ構造でテストを書いていますので、参考までに。
./Feature結合テスト(API テスト)を書きます。HTTP のエンドポイント単位でのテストを書きます。API を開発したときは原則必須で対応するテストコードを実装しましょう。
./Unit単体テストを書きます。クラス単位でのテストを書きますが、工数が多くかかること、設計変更に追従するコスト、および Final Class をモック化できない Mockery の制限などの理由から、あまり実装されることはありません。Rendering まで Laravel で行っている一部ページや、クラス単位でのテストをどうしても書きたい場合はこちらに書きます。
./Fixtureテスト用のデータを書きます。アプリ経由の課金で Apple から送信されるレシートなど、細かいデータを表現するときに使います。
./Lib複数のテストケースの基底となるクラスなどを格納します。各テストコードの基本
各テストコードは、それぞれのディレクトリ以下にドメインごとに namespace を切って作成してください。また、命名は ◯◯Test.php で作成してください。
ex. ./Feature/Chat/RoomTest.php
また、各テストケースは
Tests\TestCaseクラスを継承してください。final class RoomTest extends TestCase各テストケースの基本
各テストケースは、test◯◯ という名称の関数で作成します。
use Tests\TestCase; final class RoomTest extends TestCase { protected function setUp() // 後述 { parent::setUp(); } public function testSendMessage() { // hogehoge }API テスト
一つのテストケースの中で、下記のようなフェーズを順に実行します。
- テストデータの準備
- API リクエストの実行
- レスポンスのステータス、Body のテスト
- 必要に応じて、変化したデータベースの中身のテスト
テストデータの準備
一般に、各テストケースにおけるテストデータの準備は、
setUp()メソッドにて実装します。setUp()は各テストケースの実行直前に呼ばれます。反対に、終わったあとに呼ばれるのはtearDown()です。こちらはデータの後始末などに利用します。protected function setUp() { parent::setUp(); $this->room = factory(Room::class)->create([ 'user_id' => 1111 ]); }ここでのポイントは大きく 2 点あります。
まず、
parent::setUp()を実行することです。これを実行することで、Laravel アプリケーションを動かすための初期設定が終わります。実行しない場合、例えばconfig()を使った実装等が動かないので必ず実行してください。詳しい実装は
/Illuminate/Foundation/Testing/TestCase.phpに書いてあります。2 つめのポイントは
factory()グローバルヘルパを利用してテストデータを生成することです。ファクトリは
database/factories以下に作成します。詳しい書き方は実際のコードを読んだり、公式ドキュメントを参照してください。ここで重要なのは以下の 2 点です。
- factory を create するタイミングでテストデータを Array で渡すことで上書きできる
- factory 自体に名前をつけることができるため、例えば同じ User Model に対する factory でも違う初期データを持ったインスタンスを作り分けることができる
ファクトリ生成後は、実データがデータベースにインサートされているため、それがある前提で以降のテストを書くことができます。
【補足】テストデータの削除
テストデータを毎回テストのたびに生成していると、データベースがテストデータで溢れかえってしまいます。ここで、
DatabaseTransactionsを利用することで当該テストにて作成されたデータは全てテスト終了後に削除されます。use Illuminate\Foundation\Testing\DatabaseTransactions; class RoomTest extends TestCase { use DatabaseTransactions;API リクエストの実行
API リクエストは下記のように実行します。リクエストするメソッドには複数ありますが、個人的には
jsonメソッドが好きです。$response = $this->json( 'PUT', "/api/chat/{$this->room->id}", [ 'body' => 'message ], $headers )【補足】Laravel Passport を利用している場合
Passport を使っていると、都度都度 API リクエストに特定のヘッダを指定しなければなりません。具体的には、
Bearerトークンが必要です。これを解消するための手段を公開してくれている Web ページが有りましたので、ご参照ください。大まかに言えば、
setUp()を拡張してそこでUserに対応したアクセストークンを都度都度発行しています。https://www.whizz-tech.co.jp/1442/
こうすることで、
json()実行時の第 4 引数にヘッダーでBearerを渡せば認証済みユーザーとしてテストが実行できます。また、弊社のネイティブアプリ向けの API の場合、ログアウトユーザーでもクライアント単位でのグラントトークンを Bearer に載せることを必須としているのですが、そのトークンも
setUp()を使って実装できます。/oauth/tokenへ POST を飛ばしてトークンを都度発行すればいいです。補足: https://laravel.com/docs/5.7/passport#testing に書かれている
actingAsを使った実装は何故か動きませんでした【補足】Cookie 認証の API を利用している場合
Cookie 認証の API を利用していて、かつ認証済みのユーザーで API をリクエストしたいときは
actingAsメソッドを利用します。$response = $this->actingAs($user) ->json('PUT', '/api/room', [ ]);
factory(User::class)->create()でテストユーザーを作成後、そのユーザーで認証された状態で API を叩いた場合のテストなどをするときに有効です。レスポンスのステータス、Body のテスト
レスポンスのステータスは、下記のようにテストします。
$response->assertStatus(204);レスポンスの Body は、下記のようにテストします。
$response->assertJson([ 'message_id' => 100 ]);この場合のテストは一例で、具体的な値をテストできるものから、JSON の構造のみテストするものまで色々あります。
assertJsonはかなりゆるく、指定したキーを持ってさえいれば、余分なキーが含まれていてもエラーになりません(第 2 引数でより strict にできるようです)。ここでどうテストするかはテストケースで何をしたいかによるでしょう。ただ、特にネイティブアプリへのレスポンスなど、型にまで気を使わなければいけない場合において、下記のように構造とそれぞれの持っている型を見ることになると思います。
$response->assertJsonStructure([ 'id', 'user' => [ 'id', 'name', ], 'created_at', ]); $this->assertIsInt($response->json('id')); $this->assertIsInt($response->json('created_at')); $this->assertIsInt($response->json('user.id')); $this->assertIsString($response->json('user.name'));ネストしているキーに、Laravel にあるあるのドット記法でアクセスできるのは便利ですね。
詳しくは公式情報をどうぞ。
https://laravel.com/api/5.7/Illuminate/Foundation/Testing/TestResponse.html
https://phpunit.readthedocs.io/ja/latest/assertions.html#assertisint必要に応じて、変化したデータベースの中身のテスト
副作用のある API であれば、データベースに変更をもたらすでしょう。
データベースの中身をテストする場合は下記のように記述します。
$this->assertEquals( 3, Room::where('body', 'hogehoge')->count() );https://readouble.com/laravel/5.7/ja/database-testing.html を見る限り、
assertDatabaseHasというメソッドも用意されています。しかし、この場合はカウントで 2 つ以上のものがあること、というのは確認できませんので、用途に応じて使い分けてください。このように Eloquent Model を使ってクエリを発行して結果をチェックというのは少々我ながら筋が悪いため、より上手に書ける方法を知っている方は教えていただけると嬉しいです。
単体テスト
単体テストは、前述したとおり結構モック化を頑張らないと厳密な単体テストの実現が難しいです。
もし実装したい場合は、下記のようなコツを念頭に実装してみてください。
ここではユースケース層のテストを例に話します。NoSchool の場合、ユースケース層からは Repository の Interface を呼んでおり、
AppServiceProviderで実装クラスにbindingしているという特徴を持っているため、その前提での解説となります。DI を使っている場合は
resolveヘルパでインスタンスを取得DI している場合は普通にコンストラクタを書けないはずなので、
resolveヘルパでインスタンスを取得し、そこからメソッドを実行してテストします。$sendChatMessageUseCase = resolve(SendChatMessageUseCase::class);テストしたいクラス内で使っている外部クラスを Mock にしたい場合、DI するのが一番確実っぽい
以下の例はユースケースで使っている Repository をモック化した例です。
$repository = Mockery::mock(new MemberRepository($this->user)); $repository->shouldReceive('findMember')->once()->andReturn( $member ); $repository->shouldReceive('createMember')->once()->andReturn($member); $this->app->instance( 'App\Infrastructure\MemberRepository', $repository );final クラスをモック化するときは
alias:記法を使うfinal なクラスをモックにするときは
alias:を先頭につけて namespace を記述します。なんじゃこりゃ。まあ、そもそも final なクラスをモックにする意味あるのかって話なのですが。$requestStub = \Mockery::mock('alias:App\Helpers\HttpClient');といったややこしい仕様が散見されるので、特に
finalなクラスを使うのをやめるか、\Mockeryを使った単体テストはあまり書かないようにするか、というのが工数的なデメリットを鑑みると妥当かなというのが感想です。より良いライブラリや、Mockery でももっと楽に書けるぞ、という情報提供お待ちしております。テストの実行
テストの実行は、
./vendor/bin/phpunitコマンドで実行します。ディレクトリをオペランドに指定することも、ファイルを指定することも、何も指定せずに全てをテストすることも可能です。[mejileben]$ ./vendor/bin/phpunit ./tests/Feature/Room上記のコマンドでは、
Roomディレクトリ以下のテストケースを全て実行します。コマンドを覚えたくない方は、
composerやartisanコマンドとしてエイリアスを貼ってもいいでしょう。Postman
概要
Postman は、API のドキュメントを書いたり、テストケースを書いてまとめて複数の API をテストできるサービスです。
Swagger のテスト実行機能がついたようなイメージです。Swagger を使ったことがないのでイメージですが。
Postman を導入する目的
Postman を導入することで、API ドキュメントをネイティブアプリエンジニアに Web 経由で共有することができます。NoSchool では API 定義を JSON でエクスポートし、Git 管理しています。これを各自が Import することで API 定義を確認できます。有料プランに加入することで複数人が Web 上で共有を完結できるそうですが、ケチっているのでまだ無料プランです。
Postman 環境設定の概要
詳しくは割愛しますが、主に以下の点に注意が必要です。
- 秘匿情報は Environment として管理し、GitHub には上げない。または暗号化する
- ローカル環境で HTTPS を利用しているとき、設定画面から SSL のチェックを無効化しないとオレオレ証明書でエラーを吐いてしまう
Postman を使った API テストのしかた
Postman で API 定義を作成し、試しに「Send」ボタンを押すと、レスポンスが返ってきます。
ここまで終わったら、「Tests」タブを開き、下記のようにテストを書いていきます。
var jsonData = pm.response.json(); pm.test("id is number", function() { pm.expect(jsonData.id).to.be.a("number"); }); pm.test("room.id is 1540", function() { pm.expect(jsonData.room.id).to.eq(1540); });Postman のテストケースは
JavaScriptのライブラリであるChaiを利用しているため、JavaScript ベースで書くことができます。まず
json()メソッドでレスポンスの JSON を取得します。次に
pm.test()でテストケースを作成します。expect()でテストしたい値を JSON から抜き取り、その後は英文を書くかのようにメソッドチェーンで.to.be.aなどとつないでいき、最後に型名か具体的な値を書くことでテストが実行されます。ここでは
.toや.beをつけることは必須ではないのですが、そのほうが英文っぽく書けるので好ましいと思っています。それだけです。Rspec 等と思想が似ていると思います。Postman のテストでは具体的な値等までテストすることは難しいので、基本的にレスポンスの型のみテストすることが大半です。
Collection を作る
さて、API テストをいくつか作成すると、Collection というものにまとめることができます。Collection は、例えば Q&A サイトの場合、「質問したあとに回答がつき、それをベストアンサーにする」といった複数の連続した API のテストをすることが可能です。
テストを実行する
テストの実行は、クライアントアプリケーション上からもできるし、CLI から Node.js 等のコマンドベースでも実行可能です。
Postman を利用するメリットは?
API テストの表現力という意味では PHPUnit で頑張るほうが身の丈に合ってそうですが、例えば検証環境など、HTTP 越しでアクセスしたい場合は Postman でリクエストの向き先を Localhost から検証環境に向けて実行することができるなど、手軽さという意味では Postman に軍配があがるイメージです。
ただ、CI を組み込んだ場合などは結局 PHPUnit を検証環境で実行することなど簡単でしょうから、Postman でテストを頑張るメリットは薄れ、Swagger 等と同様で API の定義を複数の社員間で共有することがメイン目的となるでしょう。
個人的には Passport を使っている環境でも PHPUnit ベースでテストできることが衝撃でした。先の記事を公開した方に感謝です。
まとめ
PHPUnit も Postman も両方 API ベースでのテストの記述が可能ですが、PHPUnit のほうがより細かい制御が可能です。Postman は API 定義をまとめるのに特化して、PHPUnit でできる限りテストを書いていくのが 1 つの良い共存方法かと思います。
- 投稿日:2020-01-10T16:19:53+09:00
LaravelをPHPUnitとPostman でテストする手法まとめ(Passport にも対応)
Laravel で開発されているプロダクトにおいてテストコードを書く方法を、 PHPUnit および Postman を主体にしてまとめます。
テスト作成の背景
弊社(株式会社 NoSchool)では、Web フロントエンドに Nuxt を利用しており、また、iOS ネイティブアプリも開発しているため、Laravel を原則 API ベースで開発することが多いです。
API のテストを作成することは、バグのリスクを低減したり、アプリエンジニアとの仕様の共有のために必須となります。2020 年 1 月現在 NoSchool では
PHPUnitおよびPostmanを利用していますので、ドキュメントを兼ねてまとめました。この記事を読むとわかること
- PHPUnit を使って API 単位でのテストを書く方法
- PHPUnit を使って Class 単位での単体テストを書く方法
- Postman を使って API のレスポンスをテストする方法
- それぞれの比較と、実運用する際に共存する方法
環境
- Laravel 5.7(古い...)
- PHPUnit 7.0
- Mockery 1.2
PHPUnit
概要
PHPUnit は、PHP のコードベースでテストを記述できるフレームワークです。
PHPUnit でテストを書く目的
PHP のコードベースのため、データベースのサンプルデータを柔軟に生成し、テスト後に破棄すると言ったことが可能になります。そのため、細かいテストケースを実装するのに向いています。
書き方
ディレクトリ構造
弊社では
testsディレクトリ以下に下記のディレクトリ構造でテストを書いていますので、参考までに。
./Feature結合テスト(API テスト)を書きます。HTTP のエンドポイント単位でのテストを書きます。API を開発したときは原則必須で対応するテストコードを実装しましょう。
./Unit単体テストを書きます。クラス単位でのテストを書きますが、工数が多くかかること、設計変更に追従するコスト、および Final Class をモック化できない Mockery の制限などの理由から、あまり実装されることはありません。Rendering まで Laravel で行っている一部ページや、クラス単位でのテストをどうしても書きたい場合はこちらに書きます。
./Fixtureテスト用のデータを書きます。アプリ経由の課金で Apple から送信されるレシートなど、細かいデータを表現するときに使います。
./Lib複数のテストケースの基底となるクラスなどを格納します。各テストコードの基本
各テストコードは、それぞれのディレクトリ以下にドメインごとに namespace を切って作成してください。また、命名は ◯◯Test.php で作成してください。
ex. ./Feature/Chat/RoomTest.php
また、各テストケースは
Tests\TestCaseクラスを継承してください。final class RoomTest extends TestCase各テストケースの基本
各テストケースは、test◯◯ という名称の関数で作成します。
use Tests\TestCase; final class RoomTest extends TestCase { protected function setUp() // 後述 { parent::setUp(); } public function testSendMessage() { // hogehoge }API テスト
一つのテストケースの中で、下記のようなフェーズを順に実行します。
- テストデータの準備
- API リクエストの実行
- レスポンスのステータス、Body のテスト
- 必要に応じて、変化したデータベースの中身のテスト
テストデータの準備
一般に、各テストケースにおけるテストデータの準備は、
setUp()メソッドにて実装します。setUp()は各テストケースの実行直前に呼ばれます。反対に、終わったあとに呼ばれるのはtearDown()です。こちらはデータの後始末などに利用します。protected function setUp() { parent::setUp(); $this->room = factory(Room::class)->create([ 'user_id' => 1111 ]); }ここでのポイントは大きく 2 点あります。
まず、
parent::setUp()を実行することです。これを実行することで、Laravel アプリケーションを動かすための初期設定が終わります。実行しない場合、例えばconfig()を使った実装等が動かないので必ず実行してください。詳しい実装は
/Illuminate/Foundation/Testing/TestCase.phpに書いてあります。2 つめのポイントは
factory()グローバルヘルパを利用してテストデータを生成することです。ファクトリは
database/factories以下に作成します。詳しい書き方は実際のコードを読んだり、公式ドキュメントを参照してください。ここで重要なのは以下の 2 点です。
- factory を create するタイミングでテストデータを Array で渡すことで上書きできる
- factory 自体に名前をつけることができるため、例えば同じ User Model に対する factory でも違う初期データを持ったインスタンスを作り分けることができる
ファクトリ生成後は、実データがデータベースにインサートされているため、それがある前提で以降のテストを書くことができます。
【補足】テストデータの削除
テストデータを毎回テストのたびに生成していると、データベースがテストデータで溢れかえってしまいます。ここで、
DatabaseTransactionsを利用することで当該テストにて作成されたデータは全てテスト終了後に削除されます。use Illuminate\Foundation\Testing\DatabaseTransactions; class RoomTest extends TestCase { use DatabaseTransactions;API リクエストの実行
API リクエストは下記のように実行します。リクエストするメソッドには複数ありますが、個人的には
jsonメソッドが好きです。$response = $this->json( 'PUT', "/api/chat/{$this->room->id}", [ 'body' => 'message ], $headers )【補足】Laravel Passport を利用している場合
Passport を使っていると、都度都度 API リクエストに特定のヘッダを指定しなければなりません。具体的には、
Bearerトークンが必要です。これを解消するための手段を公開してくれている Web ページが有りましたので、ご参照ください。大まかに言えば、
setUp()を拡張してそこでUserに対応したアクセストークンを都度都度発行しています。https://www.whizz-tech.co.jp/1442/
こうすることで、
json()実行時の第 4 引数にヘッダーでBearerを渡せば認証済みユーザーとしてテストが実行できます。また、弊社のネイティブアプリ向けの API の場合、ログアウトユーザーでもクライアント単位でのグラントトークンを Bearer に載せることを必須としているのですが、そのトークンも
setUp()を使って実装できます。/oauth/tokenへ POST を飛ばしてトークンを都度発行すればいいです。補足: https://laravel.com/docs/5.7/passport#testing に書かれている
actingAsを使った実装は何故か動きませんでした【補足】Cookie 認証の API を利用している場合
Cookie 認証の API を利用していて、かつ認証済みのユーザーで API をリクエストしたいときは
actingAsメソッドを利用します。$response = $this->actingAs($user) ->json('PUT', '/api/room', [ ]);
factory(User::class)->create()でテストユーザーを作成後、そのユーザーで認証された状態で API を叩いた場合のテストなどをするときに有効です。レスポンスのステータス、Body のテスト
レスポンスのステータスは、下記のようにテストします。
$response->assertStatus(204);レスポンスの Body は、下記のようにテストします。
$response->assertJson([ 'message_id' => 100 ]);この場合のテストは一例で、具体的な値をテストできるものから、JSON の構造のみテストするものまで色々あります。
assertJsonはかなりゆるく、指定したキーを持ってさえいれば、余分なキーが含まれていてもエラーになりません(第 2 引数でより strict にできるようです)。ここでどうテストするかはテストケースで何をしたいかによるでしょう。ただ、特にネイティブアプリへのレスポンスなど、型にまで気を使わなければいけない場合において、下記のように構造とそれぞれの持っている型を見ることになると思います。
$response->assertJsonStructure([ 'id', 'user' => [ 'id', 'name', ], 'created_at', ]); $this->assertIsInt($response->json('id')); $this->assertIsInt($response->json('created_at')); $this->assertIsInt($response->json('user.id')); $this->assertIsString($response->json('user.name'));ネストしているキーに、Laravel にあるあるのドット記法でアクセスできるのは便利ですね。
詳しくは公式情報をどうぞ。
https://laravel.com/api/5.7/Illuminate/Foundation/Testing/TestResponse.html
https://phpunit.readthedocs.io/ja/latest/assertions.html#assertisint必要に応じて、変化したデータベースの中身のテスト
副作用のある API であれば、データベースに変更をもたらすでしょう。
データベースの中身をテストする場合は下記のように記述します。
$this->assertEquals( 3, Room::where('body', 'hogehoge')->count() );https://readouble.com/laravel/5.7/ja/database-testing.html を見る限り、
assertDatabaseHasというメソッドも用意されています。しかし、この場合はカウントで 2 つ以上のものがあること、というのは確認できませんので、用途に応じて使い分けてください。このように Eloquent Model を使ってクエリを発行して結果をチェックというのは少々我ながら筋が悪いため、より上手に書ける方法を知っている方は教えていただけると嬉しいです。
単体テスト
単体テストは、前述したとおり結構モック化を頑張らないと厳密な単体テストの実現が難しいです。
もし実装したい場合は、下記のようなコツを念頭に実装してみてください。
ここではユースケース層のテストを例に話します。NoSchool の場合、ユースケース層からは Repository の Interface を呼んでおり、
AppServiceProviderで実装クラスにbindingしているという特徴を持っているため、その前提での解説となります。DI を使っている場合は
resolveヘルパでインスタンスを取得DI している場合は普通にコンストラクタを書けないはずなので、
resolveヘルパでインスタンスを取得し、そこからメソッドを実行してテストします。$sendChatMessageUseCase = resolve(SendChatMessageUseCase::class);テストしたいクラス内で使っている外部クラスを Mock にしたい場合、DI するのが一番確実っぽい
以下の例はユースケースで使っている Repository をモック化した例です。
$repository = Mockery::mock(new MemberRepository($this->user)); $repository->shouldReceive('findMember')->once()->andReturn( $member ); $repository->shouldReceive('createMember')->once()->andReturn($member); $this->app->instance( 'App\Infrastructure\MemberRepository', $repository );final クラスをモック化するときは
alias:記法を使うfinal なクラスをモックにするときは
alias:を先頭につけて namespace を記述します。なんじゃこりゃ。まあ、そもそも final なクラスをモックにする意味あるのかって話なのですが。$requestStub = \Mockery::mock('alias:App\Helpers\HttpClient');といったややこしい仕様が散見されるので、特に
finalなクラスを使うのをやめるか、\Mockeryを使った単体テストはあまり書かないようにするか、というのが工数的なデメリットを鑑みると妥当かなというのが感想です。より良いライブラリや、Mockery でももっと楽に書けるぞ、という情報提供お待ちしております。テストの実行
テストの実行は、
./vendor/bin/phpunitコマンドで実行します。ディレクトリをオペランドに指定することも、ファイルを指定することも、何も指定せずに全てをテストすることも可能です。[mejileben]$ ./vendor/bin/phpunit ./tests/Feature/Room上記のコマンドでは、
Roomディレクトリ以下のテストケースを全て実行します。コマンドを覚えたくない方は、
composerやartisanコマンドとしてエイリアスを貼ってもいいでしょう。Postman
概要
Postman は、API のドキュメントを書いたり、テストケースを書いてまとめて複数の API をテストできるサービスです。
Swagger のテスト実行機能がついたようなイメージです。Swagger を使ったことがないのでイメージですが。
Postman を導入する目的
Postman を導入することで、API ドキュメントをネイティブアプリエンジニアに Web 経由で共有することができます。NoSchool では API 定義を JSON でエクスポートし、Git 管理しています。これを各自が Import することで API 定義を確認できます。有料プランに加入することで複数人が Web 上で共有を完結できるそうですが、ケチっているのでまだ無料プランです。
Postman 環境設定の概要
詳しくは割愛しますが、主に以下の点に注意が必要です。
- 秘匿情報は Environment として管理し、GitHub には上げない。または暗号化する
- ローカル環境で HTTPS を利用しているとき、設定画面から SSL のチェックを無効化しないとオレオレ証明書でエラーを吐いてしまう
Postman を使った API テストのしかた
Postman で API 定義を作成し、試しに「Send」ボタンを押すと、レスポンスが返ってきます。
ここまで終わったら、「Tests」タブを開き、下記のようにテストを書いていきます。
var jsonData = pm.response.json(); pm.test("id is number", function() { pm.expect(jsonData.id).to.be.a("number"); }); pm.test("room.id is 1540", function() { pm.expect(jsonData.room.id).to.eq(1540); });Postman のテストケースは
JavaScriptのライブラリであるChaiを利用しているため、JavaScript ベースで書くことができます。まず
json()メソッドでレスポンスの JSON を取得します。次に
pm.test()でテストケースを作成します。expect()でテストしたい値を JSON から抜き取り、その後は英文を書くかのようにメソッドチェーンで.to.be.aなどとつないでいき、最後に型名か具体的な値を書くことでテストが実行されます。ここでは
.toや.beをつけることは必須ではないのですが、そのほうが英文っぽく書けるので好ましいと思っています。それだけです。Rspec 等と思想が似ていると思います。Postman のテストでは具体的な値等までテストすることは難しいので、基本的にレスポンスの型のみテストすることが大半です。
Collection を作る
さて、API テストをいくつか作成すると、Collection というものにまとめることができます。Collection は、例えば Q&A サイトの場合、「質問したあとに回答がつき、それをベストアンサーにする」といった複数の連続した API のテストをすることが可能です。
テストを実行する
テストの実行は、クライアントアプリケーション上からもできるし、CLI から Node.js 等のコマンドベースでも実行可能です。
Postman を利用するメリットは?
API テストの表現力という意味では PHPUnit で頑張るほうが身の丈に合ってそうですが、例えば検証環境など、HTTP 越しでアクセスしたい場合は Postman でリクエストの向き先を Localhost から検証環境に向けて実行することができるなど、手軽さという意味では Postman に軍配があがるイメージです。
ただ、CI を組み込んだ場合などは結局 PHPUnit を検証環境で実行することなど簡単でしょうから、Postman でテストを頑張るメリットは薄れ、Swagger 等と同様で API の定義を複数の社員間で共有することがメイン目的となるでしょう。
個人的には Passport を使っている環境でも PHPUnit ベースでテストできることが衝撃でした。先の記事を公開した方に感謝です。
まとめ
PHPUnit も Postman も両方 API ベースでのテストの記述が可能ですが、PHPUnit のほうがより細かい制御が可能です。Postman は API 定義をまとめるのに特化して、PHPUnit でできる限りテストを書いていくのが 1 つの良い共存方法かと思います。
- 投稿日:2020-01-10T15:41:26+09:00
phpでテキストファイル内を検索し、表示する
検索はこんな簡単にできるんです!
php<style> #form{ position: fixed; top: 0; z-index: 50; } </style> <?php $SPLIT = "|-|-|-|-|-|"; if (isset($_POST["keyword"])){ $keyword = $_POST["keyword"]; ?> <div id="form"> <form method="POST" action=""> <input type="text" name="keyword" placeholder="ここに検索ワードを入力" value="<?php echo $keyword ?>"> <input type="submit" value="検索"> </div> </div> <?php $kekka = 0; $data = file_get_contents("list.txt"); $data = explode( "\n", $data ); $cnt = count( $data ); for( $i=0;$i<$cnt;$i++ ){ $array = explode($SPLIT, $data[$i]); $name = htmlspecialchars($array[0]); $url = htmlspecialchars($array[1]); $url = str_replace(PHP_EOL, "", $url); $data[$i] = $name; $echomoji = ""; if ($keyword == $data[$i]){ $kekka++; $echomoji = <<<EOF <br><a href="{$url}">{$data[$i]}</a> EOF; } elseif ($keyword == ""){ } else { if(strpos($data[$i], $keyword) !== false){ $kekka++; $echomoji = <<<EOF <br><a href="{$url}">{$data[$i]}</a> EOF; } if(strpos($keyword, $data[$i]) !== false){ $kekka++; $echomoji = <<<EOF <br><a href="{$url}">{$data[$i]}</a> EOF; } } echo $echomoji; } if ($kekka == 0){ echo "<br><br>{$keyword}に関する結果は見つかりませんでした。"; echo "<title>結果は見つかりませんでした。</title>"; } else { echo "<br><br>{$kekka}件の検索結果"; echo "<title>{$kekka}件の検索結果</title>"; } } else { ?> <div id="form"> <form method="POST" action=""> <input type="text" name="keyword" placeholder="ここに検索ワードを入力"> <input type="submit" value="検索"> </div> </div> <?php }こんな簡単に作れるとは思いませんでした。
まああくまで初心者が作ったプログラムなので参考程度にお願いします。
例を作ってみました。
- 投稿日:2020-01-10T14:22:04+09:00
WordPressでPHPデビューする際にお勧めの勉強法
前提
・HTML & CSS 習得済み
・PHP知らない出来るようになること
・phpファイルを扱えて自分でテーマが作れる
手順
1.
progateのPHPコースをクリアする2.
WordPressのphp本を読む
『WordPressユーザーのためのPHP入門 はじめから、ていねいに。』3.
自分でテーマを作る教本を参考に作ってみる理由
特に伝えたいことは、手順1のprogateのPHPコースが優秀だと言うこと。
手順2の本もかなり優良だが、progateの方が資料のわかりやすさ、実際に手を動かせる点で理解度が抜群に高い。
とりあえずこのあたりから慣れて行ったあとで、テーマ作成に入った方がいいと思う。
- 投稿日:2020-01-10T13:16:31+09:00
Dockerで始めるLaravel講座 - Docker編
はじめに
気づけばオリンピックイヤーである2020年がしれっと始まりましたが皆様いかがお過ごしでしょうか。
さて、今日は「Dockerで始めるLaravel講座」のDocker編です。
今日はサクサクっとどうやってDockerを使うのかを学んでいきましょう。
アジェンダ
今日はこんな順番でお話していきます。
- Dockerをはじめてみよう
- docker-composeでより本番に近づけてみよう
Dockerをはじめてみよう
Kitematicを入れてみよう
Dockerがまずピンときてないときは、まずはピンときてしまうことです。
そのためにDockerには Kitematic(カイトマティック) という素晴らしいGUIダッシュボードツールがあります。最新版はこちらからダウンロードできます。
https://github.com/docker/kitematic/releases
なんとかかんとかインストールして開いてみるとこんな画面になると思います。
せっかくなので、ここからApache(httpd)でも立ち上げてみましょう。
Search for Docker images ~と書かれている検索ボックスにhttpdと入れてみましょう。
するとこんな感じになると思います。
では、ここで左上の
official httpdと書かれているボックスの右下にあるCREATEをクリックしてみましょう。
すると何やらダウンロードを始めて、謎のコンソール画面が出てくると思います。
そう、何を隠そう、これで実はApacheのDockerコンテナが立ち上がっている状態なのです。
試しに右上のHomeSettingsと書かれているタブをSettingsに切り替えてから中のタブをHostname/Portsに切り替えてConfigure Portsのlocalhost:xxxxと書かれているところをクリックしてみましょう。
するとブラウザに
It works!と表示されているはずです。
おめでとう! Apacheが立ち上がっていますね!
せっかくApache立ち上がったのでApacheの中に入ってみましょうか。
Kitematicに戻ってみるとEXECって書いてあるところがあるのでポチーしてみましょう。
するとなにやら黒い画面が現れてきます。
そう、これでDockerで立ち上げた仮想環境の中に入っているのです。せっかくなので、入ってる気分を体験してみましょう。
こちらのコマンドを順に打ち込んでみてください。
$ apt update $ apt install -y vim $ cd htdocs $ vi index.html
すると、さっき表示していた
It Works!の文字を表示していたHTMLファイルが開けます。
こいつをちょちょいと弄ってみてさっきのブラウザをリロードしてみましょう。表示、変わりましたか?
ではもう用済みなので、このコンテナは消してやりましょう。
Kitematicのここをクリックしてみてください。
ローカルのファイルをDockerコンテナに反映させよう
コンテナでただ遊ぶ分にはさっきの方法でもいいのですが、ちゃんと開発で使おう!と思うといちいちコンテナの中に入ってvimでファイルを開いて編集……というやり方でやってたら全然効率がよくありません。
やっぱ「Dockerやめて素直にXAMPP使うぜ」になってしまいます。
じゃあどうしたらいいのか。
VirtualBoxとかVagrantで仮想環境を使った開発をしたことある人ならもうピンときていると思いますが「共有フォルダ」機能を使えばいいのです。
ちゃんとDockerにもありますよ!
ただこれ、Kitematicからは設定できないので、コマンドラインから実行する必要があります。
Kitematicの画面の左下に
DOCKER CLIと書かれたボタンが有るの分かりますか? そこをポチーしてみましょう。
実行する環境によって出てくるものが若干違うと思いますが、これまた黒い画面が現れてきます。
Windowsならコマンドプロンプトが、Macならターミナルの画面が出てきてると思います。
多分なんとなくわかったと思いますが、実はKitematicなど使わなくてもDockerは使えます。あ、知ってましたかね?
ではデスクトップに
docker-testというフォルダを作って中にindex.htmlを作ってみてください。
index.htmlの内容は何でもいいです。
HTMLの書き方がわからなければ下をコピペしてください。<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Test</title> </head> <body> やったねたえちゃん、家族が増えたよ </body> </html>
できたらターミナルの画面でなんとかかんとかデスクトップまでたどり着いてください。
そこで下記コマンドを入力してみてください。docker run -v $(pwd)/docker-test:/usr/local/apache2/htdocs -d -p 8080:80 httpd今度はブラウザで
http://localhost:8080を開いてみてください。
さっき作ったやつになりましたよね?
docker-composeでより本番に近づけてみよう
そもそもなんでDockerを使うのか
割と肝心の話をしてなかったような気がしますね。
当たり前なのですが、Web開発におけるローカル開発環境の手段はDockerだけではありません。
大まかには以下の3通りのどれかになるかと思います。
- ローカルのOSにApache/PHP/MySQLなどのミドルウェアを直接入れて開発する
- VirtualBoxなどのVM型仮想環境を使う
- Dockerなどのコンテナ型仮想環境を使う
それぞれにメリット・デメリットはあるのですが、大体のケースに置いてDockerはおすすめです。
かんたんにメリット・デメリットまとめるとこんな感じですかね。
方法 メリット デメリット 直ミドルウェア かんたん(何なら最初から入ってることも) 環境が汚れてしまうので複数案件まわせない。たくさん建てるのは無理 VM どこでも安定して動く。 おもい。たくさん立てるのが大変(できないことはない) コンテナ サクサク立てられる。一度にたくさん立てられる 一部できないことがある
つまり、たくさん立てないといけないときはほぼDocker一択という選択になります。
僕が初めてDockerを仕事で作ったときはこんな構成でした。
APIサーバーと管理画面サーバーが同じDBにアクセスする構成ですね。
こんな感じの構成をちゃちゃっと作るのにはDockerの一機能である
docker-composeというのが威力を発揮します。
いや、まあ、kubernetesとかもあるっちゃあるのですが、それはまたどこかでやりましょう。
ではさっき作ったtest-dockerフォルダの中にこんな内容の
docker-compose.ymlを作ってみてください。version: '2.1' services: mysql: image: mysql:5.6 volumes: - './mysql/var/log/mysql:/var/lib/mysql' ports: - '4306:3306' environment: MYSQL_ROOT_PASSWORD: rootPass11111111 MYSQL_DATABASE: laratest MYSQL_USER: larauser MYSQL_PASSWORD: larapass api: image: httpd links: - mysql ports: - '8081:80' volumes: - './api:/usr/local/apache2/htdocs' admin: image: httpd links: - mysql ports: - '8082:80' volumes: - './admin:/usr/local/apache2/htdocs'
できたらこんどは、apiとadminディレクトリを作ってそこにindex.htmlをおいてみましょう。
内容はちょこっとだけ変えたほうが良いですね。
では、docker-composeを起動してみましょう。
こちらのコマンドを打ち込んでみてください。
$ docker-compose up -dすると楽しいことが起きていると思います。
Kitematicを見てみると一気に3つコンテナが立ち上がってるのがわかると思います。
こうすることによってマルチコンテナ構成を簡単に作れるのです。
次回はこのDocker環境を使ってLaravelのアプリケーションを作っていきたいと思います。
おつかれさまでした!
- 投稿日:2020-01-10T13:09:08+09:00
さくらレンタルサーバーでWordPressのDBバックアップを支援するPHPコード(駆け出しのWEB担当者用)
さくらレンタルサーバーでWordPressバックアップを
支援するPHPコードです。駆け出しのWEB担当者用のプログラムです、動作環境はPHP5.6以上で
お願いします。バックアップする際にWARNINGが発生してしまうには
対応しておりません。メールが貯まりますが大丈夫ですよ、きっと。プログラムを読めば何を書いているのか、駆け出しのWEB担当者様も
わかるとは思いますが、変更してほしいのはIDとPASS部分です?。
ルートにWordPressをインストールしていない場合は $wploadfile の変更も
お願いします。ファイルをアップロードした階層に dbbackup名でフォルダを設置し
そのフォルダにベーシック認証をかけておいてください。
外部からダウンロードが容易に出来るので・・・。上記の設定が完了したらファイルをアップロードし
sakura-rental-wpdb-backupfull.phpにアクセスしてください。
ログイン後、管理画面より設定を行った後、さくらレンタルサーバーの
コントロールパネルよりCRONの設定を行ってください。ダウンロードはこちらから
https://www.zip358.com/tool/sakura-rental-wpdb-backupfull/sakura-rental-wpdb-backupfull.zip尚、CRON設定に関しては例を管理画面に書いていますので
そちらの参考に設定をお願いいたします、
またバックアップは1週間保持します。
月曜日~日曜日まで7ファイルのMYSQL、DBのバックアップファイルが
出来上がります。管理画面の曜日設定にチェックが入っていないものは
バックアップ致しません。※ソースコードは下記になります。
<?php /* URI: https://www.zip358.com/tool/sakura-rental-wpdb-backupfull/sakura-rental-wpdb-backupfull.zip Description: さくらレンタルサーバーのWordPress:DBを指定の曜日にフルバックする 時間帯はcronで指定(さくらレンタルサーバー管理画面より) Author: taoka */ $base_id ="id";//←任意のIDを設定してください $base_pass ="pass";//←任意のPASSを設定してください $wploadfile =__DIR__."/wp-load.php";//wordpressロードファイル場所を指定(現在の階層から呼び出し) ///////////////////////////////////////////// //プログラム開始 下記の基準は可変しないでください ///////////////////////////////////////////// if($argv[1]=="sakurabackup"){ $data = @file_get_contents("week.dat"); $sakura_backup = function($week,$wploadfile){ require_once ($wploadfile); if(is_array($week)){ $weekno = date("w"); $weekname = array( "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ); if($week[$weekno])exec("mysqldump -u ".DB_USER." -p".DB_PASSWORD." -h ".DB_HOST." ".DB_NAME." > ./dbbackup/wordpress_".DB_NAME."_".$weekname[$weekno]."full.sql"); } return ; }; $sakura_backup(explode(",",$data),$wploadfile); exit; } session_start(); $S_id = $_SESSION["sakura_backupid"]; $S_PASS = $_SESSION["sakura_backuppass"]; $data = @file_get_contents("week.dat"); $week = array( 1,//日曜日 1,//月曜日 1,//火曜日 1,//水曜日 1,//木曜日 1,//金曜日 1//土曜日 ); if($data){ $week = explode(",",$data); } if($_POST["week"]){ foreach($week as $key=>$val){ $week[$key] = (int)$_POST["week"][$key]; } } //html $css =' <style> html, form{ padding: 15px; } .col,.col-sm{ color: #FFF; background: #0b3258; border-bottom: solid 6px #6a99c7; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.25); border-radius: 9px; } </style> '; if($base_id == $S_id && $base_pass == $S_PASS){ if(is_array($week)){ file_put_contents("week.dat",implode(",",$week)); } html2($week,$css); }elseif($base_id == $_POST["id"] && $base_pass == $_POST["pass"]){ $_SESSION["sakura_backupid"] = $base_id; $_SESSION["sakura_backuppass"] = $base_pass; if(is_array($week)){ file_put_contents("week.dat",implode(",",$week)); } html2($week,$css); }else{ $err=""; if($_POST["id"] || $_POST["pass"]){ $err = "IDもしくはPASSが違います"; } html1($css,$err); } function html1($css,$err){ ?> <html> <head> <title>バックアップ管理ログイン画面</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <?=$css?> </head> <body> <div class="container"> <div class="row"> <div class="col col-sm"> <form method="POST" action=""> <h2>■バックアップ管理ログイン画面</h2> <hr> IDとパスワードを入力してください<br> <font style="color:brown"><?=$err?></font><br><br> ID<input id="my-input" class="form-control" type="text" name="id"> PASS<input id="my-input" class="form-control" type="password" name="pass"> <br> <button class="btn btn-primary" type="submit">ログインする</button> </form> </div> </div> </div> </body> </html> <?php } function html2($week,$css){ ?> <html> <head> <title>バックアップ管理設定画面</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <?=$css?> <script> $(function(){ $("[type='submit']").on("click",function(){ alert("設定しました"); }); }); </script> </head> <body> <div class="container"> <div class="row"> <div class="col col-sm"> <form method="POST" action=""> <h2>■バックアップ管理設定画面</h2> <hr> バックアップを行う曜日を設定してください。 <div class="form-check"> <input id="my-input1" class="form-check-input" type="checkbox" name="week[0]" value="1" <?=(int)$week[0]?"checked":""?>> <label for="my-input1" class="form-check-label">日曜日</label> </div> <div class="form-check"> <input id="my-input2" class="form-check-input" type="checkbox" name="week[1]" value="1" <?=(int)$week[1]?"checked":""?>> <label for="my-input2" class="form-check-label">月曜日</label> </div> <div class="form-check"> <input id="my-input3" class="form-check-input" type="checkbox" name="week[2]" value="1" <?=(int)$week[2]?"checked":""?>> <label for="my-input3" class="form-check-label">火曜日</label> </div> <div class="form-check"> <input id="my-input4" class="form-check-input" type="checkbox" name="week[3]" value="1" <?=(int)$week[3]?"checked":""?>> <label for="my-input4" class="form-check-label">水曜日</label> </div> <div class="form-check"> <input id="my-input5" class="form-check-input" type="checkbox" name="week[4]" value="1" <?=(int)$week[4]?"checked":""?>> <label for="my-input5" class="form-check-label">木曜日</label> </div> <div class="form-check"> <input id="my-input6" class="form-check-input" type="checkbox" name="week[5]" value="1" <?=(int)$week[5]?"checked":""?>> <label for="my-input6" class="form-check-label">金曜日</label> </div> <div class="form-check"> <input id="my-input7" class="form-check-input" type="checkbox" name="week[6]" value="1" <?=(int)$week[6]?"checked":""?>> <label for="my-input7" class="form-check-label">土曜日</label> </div> <br> <button class="btn btn-primary" type="submit">設定する</button> <br><br> <div class="form-group"> <label for="my-textarea">cronの実行コマンドには下記の内容を設定してください。</label> <textarea id="my-textarea" class="form-control" name="cron" cols="10"><?="cd ".__DIR__." ; /usr/local/bin/php sakura-rental-wpdb-backupfull.php 'sakurabackup'"?></textarea> </div> <br> cron 設定例<br> ※毎朝:3時に処理を走らせる場合、尚、曜日に関してはプログラムで処理しますので全チェックでお願いします。<br> <img src="https://zip358.com/tool/sakura-rental-wpdb-backupfull/2020-01-10_1117.png" class="img-fluid"><br><br> ■注意事項<br> ※sakura-rental-wpdb-backupfull.phpを置いてある同階層にdbbackupというフォルダを作成しベーシック認証の設定を行ってください。<br><br> </form> </div> </div> </div> </body> </html> <?php } ?>
- 投稿日:2020-01-10T10:56:13+09:00
脆弱性診断で指摘されがちな項目と対策方法をまとめる(CentOS+Apache+php)
前提
AWSでEC2インスタンスを立ち上げた後に、セキュリティ系の対策をしない状態で脆弱性診断をするという機会があったので、その場合に指摘される項目と対策をまとめました。
セキュリティ対策初心者なのでご指摘あればコメントをお願いします。想定される環境
- CentOS 7系
- Apache 2.4
- php 7.2
指摘事項
TRACEメソッドが有効になっている
/etc/httpd/cond/httpd/confに以下を追記する
/etc/httpd/cond/httpd/confTraceEnable offSSL/TLSに脆弱性が存在するプロトコルが使われている
/etc/httpd/conf.d/ssl.confに記載されているSSLProtocolにTLSv1.2を設定する
/etc/httpd/conf.d/ssl.conf#SSLProtocol all -SSLv3 //コメントアウト SSLProtocol -all +TLSv1.2 //TLSv1.2のみを許可あまりに古いOSやブラウザの場合はTLS1.2に対応していない可能性があるので注意が必要です。
apacheのバージョンが公開されている
/etc/httpd/conf/httpd.confのServerTokensをProdにする
/etc/httpd/conf/httpd.conf#ServerTokens OS //コメントアウト ServerTokens ProdSSH通信において非推奨の暗号アルゴリズムが使用されている
/etc/ssh/sshd_configの最後に追記する。
/etc/ssh/sshd_configCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr推奨の暗号化アルゴリズムはmozillaの推奨設定などで確認する。
phpのバージョンが公開されている
php.iniの場所を確認する
php --ini //Loaded Configuration File: /etc/php.iniphp.iniのexpose_phpをOffにする。(viなら/exposeで検索をかけるとはやい)
php.iniexpose_php=Offファイル一覧ページ(Index of)が公開されている
<Directory "/var/www/html">内のOptionsからIndexesを削除する。
.htaccess<Directory "/var/www/html"> ~ #Options Indexes FollowSymLinks //コメントアウト Options FollowSymLinks ~ </Directory>Apacheのテストページが表示されている
/etc/httpd/conf.d/welcome.confの<LocationMatch "^/+$">部分をコメントアウトする
/etc/httpd/conf.d/welcome.conf#全体をコメントアウト #<LocationMatch "^/+$"> # Options -Indexes # ErrorDocument 403 /.noindex.html #</LocationMatch>最後に
設定ファイルを書き換えた後は再読み込みする。
systemctl restart httpd // /etc/httpd/の中身を書き換えたときsystemctl reload sshd // /etc/ssh/の中身を書き換えたとき再読み込みを行ったときにエラーが出た場合はjournalctl -xeでエラー内容を確認するといいです。
journalctl -xe #いろいろ出てきますがSyntaxエラーなら以下のように書いています。 #1月 10 10:00:00 ~~~ httpd[1234]: AH00526: Syntax error on line 362 of /etc/httpd/conf/httpd.conf:参考









