がんばるぞ

がんばります

LaravelでRoutingしてるのに404エラーが返ってくる

Routingしてるのに404エラーが帰って来た

こんな感じで定義してたんですが

<?php
Route::patch('/user/{user-id}, 'UsersController@update');

どうやら
{user-id} という書き方はダメみたい。
{user_id} に変更したら普通に動きました。

Laravelのバグ発見できたかと思って一瞬喜んだけど僕が悪いだけだった

公式ドキュメントにも書いてあった

ルートパラメータは、いつも{}括弧で囲み、アルファベット文字で構成してください。
ルートパラメータには、ハイフン(-)を使えません。下線(_)を代わりに使用してください。
ルートパラメータは、ルートコールバック/コントローラへ順番通りに注入されます。
コールバック/コントローラ引数の名前は考慮されません。

【読んだ】PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応

最近出たLaravel本を読み終わったので感想文です。

どんなことが書いてあるのか

  • Laravelの内部処理の解説
  • Laravelの内部処理を独自実装に変える方法
  • アプリケーションの設計
  • TDDも軽く

Laravelを触ったことない人を置いてけぼりにした内容なおかげでかなり実践的な内容がたくさん書かれてます。笑

特に設計の話はCQRSやらRepositoryパターンやらレイヤードアーキテクチャやらの話が出てくるので、Laravelに限らずどんなアプリケーションでも役に立つ内容でした。

この本を読むのにおすすめの人

  • Laravelに慣れてきた人
  • コントローラーに処理をベタ書きしてる人
  • Laraveのソースコードを読んだことがない人

サービスコンテナ?なにそれ?
認証系はEloquentでしか使えないっしょ!みたいな人は読むと非常に学びがあると思います。

おすすめしない人

  • Laravelを触ったことがない人

Laravelを触ったことがないと全くついていけないと思いますので、この本買う前にLaravelでアプリケーションを1回作ってから読んだ方がいいと思います。

感想

Laravelに慣れてきた人とかにめちゃくちゃ役に立つと思います。おすすめです。

特に認証をEloquent以外で使いたいとかは絶対に出てくる要件なんで、ここらへん解説してくれてるのはすごくありがたいですね

あと最後のTDDも、TDD未経験者にはめちゃくちゃとっつきやすい内容になってて良いですね。
せっかくなので写経しときました laravel-socym-tdd-sample

おわり

PHP勉強会#129でLTしてきた

PHP勉強会でLTしてきました。

DDDについて1ヶ月ほど勉強したので、それを5分にまとめるという苦行をしてきました。

DDDを5分でまとめるのはそもそも無理があるため、かなり大雑把な説明になってしまったので
今度は20分枠で発表したいなあという感じです。

LaravelでCSVファイルをダウンロードさせる

業務案件で毎回と言っていいほど機能として追加するのが、CSVのダウンロード機能ではないでしょうか。

ただCSVをダウンロードするだけですが、適当にやってしまうと文字化けやメモリリークなど、痛い思いをすることになります。

そこで、CSVファイルダウンロード機能の実装方法を考えてみることにしました。  
まずは単純なコードです。

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Response;
use App\User;

class HomeController extends Controller
{
    public function download()
    {
        $stream = fopen('php://output', 'w');
        fputcsv($stream, ['氏名', '住所', 'メールアドレス']);

        $users = User::all();
        foreach ($users as $user) {
            fputcsv($stream, [$user->user_name, $user->address, $user->email]);
        }

        $headers = [
            'Content-Type' => 'text/csv',
            'Content-Disposition' => 'attachment; filename="users.csv"'
        ];
        return Response::make(
            stream_get_contents($stream),
            200,
            $headers
        );
    }
}

このコードにはいくつか問題が含まれています。

まず最初に、メモリのことを考慮していません。
試しに10万レコードほどテストデータを作成したところ、160MB近くメモリーを消費していました。

次に、CSVファイルはExcelで開くパターンが多いにも関わらず、文字コードUTF-8のままです。 これではExcelで開いた場合に文字化けしてしまいます。

最後に、Httpヘッダーの作成までに時間がたくさんかかってしまった場合、そもそもファイルのダウンロードに失敗してしまいます。

これは、ブラウザがしばらくレスポンスを待ってもHttpヘッダーが返ってこない場合、デフォルトのヘッダーを使ってブラウザにデータを吐き出してしまうという仕様によるものです。

具体的には以下のような画面になります。

CSVファイルのダウンロードに失敗した図

では、これらの問題を解決するにはどうすればいいでしょうか?

メモリに優しい処理にするためには、一度に全てのデータをDBから取得するのではなく、LIMITとOFFSETを適切に使用し、分割して取得する必要があります。

CSVファイルをExcelで開く場合、ファイルの先頭にUTF-8であることを示すBOMを付与すれば文字コードを変換することなくExcelで閲覧することが可能になります。 Httpヘッダーより前にデータを出力しないようにすることで、上記の画像のような不具合を防ぐことができます。

以上を踏まえたコードが以下になります。

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\User;

class HomeController extends Controller
{
    public function download()
    {
        // コールバック内でデータを出力することで、Httpヘッダを先に出力することが出来る
        return  new StreamedResponse(
            function () {
                $stream = fopen('php://output', 'w');
                // ExcelでUTF-8と認識させるためにBOMを付ける
                fwrite($stream, '\xEF\xBB\xBF');

                fputcsv($stream, ['氏名', '住所', 'メールアドレス']);
                // 1000レコードずつ取得することで、データ量が多くなってもメモリの消費量は一定になる
                User::chunk(
                    1000,
                    function($users) use ($stream) {
                        foreach ($users as $user) {
                            fputcsv($stream, [$user->user_name, $user->address, $user->email]);
                        }
                    }
                );
                fclose($stream);
            },
            200,
            [
                'Content-Type' => 'text/csv',
                'Content-Disposition' => 'attachment; filename="users.csv"'
            ]
        );
    }
}

これで、前述の問題は全て解決すると思います。
ちなみに、メモリー消費量は1MB程度まで落ち着きました。やったね。