vitorccs / laravel-csv

PHP Laravel package to export and import CSV files in a memory-optimized way
MIT License
25 stars 5 forks source link
laravel laravel-package php8

Laravel CSV

PHP Laravel package to export and import CSV files in a memory-optimized way.

Description

Export CSV files from PHP arrays, Laravel Collections or Laravel Queries and choose to prompt the user to download the file, store it in a Laravel disk or create the file in background as a Laravel Job.

Import CSV files from Laravel Disks, local files, strings or resources and choose to retrieve the full content or in small chunks.

The memory usage is optimized in this project by using PHP streams, which places the content in a temporary file (rather than PHP thread memory) and reads/writes content one line at a time.

NOTE: This project was inspired on https://github.com/maatwebsite/Laravel-Excel which is a great project and can handle many formats (Excel, PDF, OpenOffice and CSV). But since it uses PhpSpreadsheet, it is not optimized for handling large CSV files (thousands of records) causing the PHP memory exhaustion.

Upgrading from v1.0 to v2.0

Version 2.0 adds the importing feature so the only required action is to change the importing namespace:

// v1.0 (old)
use Vitorccs\LaravelCsv\Concerns\Exportable;
use Vitorccs\LaravelCsv\Concerns\FromArray;
use Vitorccs\LaravelCsv\Concerns\FromCollection;
use Vitorccs\LaravelCsv\Concerns\FromQuery;
// v2.0 (new)
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromArray;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromCollection;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromQuery;

Requirements

Installation

Step 1) Add composer dependency

composer require vitorccs/laravel-csv

Step 2) Publish the config file

php artisan vendor:publish --provider="Vitorccs\LaravelCsv\ServiceProviders\CsvServiceProvider" --tag=config

Step 3) Edit your local config\csv.php file per your project preferences

How to Export

Step 1) Create an Export class file as shown below

Note: you may implement FromArray, FromCollection or FromQuery

namespace App\Exports;

use App\User;
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromQuery;

class UsersExport implements FromQuery
{
    use Exportable;

    public function query()
    {
        return User::query()
            ->where('created_at', '>=', '2024-01-01 00:00:00');
    }
}

Step 2) The file can now be generated by using a single line:

# prompt the client browser to download the file 
return (new UsersExport)->download('users.csv');

In case you want the file to be stored in the disk:

# will save the file in 's3' disk
return (new UsersExport)->store('users.csv', 's3');

You may also get the content as stream for better control over the output:

# will get the content in a stream (content placed in a temporary file)
return (new UsersExport)->stream();

For larger files, you may want to generate the file in background as a Laravel Job

use App\Jobs\NotifyCsvCreated;

# generate a {uuid-v4}.csv filename
$filename = CsvHelper::filename();

# will create a job to create and store the file in disk
# and afterwards notify the user
(new BillsExport())
    ->queue($filename, 's3')
    ->allOnQueue('default')
    ->chain([
        // You must create the Laravel Job below
        new NotifyCsvCreated($filename)
    ]);

Export - Data sources

Note: Only FromQuery can chunk results per chunk_size parameter from config file.

Laravel Eloquent Query Builder

namespace App\Exports;

use App\User;
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromQuery;

class MyQueryExport implements FromQuery
{
    use Exportable;

    public function query()
    {
        return User::query();
    }
}

Laravel Database Query Builder

namespace App\Exports;

use App\User;
use Illuminate\Support\Facades\DB;
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromQuery;

class MyQueryExport implements FromQuery
{
    use Exportable;

    public function query()
    {
        return DB::table('users');
    }
}

Laravel Collection

namespace App\Exports;

use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromCollection;

class MyCollectionExport implements FromCollection
{
    use Exportable;

    public function collection()
    {
        return collect([
            ['a1', 'b1', 'c1'],
            ['a2', 'b2', 'c2'],
            ['a3', 'b3', 'c3']
        ]);
    }
}

Laravel LazyCollection

namespace App\Exports;

use App\User;
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromCollection;

class MyQueryExport implements FromCollection
{
    use Exportable;

    public function collection()
    {
        return User::cursor();
    }
}

PHP Arrays

namespace App\Exports;

use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromArray;

class MyArrayExport implements FromArray
{
    use Exportable;

    public function array(): array
    {
        return [
            ['a1', 'b1', 'c1'],
            ['a2', 'b2', 'c2'],
            ['a3', 'b3', 'c3']
        ];
    }
}

How to Import

Step 1) Create an Import class file as shown below

Note: you may implement FromDisk, FromFile, FromResource or FromContents

namespace App\Exports;

