20190226のlaravelに関する記事は9件です。

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 を実行すると $userposts 情報がキャッシュされる。
これ以降、 $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パターン)
}

このようにリレーションメソッドでアクセスすると都度クエリが発行されてしまうため駄目ですよ、遅延ロードの際は動的プロパティを使いましょうという話。

何か誤解があれば指摘頂きたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

stdClassのオブジェクトを作る方法

  1. データベースから取ってくるデータがstdClassの配列だったりします。
  2. そのstdClassの配列を処理するコードを作ったりします。
  3. コードを作るとPHPUnitでテストしたりします。
  4. そうすると期待値や引数がstdClassだったりします。
  5. するとstdClassのオブジェクトを作らねばなりません。
  • 環境
    • macOS Mojave バージョン10.14.3
    • PHP 7.3.1
    • Laravel Framework 5.7.26

方法1. 配列をオブジェクトでキャストする

controlStdClassArray->createStdClassArrayCast
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;
}
$ 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->createStdClassArrayNew
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;
}
$ 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());

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のまま:sob:
どうしてだろう。。。

ここで何をしているかというと、
ロードバランサーからのリクエストを信頼できる通信であると、
しかもprotected $proxies = '*';と、すべてを許可するようにしています。
そして信頼できる通信だと、route()での出力がhttps://になります。
さらに詳しく調べると、vendor/symfony/http-foundation/Request.phpisSecure()が信頼できるかどうかを判断しています。

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のチェック項目があったような。。。

スクリーンショット 2019-02-26 20.01.30.png

ありました!!
ということで「ヘッダーにX-Forwarded-Proto(SLBへの接続プロトコル)を追加します」にチェックを入れて更新したら、
晴れてroute()で出力されるURLがhttps://となりました。
めでたしめでたし:relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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)
);

とかのあたりね、そのね、全く意味がわからんのじゃけど。


  1. 実際はstrtolower(env('APP_NAME')).'_session'のような名前になる 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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 のミューテターを定義してあげる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.php
protected 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel5.7で前回のTwitterOAuthを使ってツイートやらハッシュタグやらユーザーやらの取得方法一覧(投稿あり)

TwitterOAuthでツイートやらを取得する記事はいくらでもあるが、中々取得する一覧みたいのがなかったので、完全自分用に書いていく。

前回で設定したTwitterOAuthを使って色々取得してみる。

参考リンク
【前回】Laravel5.7でTwitterOAuthを使って認証やらツイート取得やら

以下任意のコントローラーで試してください
この記事ではSampleController.phpというファイル名で使用します。
※全てdump()でjsonデータを表示するだけの処理

任意のキーワードを検索する

キーワードを複数指定する場合は半角スペース区切りで入力する
※ツイートしたばかりだと少し時間を置かないとツイートを取得できない(約3分ほど?)

SampleController.php
    public function sample()
    {
        //"TwitterOAuthを使って検索するよ"というツイートを10件取得する
        $search_word = \Twitter::get("search/tweets", array("q" => "TwitterOAuthを使って検索するよ", 'count' => 10));

        dump($search_word);
    }

任意のハッシュタグを検索する

上記のキーワード検索に#ハッシュタグをつけるだけ

SampleController.php
    public function sample()
    {
        //"TwitterOAuthを使って検索するよ"というツイートを10件取得する
        $hash_tag = \Twitter::get("search/tweets", array("q" => "#TwitterOAuthを使ってハッシュタグを検索するよ", 'count' => 10));

        dump($hash_tag);
    }

自分のタイムラインを取得

SampleController.php
    public function sample()
    {
        // 自分のタイムラインを10件取得
        $time_line = \Twitter::get('statuses/home_timeline', ['count' => 10]);

        dump($time_line);
    }

フォロワーを取得

自分のフォロワーを取得する

SampleController.php
    public function sample()
    {
        // フォロワーを10件取得
        $follower = \Twitter::get('followers/list', ['count' => 10]);

        dump($follower);
    }

任意のユーザをIDで検索する

とりあえず僕のTwitterIDを取得する(フォローしてほしいだけ)

SampleController.php
    public function sample()
    {
        // @以降のuser_idを指定してください
        $search_user = \Twitter::get('users/show', ['screen_name'=> '@namizatop']);

        dump($search_user);
    }

ツイートする

TwitterOAuthを使ってツイートとツイートしてみる
※2回投稿するとStatus is a duplicate.というerrorが吐かれるので注意。

SampleController.php
    public function sample()
    {
        // "TwitterOAuthを使ってツイート"とツイート
        $tweet = \Twitter::post('statuses/update', ['status'=> 'TwitterOAuthを使ってツイート']);

        dump($tweet);
    }

任意のツイートをリツイートする

僕の投稿のTwitterOAuthを使ってリツイートするよをリツイートしてみる

SampleController.php
    public function sample()
    {
        // 僕の投稿の"TwitterOAuthを使ってリツイートするよ"の投稿をリツイート
        $retweet = \Twitter::post("statuses/retweet/1100242514680807424");

        dump($retweet);
    }

任意のツイートをいいね!する

僕の投稿のTwitterOAuthを使っていいね!するよをいいね!してみる

SampleController.php
    public function sample()
    {
        // 僕の投稿の"TwitterOAuthを使っていいね!するよ"の投稿をいいね!
        $favorite = \Twitter::post("favorites/create", ['id' => '1100245496159821825']);

        dump($favorite);
    }

以上!僕のTwitterのステマでした!

参考リンク

参考にさせていただきましたm(_ _)m
Twitter APIでつぶやきを取得する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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#tinker

laravelのバリデーションって豊富だ。

ルールがアレコレあってチョコっとだけ試したい時があるなぁ。
https://readouble.com/laravel/5.5/ja/validation.html#available-validation-rules

tinkerを起動したら以下の様にかく

# こうかくと
>>> 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.",
     ],
   ]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む