- 投稿日:2019-02-26T23:29:18+09:00
Laravel $user->posts()->get() と $user->posts の違いを理解する
環境
- PHP 7.2
- Laravel 5.6
前提
- ブログサービスを想定、Userが複数のPost(投稿)を持つ(1対多関係)
- Userテーブルはid, name, email, password, created_at, updated_atカラムを持つ
- Postテーブルはid, content, created_at, updated_atカラムを持つ
リレーションメソッドと動的プロパティ
$user = User::find($id); // ユーザを取得 $posts = $user->posts()->get(); // ユーザが持つ投稿一覧を取得2行目は
user->posts
と書いても同じ結果が得られる。https://readouble.com/laravel/5.6/ja/eloquent-relationships.html
リレーションが定義できたらEloquentの動的プロパティを使って、関係したレコードを取得できます。動的プロパティによりモデル上のプロパティのようにリレーションメソッドにアクセスできます。
ドキュメントにならって
$user->posts()->get()
をリレーションメソッドを扱ったアクセス、$user->posts
を動的プロパティを扱ったアクセスと呼ぶことにする。筆者はてっきり動的プロパティの記法はリレーションメソッドの省略形と思っていた。
しかしこの後の検証で動作上の違いを認識することができた。
LaravelのREPL環境である
php artisan tinker
を実行する。>>> $user = User::find(5) => App\User {#2952 id: 5, name: "test", created_at: "2019-02-26 20:12:50", updated_at: "2019-02-26 20:12:50", } >>> $user => App\User {#2952 id: 5, name: "test", created_at: "2019-02-26 20:12:50", updated_at: "2019-02-26 20:12:50", } >>> $user->posts()->get() => Illuminate\Database\Eloquent\Collection {#2958 all: [ App\Post {#2950 id: 67, content: "test", user_id: 5, created_at: "2019-02-26 20:13:14", updated_at: "2019-02-26 20:13:14", }, ], } >>> $user => App\User {#2952 id: 5, name: "test", created_at: "2019-02-26 20:12:50", updated_at: "2019-02-26 20:12:50", } >>> $user->posts => Illuminate\Database\Eloquent\Collection {#2955 all: [ App\Post {#2942 id: 67, content: "test", user_id: 5, created_at: "2019-02-26 20:13:14", updated_at: "2019-02-26 20:13:14", }, ], } >>> $user => App\User {#2952 id: 5, name: "test", created_at: "2019-02-26 20:12:50", updated_at: "2019-02-26 20:12:50", posts: Illuminate\Database\Eloquent\Collection {#2955 all: [ App\Post {#2942 id: 67, content: "test", user_id: 5, created_at: "2019-02-26 20:13:14", updated_at: "2019-02-26 20:13:14", }, ], }, }リレーションメソッドはレコードの結果を返す。
動的プロパティもレコードの結果を返す。
違いは、動的プロパティを使った場合はレシーバである$user
インスタンスにposts
プロパティが追加される点。つまり、
$user->posts
を実行すると$user
にposts
情報がキャッシュされる。
これ以降、$user->posts
を実行した場合は、クエリを発行せずにキャッシュされたデータを返すようだ。遅延ロード
この挙動は遅延ローディングとその目的であるN+1問題の解消のために存在しているようだ。
動的プロパティは「遅延ロード」されます。つまり実際にアクセスされた時にだけそのリレーションのデータはロードされます。そのため開発者は多くの場合にEagerローディングを使い、モデルをロードした後にアクセスするリレーションを前もってロードしておきます。Eagerロードはモデルのリレーションをロードするため実行されるSQLクエリを大幅に減らしてくれます。
ドキュメントではAuthorとBookの例で説明されている。
$books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }
with
でN+1を防止できることは知っていたが、そちらばかりに注目がいっており動的プロパティは雑な理解に留まってしまっていた。結論
まとめると下記のようになる。
- リレーションメソッドによるアクセスは常にクエリ発行する
- 動的プロパティによるアクセスは、キャッシュデータが存在しなければクエリ発行し、レシーバインスタンスにキャッシュとなるプロパティを生成する。キャッシュが存在すればそれを返すためクエリ発行しないという挙動をする
App\Book::with('author')->get();
で返ってくるBooksコレクションにはその一つ一つにAuthorのデータがキャッシュされている状態になっており、キャッシュデータを利用するには動的プロパティでアクセスしなければならない。すなわち、
$books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author()->first()->name; // リレーションメソッドによるアクセス(NGパターン) }このようにリレーションメソッドでアクセスすると都度クエリが発行されてしまうため駄目ですよ、遅延ロードの際は動的プロパティを使いましょうという話。
何か誤解があれば指摘頂きたいと思います。
- 投稿日:2019-02-26T23:28:34+09:00
stdClassのオブジェクトを作る方法
- データベースから取ってくるデータがstdClassの配列だったりします。
- そのstdClassの配列を処理するコードを作ったりします。
- コードを作るとPHPUnitでテストしたりします。
- そうすると期待値や引数がstdClassだったりします。
- するとstdClassのオブジェクトを作らねばなりません。
- 環境
- macOS Mojave バージョン10.14.3
- PHP 7.3.1
- Laravel Framework 5.7.26
方法1. 配列をオブジェクトでキャストする
controlStdClassArray->createStdClassArrayCastpublic function createStdClassArrayCast(): array { $stdArray = [ (object)['name' => 'イタリアンパセリ', 'otherName' => 'パースレー', 'gakumei' => 'Petroselinum neapolitanum', 'bunrui' => 'セリ科オランダゼリ属'], (object)['name' => 'グラジオラス', 'otherName' => '唐菖蒲', 'gakumei' => 'Gladiolus', 'bunrui' => 'アヤメ科グラジオラス属'], (object)['name' => 'アボカド', 'otherName' => 'ワニナシ', 'gakumei' => 'Persea americana', 'bunrui' => 'クスノキ科ワニナシ属'], (object)['name' => 'マロウ', 'otherName' => 'ウスベニアオイ', 'gakumei' => 'Malva sylvestris', 'bunrui' => 'アオイ科ゼニアオイ属'], ]; return $stdArray; }$ php controlStdClassArray.php array(4) { [0]=> object(stdClass)#2 (4) { ["name"]=> string(24) "イタリアンパセリ" ["otherName"]=> string(15) "パースレー" ["gakumei"]=> string(25) "Petroselinum neapolitanum" ["bunrui"]=> string(30) "セリ科オランダゼリ属" } [1]=> object(stdClass)#3 (4) { ["name"]=> string(18) "グラジオラス" ["otherName"]=> string(9) "唐菖蒲" ["gakumei"]=> string(9) "Gladiolus" ["bunrui"]=> string(33) "アヤメ科グラジオラス属" } [2]=> object(stdClass)#4 (4) { ["name"]=> string(12) "アボカド" ["otherName"]=> string(12) "ワニナシ" ["gakumei"]=> string(16) "Persea americana" ["bunrui"]=> string(30) "クスノキ科ワニナシ属" } [3]=> object(stdClass)#5 (4) { ["name"]=> string(9) "マロウ" ["otherName"]=> string(21) "ウスベニアオイ" ["gakumei"]=> string(16) "Malva sylvestris" ["bunrui"]=> string(30) "アオイ科ゼニアオイ属" } }方法2.
new \stdClass
をして1つ1つ設定するcontrolStdClassArray->createStdClassArrayNewpublic function createStdClassArrayNew() { $stdArray = []; $stdObj = new \stdClass(); $stdObj->name = 'イタリアンパセリ'; $stdObj->otherName = 'パースレー'; $stdObj->gakumei = 'Petroselinum neapolitanum'; $stdObj->bunrui = 'セリ科オランダゼリ属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'グラジオラス'; $stdObj->otherName = '唐菖蒲'; $stdObj->gakumei = 'Gladiolus'; $stdObj->bunrui = 'アヤメ科グラジオラス属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'アボカド'; $stdObj->otherName = 'ワニナシ'; $stdObj->gakumei = 'Persea americana'; $stdObj->bunrui = 'クスノキ科ワニナシ属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'マロウ'; $stdObj->otherName = 'ウスベニアオイ'; $stdObj->gakumei = 'Malva sylvestris'; $stdObj->bunrui = 'アオイ科ゼニアオイ属'; $stdArray[] = $stdObj; return $stdArray; }$ php controlStdClassArray.php array(4) { [0]=> object(stdClass)#2 (4) { ["name"]=> string(24) "イタリアンパセリ" ["otherName"]=> string(15) "パースレー" ["gakumei"]=> string(25) "Petroselinum neapolitanum" ["bunrui"]=> string(30) "セリ科オランダゼリ属" } [1]=> object(stdClass)#3 (4) { ["name"]=> string(18) "グラジオラス" ["otherName"]=> string(9) "唐菖蒲" ["gakumei"]=> string(9) "Gladiolus" ["bunrui"]=> string(33) "アヤメ科グラジオラス属" } [2]=> object(stdClass)#4 (4) { ["name"]=> string(12) "アボカド" ["otherName"]=> string(12) "ワニナシ" ["gakumei"]=> string(16) "Persea americana" ["bunrui"]=> string(30) "クスノキ科ワニナシ属" } [3]=> object(stdClass)#5 (4) { ["name"]=> string(9) "マロウ" ["otherName"]=> string(21) "ウスベニアオイ" ["gakumei"]=> string(16) "Malva sylvestris" ["bunrui"]=> string(30) "アオイ科ゼニアオイ属" } }全貌
controlStdClassArray.php<?php namespace App; class controlStdClassArray { public function createStdClassArrayCast(): array { $stdArray = [ (object)['name' => 'イタリアンパセリ', 'otherName' => 'パースレー', 'gakumei' => 'Petroselinum neapolitanum', 'bunrui' => 'セリ科オランダゼリ属'], (object)['name' => 'グラジオラス', 'otherName' => '唐菖蒲', 'gakumei' => 'Gladiolus', 'bunrui' => 'アヤメ科グラジオラス属'], (object)['name' => 'アボカド', 'otherName' => 'ワニナシ', 'gakumei' => 'Persea americana', 'bunrui' => 'クスノキ科ワニナシ属'], (object)['name' => 'マロウ', 'otherName' => 'ウスベニアオイ', 'gakumei' => 'Malva sylvestris', 'bunrui' => 'アオイ科ゼニアオイ属'], ]; return $stdArray; } public function createStdClassArrayNew() { $stdArray = []; $stdObj = new \stdClass(); $stdObj->name = 'イタリアンパセリ'; $stdObj->otherName = 'パースレー'; $stdObj->gakumei = 'Petroselinum neapolitanum'; $stdObj->bunrui = 'セリ科オランダゼリ属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'グラジオラス'; $stdObj->otherName = '唐菖蒲'; $stdObj->gakumei = 'Gladiolus'; $stdObj->bunrui = 'アヤメ科グラジオラス属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'アボカド'; $stdObj->otherName = 'ワニナシ'; $stdObj->gakumei = 'Persea americana'; $stdObj->bunrui = 'クスノキ科ワニナシ属'; $stdArray[] = $stdObj; $stdObj = new \stdClass(); $stdObj->name = 'マロウ'; $stdObj->otherName = 'ウスベニアオイ'; $stdObj->gakumei = 'Malva sylvestris'; $stdObj->bunrui = 'アオイ科ゼニアオイ属'; $stdArray[] = $stdObj; return $stdArray; } } $clas = new controlStdClassArray(); var_dump($clas->createStdClassArrayCast()); var_dump($clas->createStdClassArrayNew());参考
- 投稿日:2019-02-26T20:49:16+09:00
Alibaba Cloud上のECS&SLB構成でLaravelを動かすときのroute()でhttpsを出力する方法
中国版AWSであるAlibaba CloudでLaravelを動かした際に手間取った点をまとめました。タイトルの通りです。
作ったのはSLB(Server Load Balancer)とECS(Elastic Compute Service)を用いたシンプルなアーキテクチャ。AWSにおけるELBとEC2みたいなものです。
HTTPS化するために、ELBと同様にSSL証明書をSLBにインストールしました。インストールの手順はSLBのほうが簡単かもしれません。
Alibaba CloudもAWSと同様に、client <- 443 -> SLB <- 80 -> ECS
という通信の流れです。
これで何も考えずにLaravelで
route('auth.login')
と書いたら、
http://hoge.foo.bar/login
と、HTTPのURLが出力されてしまいました。
出力したいのは
https://hoge.foo.bar/login
です。まあ、これはAWSでありふれた問題なので解決方法もインターネットに溢れています。
というかLaravelの公式ドキュメントに記載されています。https://laravel.com/docs/5.7/requests#configuring-trusted-proxies
https://readouble.com/laravel/5.7/ja/requests.html#configuring-trusted-proxies公式ドキュメントにもあるように、
app/Http/Middleware/TrustProxies.php
を以下のように設定します。app/Http/Middleware/TrustProxies.php/** * The trusted proxies for this application. * * @var array */ protected $proxies = '*';これでうまくいくかと思いきやまだHTTPのまま
どうしてだろう。。。ここで何をしているかというと、
ロードバランサーからのリクエストを信頼できる通信であると、
しかもprotected $proxies = '*';
と、すべてを許可するようにしています。
そして信頼できる通信だと、route()
での出力がhttps://
になります。
さらに詳しく調べると、vendor/symfony/http-foundation/Request.php
のisSecure()
が信頼できるかどうかを判断しています。vendor/symfony/http-foundation/Request.php/** * Checks whether the request is secure or not. * * This method can read the client protocol from the "X-Forwarded-Proto" header * when trusted proxies were set via "setTrustedProxies()". * * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". * * @return bool */ public function isSecure() { if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) { return in_array(strtolower($proto[0]), array('https', 'on', 'ssl', '1'), true); } $https = $this->server->get('HTTPS'); return !empty($https) && 'off' !== strtolower($https); }
X-Forwarded-Proto
を調べてますね。
そういえば、SLBのリスナー設定でX-Forwarded-Proto
のチェック項目があったような。。。ありました!!
ということで「ヘッダーにX-Forwarded-Proto(SLBへの接続プロトコル)を追加します」にチェックを入れて更新したら、
晴れてroute()
で出力されるURLがhttps://
となりました。
めでたしめでたし
- 投稿日:2019-02-26T20:30:21+09:00
【Laravel5.7】テスト時にHTTPレスポンスからlaravel_sessionが取れず死んだ
laravel_sessionが取れない
Laravelでは、特に設定変更しなければlaravel_sessionというCookie1が発行され、それを使ってセッションを管理します。
テストからlaravel_sessionをチェックしたかったのだけれども、できなかったのでその記録をメモ。
なお解決はしていない。class FooTest extends TestCase{ /** * なんかリクエストするテスト * @return void */ public function testFoobar(){ $response = $this->get('foo/bar'); $response->assertStatus(200)->assertCookieNotExpired('laravel_session'); } }なにひとつ失敗する要素がない。
1) Tests\Feature\FooTest::testFoobar Cookie [laravel_session] not present on response. Failed asserting that null is not null. FAILURES! Tests: 1, Assertions: 2, Failures: 1.はい。
リクエストは成功しレスポンスコード200が返ってきている。
getContent()
でレスポンス本体が取得できるが、それも想定通りの内容で、別のAPIをリクエストしていたということもない。var_dump($response->headers->getCookies());array(0) {}何も取得できていない。
どういうことなの。なんか別のCookie出してみる
コントローラ
class FooController extends Controller{ public function barAction(Request $request){ return response()->cookie('hoge', 'fuga', -1); } }ブラウザから確認。
Set-Cookie: hoge=xxx; expires=xxx; Max-Age=0; path=/; httponly Set-Cookie: laravel_session=xxx; expires=xxx; Max-Age=7200; path=/; httponly中身は暗号化されているため値が正しいかはわからないが、Cookie自体はきちんと発行されている。
テスト。
class FooTest extends TestCase{ public function testFoobar(){ $response = $this->get('foo/bar'); var_dump($response->headers->getCookies()); } }実行結果。
array(1) { [0]=> object(Symfony\Component\HttpFoundation\Cookie)#396 (10) { ["name":protected]=> string(4) "hoge" ["value":protected]=> string(192) "xxx" ["domain":protected]=> NULL ["expire":protected]=> int(9999999999) ["path":protected]=> string(1) "/" ["secure":protected]=> bool(false) ["httpOnly":protected]=> bool(true) ["raw":"Symfony\Component\HttpFoundation\Cookie":private]=> bool(false) ["sameSite":"Symfony\Component\HttpFoundation\Cookie":private]=> NULL ["secureDefault":"Symfony\Component\HttpFoundation\Cookie":private]=> bool(false) } }追加で出力した
hoge
だけが取得できた。つまり、laravel_sessionは自動的に隠蔽されるということなのか?
でもセッションは継続してる
テストを1項目追加。
class FooTest extends TestCase { /** * テストを追加 * @return void */ public function testFoobar(){ $response = $this->get('foo/bar'); // foo/barが前提のリクエスト $response2 = $this->get('foo/baz'); $response2->assertStatus(200); } }コントローラ。
class FooController extends Controller{ public function barAction(Request $request){ $request->session()->put('hoge', 'fuga'); } public function bazAction(Request $request){ if($request->session()->get('hoge') === 'fuga'){ return []; } abort(404); } }
foo/baz
に来たときに、foo/bar
を経由していれば200、していなければ404が出力されることになる。
さて結果は?OK (1 test, 1 assertion)はい。
なぜなのか。
セッションデータは何処にあるの?
ここ。
class FooTest extends TestCase { public function testFoobar(){ $response = $this->get('foo/bar'); $response2 = $this->get('foo/baz'); // セッションデータはここ var_dump($this->app['session']->all()); } }セッションIDではなく、
["hoge"]=>"fuga"
というデータそのものがここに入ってる。laravel_sessionはどこにあるの?
どこでしょうね?
どうにかした
laravel_session
ではなく別のCookieを発行して、そちらに対してassertCookieXXX
を行うことでお茶を濁した。なお、"Cookieが発行されていること"そのものを見る必要がなく、セッションの中身だけ検証すればよいのであれば、最初から
assertSessionXXX
を使うといい。感想
結局
laravel_session
が何処で消されているのかはわからなかった。$response = $kernel->handle( $request = Request::createFromBase($symfonyRequest) );とかのあたりね、そのね、全く意味がわからんのじゃけど。
実際は
strtolower(env('APP_NAME')).'_session'
のような名前になる ↩
- 投稿日:2019-02-26T18:20:07+09:00
PHPで容量の大きいデータをCSV出力するときに工夫したこと
前にもPHPでSJISのデカイCSVデータを扱った時に困ったことという記事を書いたけど、やっぱりCSVを扱うのって少し難しい。
今回は 「ログのデータをCSV出力してほしい」 という依頼があったときの話です。検索をかければ、スニペットコードはたくさん見つかるのでなんとなく組み合わせて動くコードを書くところまではすんなりいったけれど、それだと容量の大きいデータを出力するときにうまくいかなかったりと手こずりました。
この記事では「容量の大きいデータだとCSV出力できないコード」をどうやって「最大20000件のデータまで出力できるように修正」したときのポイントなどについてまとめます。
備考
自分なりに調べて書いた記事なので、解釈が間違っている箇所もあるかもしれません。
間違っているところがあればコメントでご指摘いただけると幸いです!仕様
- Laravel5.4
t_logs
というテーブルに入ってくるログをCSV出力してほしいとのこと- アプリケーションは Heroku にデプロイされている
いちばん最初に書いたコード(容量が大きくなければ動くコード)
Route::get('/log', function(){ $stream = fopen('php://temp', 'w'); //1.一時的なファイルポインタを作成 $result = DB::table("t_logs")->get(); $data = []; foreach ($result as $log) { fputcsv($stream, array_values((array)$log))); //2.ファイルポインタに書き込む } rewind($stream); //3.ファイルポインタの位置を先頭に戻す $csv = str_replace(PHP_EOL, "\r\n", stream_get_contents($stream)); //4.改行コードの置き換え return Response::make($csv, 200, [ //5.CSV出力 'Content-Type' => 'text/csv', 'Content-Disposition' => "attachment; filename=t_logs.csv" ]); });1. 一時的なファイルポインタを作成
fopen()
はファイルまたは URL をオープンする関数で、これを利用して一時的なファイルポインタを作成できる。
fopen()
は第二引数にモードを指定することができ、これを変えれば「読み出しのみ」や「書き込み&読み出し可」のような指定が可能。今回は
書き出しのみでオープンします。ファイルポインタをファイルの先頭に置き、 ファイルサイズをゼロにします。ファイルが存在しない場合には、 作成を試みます。
というモード
w
を使ってファイルポインタを作成した。補足:ファイルポインタってなに?
ファイルの操作でよく出てくる「ファイルポインタ」というものは、サーバーがファイルを扱うための専用の"しおり"のようなもの。(どこまで読み取ったか、など)
補足:
php://temp
って?php://memory および php://temp は読み書き可能なストリームで、一時データをファイルのように保存できるラッパーです。
両者の唯一の違いは、php://memory が常にデータをメモリに格納するのに対して php://temp は定義済みの上限 (デフォルトは 2 MB) に達するとテンポラリファイルを使うという点です。
このテンポラリファイルの場所は、 sys_get_temp_dir() 関数と同じ方法で決めます。どちらもメモリ上に領域を確保するという点では同じだが、
php://memory
だとメモリリークしてしまうことがあるため、
2MBを超えた場合はテンポラリファイル(自動削除される一時ファイル)を作ってくれるphp://temp
を使うことにした。補足:メモリリークってなに?
プログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうこと。
2. ファイルポインタに書き込む
fputcsv()
を使って、列をCSV 形式にフォーマットしてファイルポインタに書き込んでいく。
このとき、 いちばん最後に改行が追加 される。3. ファイルポインタを先頭に戻す
fputcsv()
ファイルの書き込みが終わると、ファイルポインタの位置は最後に書き込みが終えた時点になっている。
次に行う処理は書き込んだテンポラリファイルの先頭から開始する必要があるため、rewind()
を使ってファイルポインタを先頭に戻す。4. 改行コードを置き換え
fputcsv
でファイルポインタを書き込んでいくときに改行コード(PHP_EOL)が追加されるが、この改行コードはOSに依存する。
OS 改行コード 改行コード文字 Windows CRLF \r\n Linux,Unix,MACOS10 LF \n (引用: https://qiita.com/kazu56/items/bc77582313918fe2a3b1 )
そのため、CSV出力したサーバーのOSと、実際にCSVを使うOSが異なる場合(LinuxとWindows)にうまいこと改行してくれない...ということが起こってしまう。
これを防ぐため、
str_replace()
を使って\r\n
に置き換えた。(今回CSVを使うOSはWindows)補足:
stream_get_contents
file_get_contents() と似ていますが、 stream_get_contents() は既にオープンしているストリームリソースに対して操作を行います。
そして、指定した offset から始まる最大 maxlength バイトのデータを取得して文字列に 保存します。今回はすでに
fopen()
でファイルはオープンしているためstream_get_contents()
を使用し、文字列として読み込んだ。5. CSV出力
文字列にしたデータを
Response::make()
を使ってCSV出力。容量が大きすぎてエラーになった
実際にテスト環境で出力しようとしたところ、エラーとなってしまった。
Fatal error: Allowed memory size of ...データが大きい時はどうやらメモリのことを考えないといけないらしい...。
原因となった部分
想定していたよりもデータの容量が大きく(Maxで20000件)、
stream_get_contents
で文字列にするときにメモリに乗り切らなかった。修正後
Route::get('/log', function(){ $stream = fopen('php://temp', 'w'); $result = DB::table("t_logs")->get(); $data = []; foreach ($result as $log) { fputcsv($stream, str_replace(PHP_EOL, "\r\n", array_values((array)$log))); //修正 1.改行コードの置き換え時に文字列変換しない } rewind($stream); //注意:fpassthru() する前にもファイルポインタは戻しておく return response()->stream(function () use ($stream) { //修正 2. ストリームのままCSV出力できるようにする fpassthru($stream); fclose($stream); }, 200, [ 'Content-Type' => 'text/csv', 'Content-Disposition' => "attachment; filename=t_logs.csv" ]); });修正 1. 改行コードの置き換え時に文字列変換しない
stream_get_contents()
でストリームが文字列に変換されてしまうと、メモリが必要となってしまうため
fputcsv()
で書き出すと同時に改行コードの処理をしてしまうことにした。修正 2. ストリームのままCSV出力できるようにする
文字列に変換しないことになったので、出力方法にも工夫が必要。
Response::make()
では文字列のみを許容するため、ストリームのまま出力できるるようresponse()->stream()
を使う。この時、
fpassthru()
を使って、ファイルポインタ上に残っているすべてのデータを出力する。
テンポラリファイルはスクリプト終了時にも自動削除されるようだが、念のためfclose()
で終了を明示した。注意:
fpassthru()
する前にもファイルポインタは戻しておく「改行の置き換えが済んだなら、ファイルポインタを戻す必要はないんじゃないの?」と思いきや、
ポインタが一番後ろに位置したままだとfpassthru()
しても読み取るものがなく、空っぽのCSVが出力されてしまうので注意。まとめ
容量の大きくないデータをCSV出力するのであれば最初のコードでも問題はないけれど、
- どの関数がなんの役割をしているのか
- どのような形式でデータを保持しているのか
- ファイルポインタの挙動 etc...
といったことを理解していないと、今回のログデータのように容量の大きいデータを出力する際にはうまくいきませんでした。
CSV出力に限らず、何かを実現するために手法は何通りもあることが多いけれど、
状況に応じて工夫する必要は必ずでてくるので、関数を使ったりするときは公式ドキュメントなどもよんで理解することは大事だなぁと思いました。参考
PHP関数・クラス
fopen
fputcsv
rewind
file_get_contents
stream_get_contents
fclose
fpassthruその他
http://php-beginner.com/practice/file_ope/file_ope6.html
https://www.muchacolla.com/work/php/416/
https://qiita.com/mpyw/items/f24d3764fe3eedf132ff
http://www.standpower.com/php_analyz.html
http://php.net/manual/ja/wrappers.php.php
https://lab.flama.co.jp/archives/1139/
https://qiita.com/ma_me/items/fae63108dbce03290efb
- 投稿日:2019-02-26T17:11:20+09:00
[Laravel] 5.4 => 5.5 のバージョンアップでハマった点まとめ
Model
「updated_at」および「created_at」がテーブルに存在しない時に save() でデータ保存しようとするとエラーになる
エラー内容
ArgumentCountError: Too few arguments to function Illuminate\Database\Eloquent\Model::setAttribute(), 1 passed in /home/livede55.com/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php on line 525 and exactly 2 expected
setAttribute()
が呼ばれているが引数が足りないよ
と怒られてしまう原因
updated_at
およびcreated_at
が存在しないテーブルなのでEloquentの設定は以下にしているconst CREATED_AT = null; const UPDATED_AT = null;この状態で何故か
5.5
になるとupdated_at
およびcreated_at
に対してのsetAttribute()
が動作し、
カラム名がnull
なので 引数エラーが起こる本家LaravelのGitHubにもIssueが作成されている
https://github.com/laravel/framework/issues/20901
対応
Issueにハックが記載されている対応を行う
public function setUpdatedAt($value) { return $this; } public function setCreatedAt($value) { return $this; }
setAttribute()
呼び出しされる前提でupdated_at
およびcreated_at
のミューテターを定義してあげる
- 投稿日:2019-02-26T15:40:01+09:00
Laravel duskで別ホストのブラウザを使う方法
開発環境をhomestead等で構築した場合、duskテストをheadlessでしか実行できない。
そこで、ホストOS(それ以外のホストでも同じ)のブラウザを使うことでブラウザ動作を目視できるようにする。記事は下記構成を想定
・開発環境: homestead(VirtualBox)
・Laravelバージョン: Laravel5.5 + dusk2.0
・ホストOS: Windows10ホストOS側
Laravelプロジェクト内のchromedriver実行ファイルをホストOSの任意の場所にコピー
win,mac,linux用があるので環境にあわせてチョイスしてください。
vendor/laravel/dusk/bin/chromedriver-win.exe
chromedriverを実行
接続元IPアドレスのホワイトリストを設定する必要があるが、面倒なので全許可にしておく。
./chromedriver-win.exe --whitelisted-ips 0.0.0.0
ファイアウォール等の設定
外部からの接続を阻害する要因は全てパスしておくこと。
その他
Chromeブラウザがインストールされていること。
Laravelプロジェクト側
DuskTestCase.phpを編集
optionsのheadless設定はコメントアウトします。
10.0.2.2:9515がホストOSのIP:ポートです。tests/DuskTestCase.phpprotected function driver() { $options = (new ChromeOptions)->addArguments([ // '--disable-gpu', // '--headless' ]); return RemoteWebDriver::create( 'http://10.0.2.2:9515', DesiredCapabilities::chrome()->setCapability( ChromeOptions::CAPABILITY, $options ) ); }duskテスト実行
いつもどおりduskコマンドでテストを実行します。
ホストOS側のブラウザが起動して自動操作が始まります。
php artisan dusk test/Browser/HogeTest.php
- 投稿日:2019-02-26T13:10:23+09:00
Laravel5.7で前回のTwitterOAuthを使ってツイートやらハッシュタグやらユーザーやらの取得方法一覧(投稿あり)
TwitterOAuthでツイートやらを取得する記事はいくらでもあるが、中々取得する一覧みたいのがなかったので、
完全自分用に書いていく。前回で設定したTwitterOAuthを使って色々取得してみる。
以下任意のコントローラーで試してください
この記事ではSampleController.phpというファイル名で使用します。
※全てdump()でjsonデータを表示するだけの処理任意のキーワードを検索する
キーワードを複数指定する場合は半角スペース区切りで入力する
※ツイートしたばかりだと少し時間を置かないとツイートを取得できない(約3分ほど?)SampleController.phppublic function sample() { //"TwitterOAuthを使って検索するよ"というツイートを10件取得する $search_word = \Twitter::get("search/tweets", array("q" => "TwitterOAuthを使って検索するよ", 'count' => 10)); dump($search_word); }任意のハッシュタグを検索する
上記のキーワード検索に
#ハッシュタグ
をつけるだけSampleController.phppublic function sample() { //"TwitterOAuthを使って検索するよ"というツイートを10件取得する $hash_tag = \Twitter::get("search/tweets", array("q" => "#TwitterOAuthを使ってハッシュタグを検索するよ", 'count' => 10)); dump($hash_tag); }自分のタイムラインを取得
SampleController.phppublic function sample() { // 自分のタイムラインを10件取得 $time_line = \Twitter::get('statuses/home_timeline', ['count' => 10]); dump($time_line); }フォロワーを取得
自分のフォロワーを取得する
SampleController.phppublic function sample() { // フォロワーを10件取得 $follower = \Twitter::get('followers/list', ['count' => 10]); dump($follower); }任意のユーザをIDで検索する
とりあえず僕のTwitterIDを取得する(フォローしてほしいだけ)
SampleController.phppublic function sample() { // @以降のuser_idを指定してください $search_user = \Twitter::get('users/show', ['screen_name'=> '@namizatop']); dump($search_user); }ツイートする
TwitterOAuthを使ってツイート
とツイートしてみる
※2回投稿するとStatus is a duplicate.
というerrorが吐かれるので注意。SampleController.phppublic function sample() { // "TwitterOAuthを使ってツイート"とツイート $tweet = \Twitter::post('statuses/update', ['status'=> 'TwitterOAuthを使ってツイート']); dump($tweet); }任意のツイートをリツイートする
僕の投稿の
TwitterOAuthを使ってリツイートするよ
をリツイートしてみるSampleController.phppublic function sample() { // 僕の投稿の"TwitterOAuthを使ってリツイートするよ"の投稿をリツイート $retweet = \Twitter::post("statuses/retweet/1100242514680807424"); dump($retweet); }任意のツイートをいいね!する
僕の投稿の
TwitterOAuthを使っていいね!するよ
をいいね!してみるSampleController.phppublic function sample() { // 僕の投稿の"TwitterOAuthを使っていいね!するよ"の投稿をいいね! $favorite = \Twitter::post("favorites/create", ['id' => '1100245496159821825']); dump($favorite); }以上!
僕のTwitterのステマでした!参考リンク
参考にさせていただきましたm(_ _)m
Twitter APIでつぶやきを取得する
- 投稿日:2019-02-26T00:42:16+09:00
laravelのvalidatorをtinkerでちょこっとだけ試す
概要
laravelのvalidatorをチョコっと試す
忘れてしまうのでメモしておくtinkerて何。
コンソールでlaravelとかPHPをチョコっと試せる機能だ
詳しくは・・・⬇️
https://readouble.com/laravel/5.7/ja/artisan.html#tinker
(怖い人よけの為に公式も貼っておく)
https://laravel.com/docs/5.7/artisan#tinkerlaravelのバリデーションって豊富だ。
ルールがアレコレあってチョコっとだけ試したい時があるなぁ。
https://readouble.com/laravel/5.5/ja/validation.html#available-validation-rulestinkerを起動したら以下の様にかく
# こうかくと >>> Validator::make([''], ['data' => 'required'])->errors()->toArray() # こう返ってくるよ => [ "data" => [ "The data field is required.", ], ] # あとは好きに試す >>> Validator::make(['data' => 'yeah'], ['data' => 'numeric'])->errors()->toArray() => [ "data" => [ "The data must be a number.", ], ]