PHPackages                             ttskch/bulkony - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PDF &amp; Document Generation](/categories/documents)
4. /
5. ttskch/bulkony

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

ttskch/bulkony
==============

Easy and flexible CSV exports and imports in PHP ⚡

6.0.0(1y ago)98.0k↓34%1MITPHPPHP ^7.4 || ^8.0

Since Jan 10Pushed 1y ago2 watchersCompare

[ Source](https://github.com/ttskch/bulkony)[ Packagist](https://packagist.org/packages/ttskch/bulkony)[ RSS](/packages/ttskch-bulkony/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (3)Versions (18)Used By (0)

bulkony
=======

[](#bulkony)

[![](https://github.com/ttskch/bulkony/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/ttskch/bulkony/actions/workflows/ci.yaml?query=branch:main)[![codecov](https://camo.githubusercontent.com/58abcce84d1ce15f86b5ebf22448194c6ef42196d2b8c717ce622a2b0c6a55cb/68747470733a2f2f636f6465636f762e696f2f67682f7474736b63682f62756c6b6f6e792f67726170682f62616467652e7376673f746f6b656e3d7a775a48556247724870)](https://codecov.io/gh/ttskch/bulkony)[![Packagist Version](https://camo.githubusercontent.com/654b81b221302c8fe2b0a77f11bff5aaae5042c992c71ac52e29e28a072d1397/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7474736b63682f62756c6b6f6e793f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ttskch/bulkony)[![Packagist Downloads](https://camo.githubusercontent.com/831d7f4b4535cb63e70b612839f8ca28d81f05b545a6e0f43f7f513d5c5a1898/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f7474736b63682f62756c6b6f6e793f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ttskch/bulkony)

Easy and flexible CSV exports and imports in PHP ⚡

```
use Ttskch\Bulkony\Import\Importer;

$importer = new Importer();
$rowVisitor = new App\ValidatableRowVisitor();

$importer->import('/path/to/input.csv', $rowVisitor);

if ($importer->getErrorListCollection()->isEmpty()) {
    echo "Successfully imported!\n";
}
```

TOC
---

[](#toc)

Details- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
    - [Export](#export)
        - [Export to file](#export-to-file)
        - [Send HTTP response in WAF way](#send-http-response-in-waf-way)
            - [Symfony](#symfony)
            - [Laravel](#laravel)
            - [CakePHP](#cakephp)
    - [Import](#import)
        - [With validation](#with-validation)
        - [With previewing feature](#with-previewing-feature)
        - [With preprocessing](#with-preprocessing)
- [Getting involved](#getting-involved)

Features
--------

[](#features)

- Multibyte support
- MS Excel friendly (exports as UTF-8 CSV with BOM)
- Memory efficient (unless you import non UTF-8 CSV)
- Easy to validate row by row
- Easy to implement preview feature, that shows which cell will be changed after importing

Requirements
------------

[](#requirements)

- PHP &gt;= 7.4
- ext-mbstring

Installation
------------

[](#installation)

```
$ composer require ttskch/bulkony
```

Usage
-----

[](#usage)

### Export

[](#export)

```
use Ttskch\Bulkony\Export\Exporter;

$exporter = new Exporter();
$rowGenerator = new App\UserRowGenerator();

$exporter->exportAndOutput('users.csv', $rowGenerator); // send HTTP response for downloading
```

```
namespace App;

use Ttskch\Bulkony\Export\RowGenerator\RowGeneratorInterface;

class UserRowGenerator implements RowGeneratorInterface
{
    public function __construct(private $userRepository)
    {
    }

    public function getHeadingRows(): array
    {
        // return 2D array so that you can export multiple header rows
        return [['id', 'name', 'email']];
    }

    public function getBodyRowsIterator(): iterable
    {
        while ($user = $this->userRepository->findNext()) {
            // yield 2D array so that you can export multiple rows for one data
            yield [
                [$user->getId(), $user->getName(), $user->getEmail()],
            ];
        }
    }
}
```

#### Export to file

[](#export-to-file)

```
use Ttskch\Bulkony\Export\Exporter;

$exporter = new Exporter();
$rowGenerator = new App\UserRowGenerator();

$exporter->export('/path/to/output.csv', $rowGenerator);
```

#### Send HTTP response in WAF way

[](#send-http-response-in-waf-way)

##### Symfony

[](#symfony)

```
$response = new StreamedResponse();
$response->headers->set('Content-Type', 'text/csv');
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, 'users.csv'));
$response->setCallback(function () use ($exporter, $rowGenerator) {
    $exporter->export('php://output', $rowGenerator);
});

return $response->send();
```

##### Laravel

[](#laravel)

```
return response()
    ->header('Content-Type', 'text/csv')
    ->streamDownload(function () use ($exporter, $rowGenerator) {
        $exporter->export('php://output', $rowGenerator);
    }, 'users.csv');
```

##### CakePHP

[](#cakephp)

```
$stream = new CallbackStream(function () use ($exporter, $rowGenerator) {
    $exporter->export('php://output', $rowGenerator);
});

return $response
    ->withType('csv')
    ->withDownload('users.csv')
    ->withBody($stream);
```

### Import

[](#import)

```
use Ttskch\Bulkony\Import\Importer;

$importer = new Importer();
$rowVisitor = new App\UserRowVisitor();

$importer->import('/path/to/input.csv', $rowVisitor);
```

```
namespace App;

use Ttskch\Bulkony\Import\RowVisitor\Context;
use Ttskch\Bulkony\Import\RowVisitor\RowVisitorInterface;

class UserRowVisitor implements RowVisitorInterface
{
    public function __constructor(private $userRepository)
    {
    }

    public function import(array $csvRow, int $csvLineNumber, Context $context): void
    {
        $this->userRepository->persist($this->hydrate($csvRow));
    }

    private function hydrate(array $csvRow): App\User
    {
        // create App\User instance from csv row in some way
        return new App\User($csvRow);
    }
}
```

#### With validation

[](#with-validation)

```
use Ttskch\Bulkony\Import\Importer;

$importer = new Importer();
$rowVisitor = new App\UserRowVisitor();

$importer->import('/path/to/input.csv', $rowVisitor);

if ($importer->getErrorListCollection()->isEmpty()) {
    echo "Successfully imported!\n";
} else {
    // you can access to validation errors by csv line number and column (heading) name
    // in other words,
    //   ErrorListCollection : errors in whole csv file
    //   ErrorList           : errors in one csv row
    //   Error               : errors in one csv cell (can contain multiple error messages)
    foreach ($importer->getErrorListCollection() as $errorList) {
        foreach ($errorList as $error) {
            foreach ($error->getMessages() as $message) {
                echo sprintf("Error: row %d col `%s`: %s\n", $errorList->getCsvLineNumber(), $error->getCsvHeading(), $message);
            }
        }
    }
}
```

```
namespace App;

use Ttskch\Bulkony\Import\RowVisitor\Context;
use Ttskch\Bulkony\Import\RowVisitor\ValidatableRowVisitorInterface;
use Ttskch\Bulkony\Import\Validation\ErrorList;

class UserRowVisitor implements ValidatableRowVisitorInterface
{
    public function __constructor(private $userRepository, private $validator)
    {
    }

    public function import(array $csvRow, int $csvLineNumber, Context $context): void
    {
        $this->userRepository->persist($this->hydrate($csvRow));
    }

    public function validate(array $csvRow, int $csvLineNumber, ErrorList $errorList, Context $context): void
    {
        $user = $this->hydrate($csvRow);

        foreach ($this->validator->validate($user) as $validationError) {
            // get csv heading name from validation error in some way
            $csvHeading = $this->getCsvHeadingFromValidationError($validationError);

            // upsert Error into ErrorList
            $errorList->get($csvHeading, true)->addMessage($validationError->getMessage());
        }
    }

    public function onError(array $csvRow, int $csvLineNumber,  ErrorList $errorList, Context $context): bool
    {
        // you can log errors for one csv row or do something here...

        // you can choose continue or abort on error occurred
        return ValidatableRowVisitorInterface::CONTINUE_ON_ERROR;
        // return ValidatableRowVisitorInterface::ABORT_ON_ERROR;
    }

    private function hydrate(array $csvRow): App\User
    {
        // create App\User instance from csv row in some way
        return new App\User($csvRow);
    }
}
```

In this example, you may find that `$this->hydrate($csvRow)` is called twice in `validate()` and `import()`. Sometimes this is not good.

If cost of hydrating object from csv row is very high, you can pass the hydrated object through `Context` like below.

```
public function import(array $csvRow, int $csvLineNumber, Context $context): void
{
    // get hydrated $user
    $user = $context['user'];

    $this->userRepository->persist($user);
}

public function validate(array $csvRow, int $csvLineNumber, ErrorList $errorList, Context $context): void
{
    $user = $this->hydrate($csvRow);

    // pass hydrated $user
    $context['user'] = $user;

    // validate $user ...
}
```

#### With previewing feature

[](#with-previewing-feature)

```
use Ttskch\Bulkony\Import\Importer;
use Ttskch\Bulkony\Import\Preview\Preview;

$importer = new Importer();
$rowVisitor = new App\UserRowVisitor();

/** @var Preview $preview */
$preview = $importer->preview('/path/to/input.csv', $rowVisitor);

// $preview contains whole csv data and knows WHICH CELL WILL BE CHANGED after importing
render('some/template', [
    'preview' => $preview,
]);
```

```
namespace App;

use Ttskch\Bulkony\Import\Preview\Row;
use Ttskch\Bulkony\Import\RowVisitor\Context;
use Ttskch\Bulkony\Import\RowVisitor\PreviewableRowVisitorInterface;

class UserRowVisitor implements PreviewableRowVisitorInterface
{
    // ...

    public function preview(array $csvRow, int $csvLineNumber, Row $previewRow, Context $context): void
    {
        $originalUser = $this->repository->find($csvRow['id']);
        $importedUser = $this->hydrate($csvRow);

        if ($originalUser->name !== $importedUser->name) {
            $previewRow->get('name')->setChanged();
        }

        if ($originalUser->email !== $importedUser->email) {
            $previewRow->get('email')->setChanged();
        }
    }
}
```

Of course you can implement previewing feature with validation.

In this example, if `App\UserRowVisitor` implements `ValidatableRowVisitorInterface`, `$preview` holds whole validation errors automatically.

#### With preprocessing

[](#with-preprocessing)

You can preprocess all csv rows before importing and previewing and store some results in `Context`.

```
namespace App;

use Ttskch\Bulkony\Import\Preview\Row;
use Ttskch\Bulkony\Import\RowVisitor\Context;
use Ttskch\Bulkony\Import\RowVisitor\PreprocessableRowVisitorInterface;
use Ttskch\Bulkony\Import\RowVisitor\PreviewableRowVisitorInterface;

class UserRowVisitor implements PreprocessableRowVisitorInterface, PreviewableRowVisitorInterface
{
    // ...

    public function preprocess(array $csvRow, int $csvLineNumber, Context $context): void
    {
        $originalUser = $this->repository->find($csvRow['id']);
        $importedUser = $this->hydrate($csvRow);

        $context[sprintf('originalUser%d', $csvLineNumber)] = $originalUser;
        $context[sprintf('importedUser%d', $csvLineNumber)] = $importedUser;
    }

    public function import(array $csvRow, int $csvLineNumber, Context $context): void
    {
        $user = $context[sprintf('originalUser%d', $csvLineNumber)];

        $this->userRepository->persist($user);
    }

    public function preview(array $csvRow, int $csvLineNumber, Row $previewRow, Context $context): void
    {
        $originalUser = $context[sprintf('originalUser%d', $csvLineNumber)];
        $importedUser = $context[sprintf('importedUser%d', $csvLineNumber)];

        if ($originalUser->name !== $importedUser->name) {
            $previewRow->get('name')->setChanged();
        }

        if ($originalUser->email !== $importedUser->email) {
            $previewRow->get('email')->setChanged();
        }
    }
}
```

For example, if the entity to be imported has a self-referential property and validation is required based on the member state of that property, it is necessary to pre-hydrate the entities corresponding to all rows in the CSV. This feature is useful in such cases.

Getting involved
----------------

[](#getting-involved)

```
$ composer install
$ composer bin tools install

# Develop...

$ composer tests
```

###  Health Score

40

—

FairBetter than 88% of packages

Maintenance37

Infrequent updates — may be unmaintained

Popularity31

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 98.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~86 days

Recently: every ~205 days

Total

17

Last Release

567d ago

Major Versions

1.2.0 → 2.0.02021-03-05

2.0.0 → 3.0.02021-03-12

3.4.1 → 4.0.02022-04-13

4.0.0 → 5.0.02022-08-01

5.1.3 → 6.0.02024-10-28

PHP version history (2 changes)1.0.0PHP ^7.2 || ^8.0

5.0.0PHP ^7.4 || ^8.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/4360663?v=4)[Takashi Kanemoto](/maintainers/ttskch)[@ttskch](https://github.com/ttskch)

---

Top Contributors

[![ttskch](https://avatars.githubusercontent.com/u/4360663?v=4)](https://github.com/ttskch "ttskch (54 commits)")[![polidog](https://avatars.githubusercontent.com/u/284778?v=4)](https://github.com/polidog "polidog (1 commits)")

---

Tags

bulkcsvcsv-exportcsv-importexcelphpexcelcsvcsv-exportbulkcsv-import

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ttskch-bulkony/health.svg)

```
[![Health](https://phpackages.com/badges/ttskch-bulkony/health.svg)](https://phpackages.com/packages/ttskch-bulkony)
```

###  Alternatives

[maatwebsite/excel

Supercharged Excel exports and imports in Laravel

12.7k144.3M712](/packages/maatwebsite-excel)[rap2hpoutre/fast-excel

Fast Excel import/export for Laravel

2.3k24.9M47](/packages/rap2hpoutre-fast-excel)[openspout/openspout

PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way

1.2k57.6M131](/packages/openspout-openspout)[gotenberg/gotenberg-php

A PHP client for interacting with Gotenberg, a developer-friendly API for converting numerous document formats into PDF files, and more!

3685.2M19](/packages/gotenberg-gotenberg-php)[nuovo/spreadsheet-reader

Spreadsheet reader library for Excel, OpenOffice and structured text files

669863.2k8](/packages/nuovo-spreadsheet-reader)[faisalman/simple-excel-php

Easily parse / convert / write between Microsoft Excel XML / CSV / TSV / HTML / JSON / etc formats

582599.4k1](/packages/faisalman-simple-excel-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