use Vitorccs\LaravelCsv\Concerns\Importables\Importable;
use Vitorccs\LaravelCsv\Concerns\Importables\FromDisk;

class UsersImport implements FromDisk
{
    use Importable;

    public function disk(): ?string 
    {
        return 's3'; 
    }

    public function filename(): string
    {
        return 'users.csv';
    }
}

Step 2) The content can now be retrieved by using a single line:

# get the records in array format
return (new UsersImport)->getArray();
# in case the result is too large, you may receive small chunk of results
# at a time in your callback function, preventing memory exhaustion.
(new UsersImport)->chunkArray(function(array $rows, int $index) {
    // do something with the rows
    echo "Chunk $index has the following records:";
    print_r($rows);
});

Import - Data sources

From string

namespace App\Imports;

use Vitorccs\LaravelCsv\Concerns\Importables\Importable;
use Vitorccs\LaravelCsv\Concerns\Importables\FromContents;

class MyContents implements FromContents
{
    use Importable;

    public function contents(): string
    {
        return "A1,B1,C1\nA2,B2,C2\n,A3,B3,C3";
    }
}

From local File

namespace App\Imports;

use Vitorccs\LaravelCsv\Concerns\Importables\Importable;
use Vitorccs\LaravelCsv\Concerns\Importables\FromFile;

class MyFileImport implements FromFile
{
    use Importable;

    public function filename(): string;
    {
        return storage_path() . '/users.csv';
    }
}

From resource

namespace App\Imports;

use Vitorccs\LaravelCsv\Concerns\Importables\Importable;
use Vitorccs\LaravelCsv\Concerns\Importables\FromResource;

class MyResourceImport implements FromResource
{
    use Importable;

    public function resource()
    {
        $contents = "A1,B1,C1\nA2,B2,C2\n,A3,B3,C3";
        $resource = fopen('php://memory', 'w+');

        fputs($resource, $contents);

        return $resource;
    }
}

From Laravel Disk

namespace App\Exports;

use Vitorccs\LaravelCsv\Concerns\Importables\Importable;
use Vitorccs\LaravelCsv\Concerns\Importables\FromDisk;

class UsersImport implements FromDisk
{
    use Importable;

    public function disk(): ?string 
    {
        return 'local'; 
    }

    public function filename(): string
    {
        return 'my_imports/users.csv';
    }
}

Implementations

The implementations below work with both Export and Import mode.

Headings

Implement WithHeadings for setting a heading to the CSV file.

use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromArray;
use Vitorccs\LaravelCsv\Concerns\WithHeadings;

class UsersExport implements FromArray, WithHeadings
{
    use Exportable;

    public function headings(): array
    {
        return ['ID', 'Name', 'Email'];
    }
}

Mapping rows

Implement WithMapping if you either need to set the value of each column or apply some custom formatting.

use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromArray;
use Vitorccs\LaravelCsv\Concerns\WithMapping;

class UsersExport implements FromArray, WithMapping
{
    use Exportable;

    public function map($user): array
    {
        return [
            $user->id,
            $user->name,
            $user->email ?: 'N/A'
        ];
    }
}

Formatting columns

Implement WithColumnFormatting to format date and numeric fields.

In export mode, the Date must be either a Carbon or a Datetime object, and the number must be any kind of numeric data (numeric string, integer or float).

In import mode, the string content must match with the formatting set (e.g: yyyy-mm-dd for dates).

The formatting preferences are set in the config file csv.php.

use Carbon\Carbon;
use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromArray;
use Vitorccs\LaravelCsv\Concerns\WithColumnFormatting;
use Vitorccs\LaravelCsv\Enum\CellFormat;

class UsersExport implements FromArray, WithColumnFormatting
{
    use Exportable;

    public function array(): array
    {
        return [
            [ Carbon::now(), Carbon::now(), 2.50, 1.00 ],
            [ new DateTime(), new DateTime(), 3, 2.00 ]
        ];
    }

    public function columnFormats(): array
    {
        return [
            'A' => CellFormat::DATE,
            'B' => CellFormat::DATETIME,
            'C' => CellFormat::DECIMAL,
            'D' => CellFormat::INTEGER,
        ];
    }
}

Limiting the results

Implement the method below if you need to limit the quantity of results to be exported/imported.

use Vitorccs\LaravelCsv\Concerns\Exportables\Exportable;
use Vitorccs\LaravelCsv\Concerns\Exportables\FromQuery;

class UsersExport implements FromQuery
{
    use Exportable;

    public function limit(): ?int
    {
        return 5000;
    }
}

License

Released under the MIT License.