Convert a Collection to a downloadable CSV

• 1 min read

Did you know that you can pack the content of a CSV directly into a <a>-tag's href-attribute? This is a technique I recently applied at work™ for a statistics feature. The application displays the data as a table and does some very simple calculations. This is enough for 95% of our users, but not enough for the rest. I've thought it would be great if they could just download the data set they already see and import it into Excel, Numbers or any other application and use the data further.

I've solved this with a new toInlineCsv-macro on Laravel's Collection class. Add the following macro to the boot-method in your ApplicationServiceProvider:

Collection::macro('toInlineCsv', function (array $headers) {
    $csvString = $this->map(function($value, $key) {
            return is_array($value) ? implode(',', $value) : implode(',', [$key, $value]);
        })
        ->prepend(implode(',', $headers))
        ->implode("\n");

    $encodedCsvString = strtr(
        rawurlencode($csvString),
        ['%21'=>'!', '%2A'=>'*', '%27'=>"'", '%28'=>'(', '%29'=>')']
    );

    return 'data:attachment/csv;charset=utf-8,' . $encodedCsvString;
});

(The code which generates the $encodedCsvString is the PHP equivalent to the JavaScript encodeURIComponent function.)

Let's take the following very simple collection as an example:

$sales = collect([
    'Q1' => 234621.70,
    'Q2' => 984239.50,
    'Q3' => 223764.10,
    'Q4' => 934793.30
]);

Within your Blade view call the toInlineCsv method on the collection. Pass the Headers you want to see in the CSV as an argument.

<a download="sales.csv" href="{{ $sales->toInlineCsv(['Quarter', 'Revenue']) }}">Download</a>

The macro also works well with more complex collections:

$games = collect([
    [
        'title' => 'Super Mario 64',
        'release_year' => 1996,
        'meta_score' => 94
    ],
    [
        'title' => 'Assassin’s Creed IV: Black Flag',
        'release_year' => 2013,
        'meta_score' => 83
    ],
    [
        'title' => 'Red Dead Redemption 2',
        'release_year' => 2018,
        'meta_score' => 97
    ]
]);

Usage in Blade is exactly the same.

<a download="games.csv"
    href="{{ $games->toInlineCsv(['Title', 'Release Year', 'Meta Score']) }}"
>Download</a>

I like this approach of providing users with a CSV download, as it uses the same data set which is already used in the rest of the view. I don't have to write a separate Route or Controller and the user doesn't have to make another request to the app and maybe wait until the data set has been generated.