Adding CSV Responses to Laravel Using Macros

Aug 3, 2018 laravel php
This post is more than 18 months old. Since technology changes too rapidly, this content may be out of date (but that's not always the case). Please remember to verify any technical or programming information with the current release.

Laravel has a lot of the most common functionality built into the framework. However, decisions need to be made to balance the needs of the majority of use cases with the stability and agility that programmers need. No one really wants a bloated library. Because of this, you might find that you need functionality that is not directly built into Laravel. When I started working with Laravel-based CSV responses, this was the case. (This article is based on Laravel 5.6.)

The Problem

I’ve been building out API endpoints to handle JSON responses using the built-in method Request::wantsJson and the response type JsonResponse like this:

if ($request->wantsJson()) {
  return response()->json($myPayload);
}

Now I need to build some reporting-based endpoints that respond to requests for CSV data. A quick look at the response() global helper and underlying Response facade shows methods like json(), jsonp(), streamDownload() and more. However, I don’t see something that appears to expose a CSV-based response type using the simplicity of my JSON method.

To keep my code consistent, I want to write the CSV response the same as the JSON one. I imagine I’ll write something like this:

if ($request->wantsCsv()) {
  return response()->csv($myPayload);
}

The Solution

Luckily, the Laravel maintainers have implemented a way for us to extend built-in Laravel functionality using macros. You can find documentation for macros on the Response in the official Laravel documentation. By examining the code, I found out that Request also supports this functionality.

I decided to add my CSV functionality as a macro to both the request and response objects using my app service provider. I’m using The League CSV to easily handle my CSV response (if you’ve dealt with different line-endings, encodings, complicated escape sequences and found yourself spending tons of time, wise up sooner than I have and use this library instead).

Add the following to the boot() method of the AppServiceProvider.php file.

Response::macro('csv', function (array $value, int $status = 200) 
{
  if (empty($value)) throw new \DomainException('The CSV document must have content.');

  $csv = Writer::createFromFileObject(new \SplTempFileObject());

  $headers = array_keys($value[0]);
  $csv->insertOne($headers);
  $csv->insertAll($value);

  return Response::make($csv->getContent(), $status)
    ->withHeaders(['Content-Type' => 'text/csv']);
});

Request::macro('wantsCsv', function () {
  $acceptable = $this->getAcceptableContentTypes();
  return isset($acceptable[0]) && Str::contains($acceptable[0], ['/csv', '+csv']);
});

First, I’m adding the method csv as a macro using the Response facade which will proxy it forward to the \Illuminate\Http\Response class. Then, I do a bit of housekeeping. In my use case, I’ve decided that this response should always have at least one row of associative array data. (I won’t be calling this end point if there is no data to return; your use case may be different). If there is not a first row, I’ll throw an exception.

After that, it’s time to use The League’s CSV writer and create a CSV document (your implementation of this might require additional configuration, so make sure to refer to the documentation for more help). Then, retrieve the array keys of the first row to generate the first row of the CSV, the header row. Then, complete the document by inserting all remaining data.

Finally, I create a response using the Response facade with the content of the CSV, my status code and the headers to support my content type of text/csv.

The second section of code adds a method of wantsCsv as a macro using the Request facade which will proxy it forward to the \Illuminate\Http\Request class. This functionality has been shamelessly copied from the wantsJson() method, but altered to check for CSV. (I can’t claim I’ve done anything original or interesting here).

Now, I’m free to use the request wantsCsv() and the csv() response methods in my project.

Final Notes

As with any custom implementation, there are ways that this code works for me but may not work for you. (For example, my CSV response doesn’t handle empty documents or additional headers.) The important part here is to understand that you can add functionality into classes like Request and Response using macros. For more insight into the macro functionality, I suggest checking out the Macroable trait that Laravel provides.

One note of caution: the more you use “magic” like the macro functionality, the harder it is for other people to follow your code. IDE’s won’t be able to parse your new functionality automatically, so you’ll have to up your game at writing proper PHPDoc annotations. I recommend making use of the @method annotation if you’re using Macroable on your own classes. You can find the documentation for that PHPDoc annotation here.

Looking for more Laravel Tips & Tricks? Join Joel and I on the No Compromises bi-weekly podcast; around 15 minutes of thoughtful real-world advice and helpful info.
Go to All Posts