猫でもわかるWeb開発・プログラミング

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

Laravel の response()->download() で Call to undefined method BinaryFileResponse::withHeaders()【PHP】

現象

Laravel 11 で、ファイルダウンロードを実装したく、 response()->download() を使った

実装

<?php

use App\Http\Controllers\Controller;

class HogeController extends Controller

    public function getCsv(Request $request)
    {
        $filepath = '/tmp/hoge.csv';
        $filepath = 'hoge.csv';
        return response()->download($filepath, $filepath);
    }
}

するとこのようなエラーが発生した

Call to undefined method Symfony\Component\HttpFoundation\BinaryFileResponse::withHeaders()

原因

エラー時のスタックトレースを見てみると、自分が実装した middleware でこのエラーが発生していた。

この middleware では、 $response->withHeaders() を使って、全てのレスポンスに特定のヘッダーを付与する middleware だったが、

response()->downoload() で返される Symfony\Component\HttpFoundation\BinaryFileResponse には withHeaders() メソッドが存在しないため、middleware でエラーになっていた。

withHeaders() メソッドについて

withHeaders() メソッドは Illuminate\Http\Response クラスに実装されているため、一見 middleware を通る全ての Response に対して使えそうだが、特定の場合はそうではない。

今回の response()->downoload()Symfony\Component\HttpFoundation\BinaryFileResponse を返すが、Laravel の response クラスの継承関係は以下のようになっており、 BinaryFileResponseIlluminate\Http\Response を継承していないので、 withHeaders() は使えない。

  • Symfony\Component\HttpFoundation\SymfonyResponse
    • Symfony\Component\HttpFoundation\BinaryFileResponse
    • Symfony\Component\HttpFoundation\StreamedResponse
    • Illuminate\Http\Response

このように、いくつかのレスポンス(主にファイルダウンロード周り)は Illuminate\Http\Response を継承しておらず、

  • Symfony\Component\HttpFoundation\BinaryFileResponse
    • response()->download()
    • response()->file()
  • Symfony\Component\HttpFoundation\StreamedResponse
    • response()->stream()
    • response()->streamDownload()

これらの reponse 系のメソッドを使った場合、 Illuminate\Http\Response が来ないので注意しておく必要がある。

参考として、Laravel 公式ドキュメントのレスポンスの部分を置いておく

laravel.com

対応

いくつかの方法があるが、一番素直なのは、 Symfony\Component\HttpFoundation\SymfonyResponse に対応できるようにしておくこと。

$response->withHeaders() の代わりに、

$response->headers->set('key', 'value')

を使えば、 SymfonyResponse 全体に対応できる。

その他の対応

  • middleware で withHeaders する際に $response instanceof Illuminate\Http\Response をチェックしておく
  • Route に withoutMiddleware をつけることで、指定した middleware を除外することができるため、 download などのレスポンスを返す場合はこの機能を利用する

しかし、どちらも場当たり的な実装なので推奨はできない。 SymfonyResponse が来た場合にも対応できるようにしておくほうが、王道な気がする。

特に、withoutMiddleware については、今後新たに middleware を追加した場合に、withoutMiddleware に追加し忘れるリスクがあるため、これを利用するのであれば middleware 適用のルールを根本的に見直した方がよさそう(全てのルートに対して適用させる middleware を可能な限り設定しないようにする、等)。