20220228のPHPに関する記事は7件です。

【Declined】PHP8に入ることのできなかった機能たち

PHP8では大量のRFCが受理され、多くの文法の拡充が行われました。 光あるなら陰もまたあり、すなわち華々しく活躍するRFCの傍らには、却下され消えゆくRFCもまた存在するということです。 ということでPHP8に入り込むことを目指したものの却下されたRFCを見てみることにしましょう。 採用されたRFCと却下されたRFCを見比べることで、PHPの今後の方向性とかがわかるかもしれませんよ。 Declined RFC Dump results of expressions in php -a 賛成11、反対6で却下。 php -aのインタラクティブシェルは寡黙です。 $ php -a Interactive shell php > 1+1; php > $a = 1; php > $a; php > var_dump($a); int(1) 代入などの操作を行っても何も言いません。 これがたとえばJavaScriptコンソールであれば毎回最後の演算結果を出力します。 > 1+1; << 2 > a = 1; << 1 > console.log(a); 1 そんなわけでPHPコンソールも饒舌に、デフォルトで最後の演算結果を出力しよう、という提案。 また出力を制御する関数readline_interactive_shell_result_function()というちょっとどうかしてる名前の関数も導入しようという提案でしたが、却下されました。 おそらく演算結果の出力だけだったら通ってた気がしないでもない。 StackFrame class 賛成14、反対14で却下。 debug_backtraceはトレースの返り値が配列です。 そこでStackFrameクラスのインスタンスにしようという提案。 これによってメモリ省力化につながります。 RFCでは、巨大なテストスクリプトでメモリ消費量が390MBから192MBへ削減できたということでした。 しかし投票時点でもプルリクにメモリリークのバグがあり、テストスクリプトが公開されていなかったので検証もできないということで却下になりました。 そこらへんきちんと準備していたらおそらく通っていただろうに、惜しいことをしたものです。 Make constructors and destructors return void 賛成34、反対22で却下。 コンストラクタ、デストラクタの返り値にvoidを強制する提案。 マニュアルではコンストラクタ、デストラクタの返り値の型はvoidとなっています。 しかし、これを無視して値を返しても何も言われないし、むしろvoidを付けると逆に怒られます。 class HOGE{ public function __construct(){ return 1; // OK } public function __destruct():void{ // Fatal error: Method HOGE::__destruct() cannot declare a return type } } そこでマニュアルと挙動を合わせ、コンストラクタ、デストラクタは値を返さないようにし、voidを付けられるようにしようというものです。 正しくするといえばそうなのですが、これまでvoid以外を返していたメソッドが動かなくなるという非互換があったため反対が多くなったようです。 Rename T_PAAMAYIM_NEKUDOTAYIM to T_DOUBLE_COLON 賛成44、反対30で却下。 ::はT_PAAMAYIM_NEKUDOTAYIMというよくわからない名前で知られています。 意味がわからないのでT_DOUBLE_COLONにしようという提案。 これ10年前からずっと言われているのですが、T_PAAMAYIM_NEKUDOTAYIMが導入されたのは20年以上前なので歴史の差では勝てません。 実はT_DOUBLE_COLONも大昔から定義されているのですが、これがどうなっているのかはよくわかりません。 Opcache optimization without any caching 賛成10、反対13で却下。 Opcacheの機能は最適化とキャッシュです。 これはセットになっていて、キャッシュせずに最適化だけ使用することはできません。 ということで最適化だけ単体で使えるようにしようという提案。 しかし、これを実行するにはoptimizerをopcacheからPHPコアに持っていかないといけないみたいな話になり、話が広がりすぎてしまいました。 Match expression 賛成6、反対28で却下。 match式の提案です。 $expressionResult = match ($condition) { 1, 2 => foo(), 3, 4 => bar(), default => baz(), }; このRFCは当初からブロック構文を含んでいたのですが、メモリリークバグなどの問題があり紆余曲折の末に却下されました。 その後、ブロック構文を外すなど機能を制限してブラッシュアップされたMatch expression v2として提案・受理され、PHP8.0から使えるようになりました。 Type casting in array destructuring expressions 賛成6、反対26で却下。 型キャストの新文法の提案。 // これを [$now, $future] = explode(',', '2020,2021'); $now = (int) $now; $future = (int) $future; // こうしたい [(int) $now, (int) $future] = explode(',', '2020,2021'); // これを $users = [ ['id'=>'1', 'name'=>'ore'], ['id'=>'2', 'name'=>'omae'] ]; foreach($users as ['id'=>$id, 'name'=>$name]){ $id = (int)$id; echo "{$name}のIDは{$id}です"; } // こうしたい foreach($users as ['id'=>(int)$id, 'name'=>$name]){ echo "{$name}のIDは{$id}です"; } PHPの場合、CSVやDB等から取ってきた値は全てstring型なので、取り出す時点でint型にしておきたいなどの需要はあるでしょう。 そんなときにあれば便利かもしれませんが、それだけのために新構文を追加する説得力が出せずに却下されました。 Compact Object Property Assignment 賛成2、反対48で却下。 オブジェクトのプロパティに一括で値を投入したいという提案。 $foo->[ a = 1, b = myfunc(), c = $foo->bar(), ]; 驚きの反対率で却下されました。 $foo->a = 1; $foo->b = myfunc(); $foo->c = $foo->bar(); 正直、現状の書き方と比べて長さもあまり変わらないですね。 Userspace operator overloading 賛成38、反対28で却下。 ユーザランドでの演算子オーバーロードの提案。 ここで言う演算子オーバーロードは、PHPにおけるオーバーロードではなく一般的に言うところの演算子オーバーロードです。 PHPでは演算子を再定義することはできません。 +とか<とかの定義は不変ということです。 またPHP8以降、オブジェクトへの算術演算はTypeErrorになります。 しかしGMPやDateTimeなど、独自の演算子オーバーロードが施されているオブジェクトも存在します。 ならばユーザランドでも同じようにオーバーロードできてもいいじゃない。 class Vector3(){ public static function __add($lhs, $rhs) { // 演算子'+'の定義 } public static function __mul($lhs, $rhs): ?Vector3 { // 演算子'*'の定義 } } $a = new Vector3(); $b = new Vector3(); $x = $a + $b; $y = 3 * $b; と、このように各演算子に対してマジックメソッドで独自の処理を実装できるという機能です。 しかし-は実装できるが+が実装できない例など様々な問題が広がりまくった挙句却下となりました。 User Defined Operator Overloads 賛成21、反対24で却下。 さきほどのUserspace operator overloadingのバージョンアップ版です。 といってもオーサーは全く別人ですが。 class Collection { // これはただの追加 function __add(Collection $other, OperandPosition $operandPos) {} // 演算子'+'の実装 operator +(Collection $other, OperandPosition $operandPos) {} } 新しいキーワードoperatorを追加し、演算子を直接定義できるようになります。 わしゃこのRFCに100時間以上注ぎ込んでるんじゃよとのことでしたが、長い長い議論の果てに却下となりました。 Write-Once Properties 賛成23、反対23で却下。 一回だけ書き込みが可能で、書き込んだらその後は変更できなくなるプロパティです。 class Foo { <keyword> public int $a; <keyword> public array $b; <keyword> public object $c; public function __construct() { $this->a = 1; $this->b = ["foo"]; } } $foo = new Foo(); $foo->a = 2; // EXCEPTION: 書き込み済なのでNG var_dump($foo->b); // publicなら読み込みはOK。 $foo->c = new stdClass(); // 一回目なので書き込める。 PHP8.1において、ほぼ同じ内容で受理されました。 違いはクラス外から書き込み可能か否かくらいで、どうして明暗が分かれたのかはよくわかりません。 declare(function_and_const_lookup='global') 賛成2、反対35で却下。 PHPの関数呼び出しは、 ・まず現在の名前空間に関数があるか探す ・なかったらグローバル名前空間の関数を探す と2段階になっていて効率がよくありません。 そこで常時グローバル名前空間を探すようにするディレクティブdeclare(function_and_const_lookup='global')を導入しようという提案。 declare( strict_types=1, function_and_const_lookup='global' ); namespace MyNS; use function OtherNS\my_function; use const OtherNS\OTHER_CONST; if (version_compare(PHP_VERSION, '8.0.5') >= 0) { // このversion_compareはグローバル関数であることが保証される } 当初はuse function *;って書きたかったけど書けなかったからどうにかしたいって話から始まったようです。 Deprecate Backtick Operator (V2) 賛成11、反対26で却下。 バッククォートをE_DEPRECATEDにしようという提案。 バッククォートはシェル実行するという危険な機能にもかかわらず、わかりやすい目印がなく引用符"・'と紛れやすいため安全ではありません。 そもそもshell_execと同一であるため、こちらに揃えた方がよいでしょう。 個人的にはさっさと消してしまっていいと思うのですが、しかしこのRFCが提出されたのはDeprecate short open tags, againと同時期です。 すなわち銀河に平和がもたらされた直後であり、やはりこちらもZeevが反対を表明しています。 このあたりの関係もあり、却下されたのだと思われます。 Object Initializer 賛成3、反対26で却下。 新しいオブジェクトイニシャライザ構文の提案。 // これを $customer = new Customer(); $customer->id = 123; $customer->name = "John Doe"; // こうしたい $customer = new Customer { id = 123, name = "John Doe", }; この例ではファクトリーメソッドを使うべきだろと突っ込まれていました。 まあそりゃそうだ。 Add array_group function 賛成0、反対19で却下。 関数array_groupがほしい、という提案。 $array = [ ['id' => 1, 'name' => 'hassan'], ['id' => 2, 'name' => 'sara'], ['id' => 3, 'name' => 'selim'], ['id' => 4, 'name' => 'chris'], ['id' => 5, 'name' => 'sara'], ]; var_dump(array_group($array, null, 'name')); // ↓こんなふうになる [ 'hassan' => [ ['id' => 1, 'name' => 'hassan'] ], 'sara' => [ ['id' => 2, 'name' => 'sara'], ['id' => 5, 'name' => 'sara'], ], 'selim' => [ ['id' => 3, 'name' => 'selim'] ], 'chris' => [ ['id' => 4, 'name' => 'chris'] ], ]; キーが同じ要素をまとめたいという要望ですね。 意外と需要があるようで、Qiitaでも何度も再発明されています。 元々はarray_columnの引数で返り値を変えようという意見であり、これには全員がやめろと突っ込んだ結果新たに関数を作るようにRFCを変更したのですが、それでも結局却下されてしまいました。 PHP Namespace in core 賛成19、反対24で却下。 PHPプロジェクトは\PHPで始まる名前空間を予約しています。 が、これがあんまり使われてないので言語コア機能は今後これを使っていこうぜという提案。 たとえば\PhpTokenではなく\PHP\PhpTokenにしようといったかんじです。 なお、既に実装されているものについてはこのRFCでは変更しません。 どうして却下されたかよくわからないのですが、MLによると『言語コア機能は』がひっかかった人が多いように見えます。 PHP Namespace Policy 賛成13、反対17で却下。 上のPHP Namespace in coreと同時期に作られたRFCです。 特に関連しているわけではなく、たまたま時期が被ったみたい。 PHPコア用に\PHP、拡張機能用に\Ext名前空間を予約します。 また名前空間の命名規則も規定します。 名前空間\Foo\Bar\Bazがあれば、Fooが企業名、Barがコンポーネント名となります。 なお、既に実装されているものについてはこのRFCでは変更しません。 規程までするのはやり過ぎと思われたのか、こちらも同様に拒否されました。 あと\Extを使うには申請しなきゃいけないとかPEARを思い出すような仕様があったのも原因かもしれません。 感想 そんなの却下されて当たり前だろというものから、どうしてこれが却下されたんだ?というものまで、様々なRFCが提案されています。 これらの却下されたRFCと、受理されたRFCを比べて今後のPHPの進む先を探ってみるのも楽しいかもしれませんね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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のコードを閲覧してきましたが、意外にもこのルールについて知らない人多いんではないでしょうか? 今回の内容だけでなく、便利なメソッドだからといって使うと思わぬ落とし穴があるものは他にもありそうです、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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一覧'); } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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フォルダをそれどれに突っ込む これで、マルチログインの準備はできた。 次はマルチログインの設定に行こうと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby OpenSSL::PKCS5.pbkdf2_hmac_sha1 をPHPで置き換えたときのメモ

def self.encrypt(data, pass, salt) cipher = OpenSSL::Cipher::Cipher.new("AES-256-CBC") key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, 1000, cipher.key_len+cipher.iv_len) key = key_iv[0, cipher.key_len] iv = key_iv[cipher.key_len, cipher.iv_len] cipher.encrypt cipher.key = key cipher.iv = iv encryptvalue = cipher.update(data) + cipher.final return Base64.b64encode(encryptvalue) end def self.decrypt(data, pass, salt) enctyptvalue = Base64.decode64(data) cipher = OpenSSL::Cipher::Cipher.new("AES-256-CBC") key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, 1000, cipher.key_len+cipher.iv_len) key = key_iv[0, cipher.key_len] iv = key_iv[cipher.key_len, cipher.iv_len] cipher.decrypt cipher.key = key cipher.iv = iv cipher.update(enctyptvalue) + cipher.final end function encrypt($data, $pass, $salt) { $key_iv = openssl_pbkdf2($pass, $salt, 32+16, 1000); $key = substr($key_iv, 0, 32); $iv = substr($key_iv, 32, 16); $decrypted_text = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv); return $decrypted_text; } function decrypt($data, $pass, $salt) { $key_iv = openssl_pbkdf2($pass, $salt, 32+16, 1000); $key = substr($key_iv, 0, 32); $iv = substr($key_iv, 32, 16); $decrypted_text = openssl_decrypt($data, 'AES-256-CBC', $key, 0, $iv); return $decrypted_text; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ファイルやカレンダー追加機能の実装について書きたいと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Adminer Editorでデータ更新依頼を自動化できないか検討

概要 データの修正依頼を受けてDB運用者が依頼に沿ってデータ修正するのではなく、直接依頼者がデータ修正できるようにできないかという想像から始まった。 phpmyadminやAdminerなどは機能豊富で生のSQLを叩けるのでなかなかハードルが高いし、だからといっていちいちツールを開発するのは大変。 そこで、いろいろ調べてみるとAdmin Editorというツールを見つけた。 Adminer Editorは非開発者でもわかりやすいようなUIを提供するデータ修正ツールで、Adminer同様多数のDBMSに対応している。 Adminerの機能制限版といった感じ?テーブルの作成やスキーマの修正などの機能を提供せずデータの追加、修正、削除のみを実施することができる。 そのため、アプリを立ち上げて依頼者にURLを提供すれば安全に修正できるようにならないか?という発想です。 公式サイトによると以下の制約があるようです。 SQLは直接実行できない データベースは1つしか選べない このページではAdminer EditorをDockerで試してみます。 環境構築 ローカルのDocker上でMySQL、Adminer Editorを構築する。 実行環境 AdminerはDockerイメージが公式で提供されているがAdminer Editorはなさそうだったので作成した。 mysqlのクライアントをインストールする。 Dockerfile FROM php:apache RUN docker-php-ext-install pdo_mysql mysqli それで、docker-compose.yamlでadminerをマウントするだけ。 docker-compose.yml version: '3.1' services: db: image: mysql restart: always environment: MYSQL_ROOT_PASSWORD: password adminer: build: context: . restart: always ports: - 80:80 volumes: - ./adminer:/var/www/html ソース editor.phpの1ファイルでソースは完結しているので公式ページからダウンロードして配置する。ただし、各種設定が必要なためindex.phpを新規作成して設定する。 設定方法は以下のページに記載されている。基本クラスのAdminerをオーバーライドして関数を書くことで各種設定ができる。 設定ファイル等ではないのでとっつきにくいがいろいろできるとも言える? tree ./adminer ./adminer ├── editor.php ├── index.php index.php <?php function adminer_object() { class AdminerSoftware extends Adminer { // 画面表示されるタイトル function name() { return 'tool'; } // DB設定 function credentials() { return array('db', 'root', 'password'); } // 使用するデータベース function database() { return 'world'; } // 認証方法、trueを返すと何も入力しなくても認証が通る。 function login($login, $password) { return true; } } return new AdminerSoftware(); } include './editor.php'; 確認 確認のためMySQLにMySQL公式で提供されているworld databaseをインポートした。 localhost/adminer にアクセスしログイン画面が表示された。 なんでも認証を通しているのでログイン成功。予め設定したDBのテーブルのみが表示されている。 テーブル一覧が表示された。 画面下部にはCSVインポート、エクスポートなどもある。 編集をクリックするとデータ変更できる。 データ修正しかできないのが良い感じ。 さらにカスタマイズしてみる 関数を書き換えるのでハードルが高いが以下のカスタムを試してみた。 不要なテーブルを表示しない 不要なカラムを表示しない tableNameとfieldNameをオーバーライドする。 function tableName($tableStatus) { // countryテーブルを非表示 $ignored_list = [ 'country', ]; if (in_array($tableStatus["Name"], $ignored_list, true)) { return ''; } return h($tableStatus["Name"]); } function fieldName($field, $order = 0) { // ID列を非表示 $ignored_list = [ 'ID' ]; if (in_array($field["field"], $ignored_list, true)) { return ''; } return '<span title="' . h($field["full_type"]) . '">' . h($field["field"]) . '</span>'; } countryテーブルが消えた。 IDカラムが消えた。 編集画面からも消えている。 これでIDや論理削除、作成日時などの列があれば非表示にできる。 感想 汎用的な修正ツールとして活用できそうだが、 実用を考えると操作ログがアクセスログくらいしかないことや認証周りの機能が少ないので解決する必要があるという感想を持った。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む