- 投稿日:2022-02-28T22:06:03+09:00
【laravel】複数条件でvalidation
新たにルールを作成してvalidationを作成しよう 通常のvalidation やりたかったこと 新しいルールの作成 ・ルールの記載 ・エラーメッセージの作成 validationに追加 通常のvalidation public function rules() { return [ 'email' => 'required|unique:users,email', ]; } 上記例では単純に 1.メールアドレスが必須かどうか? 2.メールアドレスがusersテーブルですでに使用されているメールアドレスではないか? これで本当に単純なvalidationが完了です! やりたかったこと ここからが自分のやりたかったことです。 仮登録ユーザーはメールアドレスのユニークチェックを行わないようにしたかったのです。 機能上どうかとは思いますが。。。 その辺りは無視してください。 今回はあくまでも新しいルールを作成することのみ説明しています。 本登録状態のデータはroleを仮登録の1としておきます。 仮登録状態のデータはroleを仮登録の9としておきます。 usersテーブル name | email | role hoge | test@test.com | 9 このような場合にはユニークチェックを行わず登録ができるようにすると言うことです。 単純に追加するとこのようになります。 usersテーブル name | email | role hoge | test@test.com | 9 hoge | test@test.com | 9 そして本登録が完了したらroleの値を1に変更しユニークチェックを走らせます。 usersテーブル name | email | role hoge | test@test.com | 9 hoge | test@test.com | 1 この状態でtest@test.comを登録しようとしてもエラーになると言うことです。 新しいルールの作成 まずは $ php artisan make:rule NewRule Rule created successfully. 新たにApp\Rules以下にルールを作成するファイルが作成されます。 作成されたファイル NewRule.php <?php namespace App\Rules; use Illuminate\Contracts\Validation\Rule; class NewRule implements Rule { /** * Create a new rule instance. * * @return void */ public function __construct() { // } /** * Determine if the validation rule passes. * * @param string $attribute * @param mixed $value * @return bool */ public function passes($attribute, $value) { // } /** * Get the validation error message. * * @return string */ public function message() { return 'The validation error message.'; } } 新たにできたファイルの中身がこちらです。 ルールの記載 public function passes($attribute, $value) { $res = User::getCountEmailByEmail($value); if($res === 0){ return true; } return false; } それではコードの説明をしていきます。 passes($attribute, $value) $attribute:フィールド名が入ります。 ここではemailが入ってきます。 $value:値が入ります。 ここではtest@test.comが入ってきます。 $res = User::getCountEmailByEmail($value); User::getCountByEmail($value) Userモデルにて return User::where('email', $email)->where('role' ,'<>' ,'9')->get()->count(); roleが9以外でtest@test.comに一致するデータのレコード数を取得しています。 if($res === 0){ return true; } return false; こちらでレコード数が0ならtrue、それ以外ならfalse falseのときにエラーが発生します。 エラーメッセージの作成 public function message() { return 'すでに登録されているメールアドレスです。'; } これで上記にエラーが返されます。 validationに追加 public function rules() { return [ 'email' => 'required|unique:users,email,new NewRule', ]; } ,new NewRule を追加しています。 これで完了です。
- 投稿日:2022-02-28T18:47:11+09:00
【Laravel】「email」のバリデーションルールの注意
初めに Laravelではデフォルトで様々なバリデーションルールを準備してくれており、生PHPと比べてかなり簡単にバリデーションを設定することができます。 しかし、デフォルトのものには実は注意が必要と言う事をお話しして行きます。 その中で、今回は 『email』のバリデーションルールについて紹介します。 問題点 デフォルトでは、emailの形式ユーザー名(メールアカウント)@ドメイン名でなければバリデーションエラーを出力してくれます。 App\Http\Controllers\Auth\RegisterController.php return Validator::make( $data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:3', 'confirmed'] ] しかし.... 以下のような形式だとバリデーションエラーとはならず登録処理が実行されてしまいます。 test@example.com test@gmail.comああ これではドメインが存在しないアドレスが気軽に登録されてしまう。 それどころか、この状態でサービスをもしリリースしたらとんでもないことになる... emailのバリデーションの種類 公式ドキュメントによると、5種類あります。 rfc: RFCValidation strict: NoRFCWarningsValidation dns: DNSCheckValidation spoof: SpoofCheckValidation filter: FilterEmailValidation 詳細 バリデーションスタイル 内容 rfc RFCと呼ばれるインターネットの標準仕様に合っているかをチェックするバリデーション strict email:rfcをより厳格にしてもので「エラーだけでなく、警告があってもダメ」なバリデーション dns DNSにそのメールアドレスのドメインが存在するかをチェックするバリデーション spoof なりすましのメールアドレスは拒否するバリデーション filter PHP関数のfilter_var()を使ったメールアドレスのチェック バリデーションの書き方 問題となるメールアドレスを通さないだけであれば、dnsのみの設定でも対応はできますが、その他の対策ができていないため併用して書きます。 以下は、strict(RFCに違反するアドレスがはじかれる)、dns(ドメインが有効でないアドレスがはじかれる)、spoof(なりすましメールもはじかれる)の3つを適用します。 App\Http\Controllers\Auth\RegisterController.php return Validator::make( $data, [ 'name' => ['required', 'string', 'max:255'], //emailに新しいバリデーションを追加 'email' => ['required', 'string', 'email:strict,dns,spoof', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:3', 'confirmed'] ] これでemailのバリデーション対策ができます。 最後に Laravelの『email』のバリデーションルールに注意が必要というお話をしてきました! Laravelを触り始めて、色んな人のGitHubのコードを閲覧してきましたが、意外にもこのルールについて知らない人多いんではないでしょうか? 今回の内容だけでなく、便利なメソッドだからといって使うと思わぬ落とし穴があるものは他にもありそうです、、
- 投稿日:2022-02-28T18:25:33+09:00
SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: Name or service not known の対処法
エラー内容 ./vender/bin/sail artisan migrate を実行すると、なぜか SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for mysql failed: Name or service not known こんなエラーが出ちゃいました。 前提として、 このコマンドの前に、色々キャッシュクリアなどやっていたら バグって、ページが表示されなくなるエラーを起こしていました。 そこで、新たなlaravelを立ち上げてmigrateしようと思っていた時に このエラーが起きました。 原因 前状況を考えると、キャッシュクリアなどで データベースが接続されていないのかな?と思い ./vender/bin/sail mysql match-app1 とDBを確認しようとしました。 すると service "mysql" is not running container と、違うエラーが発生したので。これは データベースが接続できていないのかも。 と思い、色々dockerとDBを繋ぐ方法を実行してみたものの 上手くいかず。。。 結局これで解決した あまりにも解決できなかったので、病みそうになっていたところ 適当に docker-compose up -d と打ち込んでみました。 それまでは ./vender/bin/sail up -d でやっていたのですが。 そして、dockerを立ち上げてから ./vender/bin/sail artisan migrate を実行してみたら、なんと上手くいきました!! Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (61.42ms) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (43.90ms) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (45.03ms) Migrating: 2019_12_14_000001_create_personal_access_tokens_table Migrated: 2019_12_14_000001_create_personal_access_tokens_table (70.68ms) Migrating: 2022_02_28_155100_create_user_lists_table Migrated: 2022_02_28_155100_create_user_lists_table (24.82ms) 久しぶりに、この色取り取りコードが見れてよかったです。 踏み込んで考えてみる さて、このままよかったと終わってもいいのですが なぜ、docker-compose up -dだと上手くいって ./vender/bin/sailだと、エラーが起きてしまったのかについて考えてみます。 ここを理解するには、そもそも composeとvenderの違いについて知る必要がありそうです。 sailスクリプトは、docker-compose.ymlファイルで定義されたDockerコンテナを操作するための便利なメソッドをCLIで提供します。 公式によると、sailコマンドはymlファイル操作用ぽいですね。 すみません。ちょっと時間かかりそうなので また、追々理解していくとします! とりあえず、今は終わらせることに集中していきます。
- 投稿日:2022-02-28T17:01:45+09:00
[PHP]PHPUnitのassertSeeをリダイレクト先でも行いたい。
きっかけ laravelにてfeatureテストを記述していたときに、post処理後のリダイレクト先でフラッシュメッセージをassertSeeしたくなった。 assertSee について レスポンス内に指定の文字列があるかを確認してくれる。 基本的なアサートの一つ。 public function test_テキストチェック() { $response = $this->get('user'); // /user に "user一覧" と記述されていればテストが通る $response->assertStatus(200)->assertSee('user一覧'); } この他にも、文字列を順番に記述されているかを確認してくれる「assertSeeInOrder」など、便利なものがいくつか存在します。 詳しくは、下記をご覧ください。 問題 便利なassertSeeですが、下記のような記述ではリダイレクト先までは見てくれません。。 public function test_テキストチェック() { $response = $this->post('user', [ 'name' => 'hello', 'email' => 'world@example.com', ]); // redirect前のテキストを確認してしまうので、テストは通らない。 $response->assertStatus(302)->assertSee('user一覧'); } 解決策 2パターンの解決策があるみたいです。 パターン1 post後に再度getを行う。 この場合でも flash メッセージ は正しく確認できるみたいです。 public function test_テキストチェック() { $response = $this->post('user', [ 'name' => 'hello', 'email' => 'world@example.com', ]); $response->assertStatus(302); $this->get('user')->assertSee('user一覧'); } パターン2 followRedirectsを使用する。 こちらも問題なく flash メッセージの確認ができます。 public function test_テキストチェック() { $response = $this->post('user', [ 'name' => 'hello', 'email' => 'world@example.com', ]); $response->assertStatus(302); $this->followRedirects($response)->assertSee('user一覧'); } 参考
- 投稿日:2022-02-28T14:19:57+09:00
Laravel + GCS でstorage管理していたファイルをCGSで管理するようにした
はじめに この記事ではLaravelでファイルアップロードしたファイルをGCSで管理した時の備忘録です やり方 事前準備:GCPのプロジェクト・バケットの作成 以下の記事を参考にしました?♂️ .envに登録したバケット名を入力する .env GCS_BUCKET_NAME="hoge-bucket" .envの値をapp.phpから参照する src/laravel/config/app.php 'gcp_bucket_name' => env('GCS_BUCKET_NAME', 'hoge-bucket'), GCSを操作するパッケージをインストール composer require google/cloud-storage google/cloud-storageをインスタンス化する 事前準備で作成したバケットの認証キーをstorage直下のディレクトリに移動します Config('app.gcp_bucket_name');:app.phpからバケット名を参照します $url = file_get_contents():事前準備で作成したキーファイルを参照します new StorageClient([バケット名,サービスアカウントのキーファイル]): $client->bucket();:バケットを生成します src/laravel/app/Services/Utilities/UtilityService.php class UtilityService { public $bucket; public $bucketOptions; public function __construct() { $projectId = Config('app.gcp_bucket_name'); $url = file_get_contents(storage_path('app/local-service-account.json', true)); $client = new StorageClient([ 'projectId' => $projectId, 'keyFile' => json_decode($url, true) ]); $this->bucket = $client->bucket($projectId); $this->bucketOptions = [ 'resumable' => true, // アップロードを再開可能に行うか 'name' => '', 'metadata' => [ // 日本語化 'contentLanguage' => 'ja' ] ]; } /** * 仮登録GCS保存名を作成 * * @param UploadedFile $file * @param string $dirName * @param string $uniqueKey * @param array $options * @return array */ public function setBucketOptions(UploadedFile $file, string $dirName, string $uniqueKey, array $options): array { $extension = $file->extension(); $fileName = "{$uniqueKey}_{$dirName}.{$extension}"; // 保存ディレクトリ生成 $options['name'] = "tmp/images/{$uniqueKey}/{$dirName}/{$fileName}"; return $options; } } 送信時にGCSに一時保存する tmp/images/一意な値に保存するようにします src/laravel/app/Services/SkillSheet/SkillSheetService.php protected UtilityService $utilityService; public function __construct() { $this->utilityService = new UtilityService(); } /** * GCSへの仮保存 * * @param StoreSkillSheetRequest $request * @param string $uniqueKey * @return array */ public function tmpStorageToGcsBucket(StoreSkillSheetRequest $request, string $uniqueKey): array { // バケットを生成 $bucket = $this->utilityService->bucket; // 登録した保存名を保持していく $saveNames = []; // 写真・動画・その他画像を1件ずつ振り分けする foreach ($request->file() as $key => $files) { foreach ($files as $file) { $options = $this->utilityService->setBucketOptions( $file, $key, $uniqueKey, $this->utilityService->bucketOptions ); $saveNames[$key] = $options['name']; $bucket->upload( fopen($file, 'rb'), $options ); } } return $saveNames; } GCSに一時保存した画像を正式に保存する場所に移動します images/一意な値・ユーザーIDに保存するようにします セッションに一時保存したファイル名を保持している状態と仮定します /** * GCS保存 * * @param integer $id * @param SkillSheet $target * @param array $updSkillSheet * @return array */ public function storageToGcsBucket(int $id, SkillSheet $target, array $updSkillSheet): array { // セッションを破棄する $saveNames = Session('saveNames'); session()->forget('saveNames'); // バケットイニシャライズ $bucket = $this->utilityService->bucket; // 仮登録したデータをコピーして移動させてから削除する foreach ($saveNames as $key => $v) { // 仮登録したオブジェクトがバケットに存在する場合は移管する $targetObject = $bucket->object($v); $extension = pathinfo($v)['extension']; $name = "skill_sheet/{$id}/{$key}/{$id}_{$key}.{$extension}"; // オブジェクトが存在する場合は仮置きからコピーして移動する・仮置きは削除する if ($targetObject->exists()) { $targetObject->copy($bucket, ['name' => $name]); $targetObject->delete(); } // 登録データが既にある場合は差し替える(更新) if ($target->pr_image_path) { // 差し替え前のオブジェクトを取得 $beforeObject = $bucket->object($target->pr_image_path); // 差し替え前のオブジェクトがあれば削除 if ($beforeObject->exists()) { $beforeObject->delete(); } else { // 差し替え前のオブジェクトがないのにDBに登録されている場合はリセットする $updSkillSheet['pr_image_path'] = null; } } $updSkillSheet['pr_image_path'] = $name; } return $updSkillSheet; } 移動先にファイルの実体が移動されます
- 投稿日:2022-02-28T13:10:58+09:00
laravel学んで2か月で自サービスを開発した話 Part2
みなさん、こんにちは。 早速ですが自サービスの開発記録を残していこうと思います。 サービス作成の背景および要件定義はこちらへ https://qiita.com/k-hayack884/items/e571138e8bf7f35f7502 さすがにインストールやマイグレーション作成などの設定は省きます。 ログイン機能の作成 管理機能のライブラリはLaravel/ui,Breeze,Jetstreamなどがある。 ただし、Laravel/uiのcssはBootstrap,Breeze,Jetstreamはtailwindcssが初期設定されている模様 学習サイトや技術ブログなどの文献ではLaravel/uiを使っているものが多かったが、あえてBreeze,Jetstreamに行く。 理由は laravel8以降ではtailwindcssが推奨されていること。 bootstrapでは自由なデザインをするのが難しいこと モダンなtailwind cssを使うワイ、カッコいい(ここ重要) 2段階認証などのセキュリティの機能面を考慮すれば、Jetstreamなんだが、とても難しいご様子。 公式にも Laravelを初めて使用する場合は、Laravel Jetstreamへ進む前に、Laravel Breezeで勘所を掴むことをおすめします。 Laravel 8.x スターターキット そりゃ、公式が言っているんだからBreezeにすればいいやん! あと、フロントエンドも決めないといけないね!せっかく学習したんだしvue.jsをインストールするぞ! Laravel 8.x スターターキット Laravel Breezeでは、VueやReactを使ったInertia.jsのフロントエンド実装も提供しています。Inertiaスタックを使用するには、breeze:install Artisanコマンドを実行する際に、希望するスタックとしてvueまたはreactを指定します。 Inertia.js?? とりあえず、インストールしてみたところ public function index() { return Inertia::render('User/Index',['users' => User::all()]); } えっ、いろんなlaravelのコード見たんだが、こんなコード見たの初めてなんだが・・・ もちろん、最初の3日はInertia.jsを使って開発してみたんだけど・・・ さっぱりわからん・・・ このままだと、開発速度が遅れちゃうのでInertia.jsは断念します。 vue.jsは直接インストールして使います。 マルチログイン機能の準備 breezeをインストールすれば、 このようにログインや新規登録機能、パスワードリセット機能のコントローラーやviewが自動生成される しかし、このままでは単一ログインはできるけれど、管理者とユーザーを分けてそれぞれの役割ごとにページを表示する必要がある 管理者用のテーブル、モデルの作成 まずは管理者のテーブルが必要なのでササっと作成 2022_01_14_153851_create_admins_table.php public function up() { Schema::create('admins', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } 最初からあるcreate_admins_table.phpをコピペして終了 今度はModelにAdmin.phpを作成 これも最初からあるUser.phpをコピーして終了 ※注意 php artisan make:Model で直接作ってもいいけれど、Authクラスを継承していないので、ちゃんと継承させる Admin.php class Admin extends Authenticatable //ModelからAuthenticatableに変更 おとなしくUsep.phpをコピペしたほうが確実 あとはhttp/ControllersにUserフォルダ、Adminフォルダを作成し、Authフォルダをそれぞれに突っ込む 同じく、viewsフォルダにuser,adminのフォルダを作り、Authフォルダをそれぞれに突っ込む Userフォルダ、Adminフォルダを作成し、Authフォルダをそれどれに突っ込む これで、マルチログインの準備はできた。 次はマルチログインの設定に行こうと思う。
- 投稿日:2022-02-28T08:50:46+09:00
SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost'で詰まったときの解消法
環境 MacBook Air (M1, 2020) Laravel Installer 4.2.9 エラー内容 SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: YES) (SQL: select * from information_schema.tables where table_schema = laravel_umarche and table_name = migrations and table_type = 'BASE TABLE') SQLSTATE[HY000] [1045] とは 「.envファイルに記載されているデータベース名やユーザー名、パスワードが間違っている。」ということ。 .envを修正 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=データベース名 DB_USERNAME=root DB_PASSWORD=root DB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sock これが間違っていると違うエラーが出ます。 phpMyAdminでユーザを追加した際は、適当なDB_USERNAME・DB_PASSWORDを設定してください。 これでもうまくいかない場合 .envもMAMPもphpMyAdminを見ても合っている・絶対間違ってないと思ったらこちらをターミナルで打ってください。 php artisan config:cache 投稿主もこのコマンドを打って、.env見直してを3時間やり続けたらできました。 終わりに こんなエラーで数時間も悩まされるとは思いませんでしたが、無事解決できました。 もし同じエラーにあった人がいたら、ぜひ参考にしてください
- 投稿日:2022-02-28T07:24:26+09:00
LaravelでCSVファイルのダウンロード機能を実装する
Laravelでの開発中、DBのデータが記載されたCSVファイルのダウンロード機能を実装する必要があり学習したのでまとめる。 ただし最後の1行だけ理解が及んでおらずのため悪しからず。。。 そもそもCSVとは? 「Comma Separated Value」の略。 直訳すると、カンマで区切った値となる。 散々CSVファイルを見てきたがカンマなど見たことがない、、、 どこがComma Separated Valueなんだと思ってしまった。 まず大前提として自分の認識が間違っていたのが、CSVとExcelはほぼ同じだと思っていたことだ。 というかむしろ何が違うんだくらいの認識だった。 ただこれこそが最大の勘違いで、正確には普段目にしているCSVファイルはExcelで開かれていたため全く同じに見えていたということだ。 となるとCSVファイルの実体は何なのか。 試しにCSVファイルをエディターに投げてみると以下のように表示される。 id,name\n 1,"taro"\n 2,"mike"\n 3,"john"\n まさにカンマで区切られた値、すなわちComma Separated Valueなのである。 WindowsではCSVファイルは既定でExcelに関連付けられているので、CSVファイルをダブルクリックするとExcelが起動する。 そのためCSV=Excelのように思えてしまうのだ。 CSVダウンロード実装 ここから本題に入っていく。 大まかな流れとしては以下となる。 ①書き込み用のファイルを開く ②ファイルにデータを書き込む ③書き込んだデータを文字列に変換・エンコードする ④ファイルを閉じる ⑤ファイルをCSVファイルに変換する ①書き込み用のファイルを開く まずは空のファイルを作成し、データを書き込めるよう開いておく必要がある。 その際に使うのがfopen関数である。 fopen関数 ファイルまたはURLをオープンする関数。 あるファイルの情報を新しいファイルに書き込む、DBのデータを取得してファイルに書き込むといった際、いずれもどのファイルに何をしたいのかを示す必要がある。 そこでまずはfopen関数を使って対象のファイルとアクション(書き込み用、読み込み用、追記用)を指定してファイルを準備する。 ※書き込みと追記の違いは、ファイルの先頭から書き込むか終端から書き込むか 形式としては以下のような形となり、第一引数に開くファイル名、第二引数に開くモードを指定する。 $stream = fopen('php://temp', 'w'); // php://tempについては後述 モードについては以下参照。 そして上記の場合、変数$streamにはファイルポインタというものが格納される。 ファイルポインタ テキストエディタのカーソルのようなもので、ファイルに対するアクションをどこから始めるのかを指定したもの。 これを動かして処理の開始地点を変更することもできる。 CSVファイルを作成する際にどのファイルを使用するか 上記の例でphp://tempという見慣れないファイルを使用しているがこれは何なのか。 php://memory および php://temp は読み書き可能なストリームで、一時データをファイルのように保存できるラッパーです。 両者の唯一の違いは、php://memory が常にデータをメモリに格納するのに対して php://temp は定義済みの上限 (デフォルトは 2 MB) に達するとテンポラリファイルを使うという点です。 https://www.php.net/manual/ja/wrappers.php.php とある。 極論、CSVファイルがダウンロードされるたびに新しいファイルを作成することもできるが、毎回新しいファイルが作成されてしまうのは得策とは思えない。 そこで、ストリームに一時的に保存して対応しているのである。 ストリーム プログラミングの分野では、データの入出力全般を扱う抽象的なオブジェクトやデータ型を意味する場合が多い。データが出入りする何らかの対象(メモリ領域やファイル、ネットワークなど)をプログラム中で扱えるように抽象化したもので、接続や切断、書き込みや読み込みなどを簡易な操作で行うことができる。 https://e-words.jp/w/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0.html いまいちピンとこないとは思うが、要は流動的なデータを一時的に留めておく場所だと思って良いかと思う。 テンポラリファイルも同じようなもので、一時的に作られるファイルのことを指す。 tempファイル、tmpファイルのように記載されることが多い。 ②ファイルにデータを書き込む fputcsv関数を使用する。 第一引数にfopen関数で開いたファイルポインタ、第二引数に書き込みたい内容を指定する。 記述例としては以下。 $users = User::all(); $stream = fopen('php://temp', 'w'); //書き込みモードで開く $arr = array('id', 'name'); //CSVファイルのカラム(列)名の指定 fputcsv($stream, $arr); //1行目にカラム(列)名のみを書き込む(繰り返し処理には入れない) foreach ($users as $user) { $arrInfo = array( 'id' => $user->id, 'name' => $user->name ); fputcsv($stream, $arrInfo); //DBの値を繰り返し書き込む } 上記の場合、php://tempの1行目にまずCSVファイルのカラム名を記載し、2行目以降にDBの内容を書き込んでいる。 また、ここで条件分岐させてデータがない場合は「該当データがありません。」などの記載をするのも一つのやり方だ。 ③書き込んだデータを文字列に変換・エンコードする ストリームに書き込んだデータは文字列ではないので、文字列に変換してあげる必要がある。 実際、fputcsv関数で書き込まれたストリームをvar_dumpで確認してみると、resource(358) of type (stream)と表示される。 これはリソース型というらしい。 リソース型 は PHPの外部世界 とやり取りを行うデータが格納されている型 リソース はデータベースや画像ファイルなどの 外部情報 リソース型 の種類は 特殊型 リソース型 のチェックは is_resource() 関数 で行う https://wepicks.net/resourcetype/ このままではCSVファイルに変換ができないため、文字列に変換してあげる。 ここで使用するのがstream_get_contents関数とmb_convert_encoding関数。 fputcsv関数で書き込まれたストリームをstream_get_contents関数で取得し文字列に変換後、mb_convert_encoding関数でエンコードするという流れになる。 stream_get_contents stream_get_contents関数とは、 file_get_contents() と似ていますが、 stream_get_contents() は既にオープンしている ストリームリソースに対して操作を行います。そして、指定した offset から始まる最大 maxlength バイトのデータを取得して文字列に保存します。 https://www.php.net/manual/ja/function.stream-get-contents.php とのこと。 php://tempは前段で記載の通りストリームであるためこの関数を使用する。 書き方と具体的な説明は以下。 stream_get_contents(resource $handle, int $maxlength = -1, int $offset = -1) 第一引数:fopen関数で開いたストリーム(これさえ指定すれば第2・第3引数を指定せず使用可能) 第二引数:読み込むバイト数(指定しなければデフォルト値の-1が適用され、残りのバッファ全てを読み込む) 第三引数:読み込みを開始する前に移動する位置。負の数を指定した場合は移動が発生せず、現在位置から読み込みを開始する。 第一引数の説明は良いとして、第二引数と第三引数について見ていく。 第二引数の読み込むバイト数を指定するタイミングがあまり想像できなかったので、特に指定しなくても問題ないと思う。 では第三引数について。 今回であればストリームに書き込んだデータのどこからを文字列に変換したいかということだ。 ここで一つ考えなくてはならないのが、fputcsv関数で書き込まれた後のファイルポインタがどこになるのかということだ。 結論としては、ファイルの終端となる。 そのため、第三引数を指定しないと以降の処理がファイルの終端から進んでしまう。 そして終端以降は当然何も書き込まれていないため何も起こらない。 そこで、stream_get_contents関数の第三引数を指定し、ファイルポインタを先頭(0地点)に戻す必要がある。 以下のようにしてやる。 $csv = stream_get_contents($stream, -1, 0); //第三引数を指定するために第二引数も合わせて指定 また、別のやり方としてrewind関数を使用する方法もある。 rewind関数はファイルポインタを先頭に戻してくれるため、stream_get_contents関数の前に記載することで、ストリームの最初から最後までを取得することができる。 以下のようにするだけ。 rewind($stream); $csv = stream_get_contents($stream); //ファイルポインタの変更が必要なくなったので第一引数のみで問題なし エンコーディング ここまででストリームの取得ができたので、後はmb_convert_encoding関数でエンコーディングしてあげるのみ。 今回の実装では以下のようにした。 $csv = mb_convert_encoding($csv, 'sjis-win', 'UTF-8'); 第一引数が対象文字列、第二引数が変更後の文字エンコード、第三引数が変更前の文字エンコードだ。 ここでは、$csvの文字エンコードをUTF-8からsjis-winに変更している。 CSVファイルをExcelが開く際には強制的にsjis-winで開くのだが、PHPスクリプトはUTF-8で書かれているため文字化けしてしまう。 これを防ぐためにエンコーディングをしている。 ④ファイルを閉じる ここまでの処理が完了すれば追加でファイルに手を加えることはないため、fclose関数を使用してファイルを閉じる。 引数にストリームを指定するのみだ。 fclose($stream); ⑤ファイルをCSVに変換する 最後にファイルをCSVに変換しブラウザに返して終了となる。 まずはCSVに変換するためにヘッダーに持たせたい情報を指定する。 $headers = array( 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename='test.csv' ); Content-Type Content-Typeとは、そのファイルがどんな種類のものなのかを示すもの。 「タイプ/サブタイプ」という記載の仕方で、「タイプ」でデータの種類(テキスト、画像、動画など)を定義し、「サブタイプ」で具体的なデータ形式を定義している。 text/plain, text/html, application/jsonのようになる。 Content-Disposition Content-Dispositionは、コンテンツをwebページの一部として表示するかダウンロードするかを指定する。 inlineを指定すればwebページとして、attachmentを指定すればダウンロードファイルとして表示する。 また、filenameでファイルの初期名を指定できる。 ブラウザにCSVファイルを返す ここまでできたら、最後はブラウザにCSVファイルを返すのみ。 以下のようにする。 return Response::make($csv, 200, $headers); //$csvは最後にmb_convert_encoding関数でエンコードした変数 この最後の1行が理解できておらず、、、 他にも色々書き方があるみたいだが、とにかくこのようにすると生成したCSVファイルをダウンロードできる。 改めてコード全体を見ると以下のようになる。 $users = User::all(); $stream = fopen('php://temp', 'w'); //ストリームを書き込みモードで開く $arr = array('id', 'name'); //CSVファイルのカラム(列)名の指定 fputcsv($stream, $arr); //1行目にカラム(列)名のみを書き込む(繰り返し処理には入れない) foreach ($users as $user) { $arrInfo = array( 'id' => $user->id, 'name' => $user->name ); fputcsv($stream, $arrInfo); //DBの値を繰り返し書き込む } rewind($stream); //ファイルポインタを先頭に戻す $csv = stream_get_contents($stream); //ストリームを変数に格納 $csv = mb_convert_encoding($csv, 'sjis-win', 'UTF-8'); //文字コードを変換 fclose($stream); //ストリームを閉じる $headers = array( //ヘッダー情報を指定する 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename='test.csv' ); return Response::make($csv, 200, $headers); //ファイルをダウンロードする 以上で完了となる。 今回はCSVファイルに絞って記載したが、他のファイルでも同じように実装することができる。 実際、icsファイルのダウンロード機能を実装した際にも同じ要領で進めることができた。 次回はicsファイルやカレンダー追加機能の実装について書きたいと思う。
- 投稿日:2022-02-28T01:34:02+09:00
Next.jsとLaravelで作ったアプリをAWSにデプロイ出来なかった話
はじめに 今回のお話はAWSにアプリをデプロイ出来なかった話です。 あんまり技術的な学びはないので、真面目な気持ちでクリックしてしまったあなたはブラウザバックしてください。 逆に仕事が捗らなくて技術記事読むフリして休憩したいそんなあなたにはピッタリの内容になっております。 どんなアプリを作ったか 今回の本筋ではないのですが、デプロイ出来なかったのでここでアプリを供養しておきます。 簡単なTodoアプリで、以前Nuxt.jsとFirebaseで作ったものをNext.jsとLaravelで作り直したという感じです。 未経験からエンジニア転職する際に作ったポートフォリオだったので、思い入れのあるアプリです。 CSSフレームワークとしてChakraUIを使いました。 class名でスタイリングしていくわけではなく、スタイリングされたcomponentを使う形式です。 型定義バッチリされてるし、propsで値渡せばそこそこ柔軟にスタイル変えられるし最高でした。 リポジトリはこちら ローカルでの立ち上げ方もREADME付けているので、是非触ってもらえたら嬉しいです。 やろうとしたこと Next.jsをVercelにデプロイ。 LaravelとMySQLをAWSのEC2にデプロイ。 初めてAWSを触るので、学習コストを少しでも下げるためにRDSは使わない。 出来たことと挫折したこと Next.jsをVercelにデプロイ。 簡単とは聞いていたが、ビックリするくらい簡単だった。何回かクリックしたらデプロイが終わった。 masterブランチにpushする度に自動デプロイされて、便利なことこの上なかった。 LaravelとMySQLをAWSのEC2にデプロイ。 タイトル詐欺と言われそうだが、実はEC2にLaravelとMySQLをデプロイ自体は出来た。 もう少し言うとローカルのフロントからEC2上のLaravelのAPIを叩くところまでは出来た。 CORSエラーに苦しんだが、nginxでリクエストにheaderを付けることで解消出来た。 ELBを使っていなかったので、EC2はhttpのままで、Vercelはhttpsだったので、プロトコルが違って通信出来なかった。今回ここで挫折。 そもそもやらなかった(が今後やりたい)こと Next.js getStaticPropsとgetServerPropsは使わずじまいだった。useEffectで誤魔化してました。 初めてのNext.jsだったので、Recoilは使わず。useContextで行きました。 パフォーマンスの意識。とにかく動けばOKの精神で作ってしまった。 Laravel ステートフルにしたかったが、Laravelのsessionの使い方がよく分からず見送り。 以前の記事でも書きましたが、モダンフロントとLaravelの組み合わせの記事が本当に少なくて、Blade前提のものばかりで参考にならない・・・。 CORSとCSRFはどちらもちゃんと対策できず、セキュリティガバガバアプリになった。 今後勉強しないといけないなと思ったこと Linux 参考文献のコマンドをコピペで打ってデプロイしただけなので、あんまりコマンドの意味も分からずやっていた。 普段意識しないようなパーミッションの概念が全然分かっていなかった。 nginx(別にapacheでも良いけど) 前述の通りCORSエラーにめっちゃ苦しめられた。サーバーアプリはちゃんと勉強しておかないとインフラの理解なんて出来る訳がないなと痛感した。 AWS 当たり前だけど、AWS自体も全然分かっていなかった。一度に色んなことをやろうとしたのが敗因。 基本的なVPC、EC2、Route53、ELBあたりを一つずつちゃんと学習するべきだった。 さいごに 暗い記事になってしまいましたが、初めてNext.js、AWSを触れたので学びは多かったです。 休憩ということで、次はバックエンドとインフラをAmplifyとかFirebase使って、フロントに集中したアプリを作りたい。 参考文献 【AWS入門】EC2+NGINX+MySQL環境へLaravelをデプロイする手順 nginxのadd_headerでレスポンスヘッダが付かない時の対処法 【初心者向け】AWSのサービスを使ってWebサーバーをHTTPS化する