ttskch / bulkony
PHP中简单灵活的CSV导出和导入 ⚡
5.1.2
2023-08-17 14:23 UTC
Requires
- php: ^7.4 || ^8.0
- ext-mbstring: *
- league/csv: ^9.8
Requires (Dev)
- ext-json: *
- bamarni/composer-bin-plugin: ^1.8
- phpunit/phpunit: ^9.5
README
PHP中简单灵活的CSV导出和导入 ⚡
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"; }
目录
功能
- 多字节支持
- MS Excel兼容(导出为带BOM的UTF-8 CSV)
- 内存效率高(除非您导入非UTF-8 CSV)
- 逐行验证简单
- 实现预览功能简单,该功能显示导入后哪些单元格将被更改
要求
- PHP >= 7.4
- ext-mbstring
安装
$ composer require ttskch/bulkony
使用
导出
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()], ]; } } }
导出到文件
use Ttskch\Bulkony\Export\Exporter; $exporter = new Exporter(); $rowGenerator = new App\UserRowGenerator(); $exporter->export('/path/to/output.csv', $rowGenerator);
以WAF方式发送HTTP响应
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
return response() ->header('Content-Type', 'text/csv') ->streamDownload(function () use ($exporter, $rowGenerator) { $exporter->export('php://output', $rowGenerator); }, 'users.csv');
CakePHP
$stream = new CallbackStream(function () use ($exporter, $rowGenerator) { $exporter->export('php://output', $rowGenerator); }); return $response ->withType('csv') ->withDownload('users.csv') ->withBody($stream);
导入
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); } }
带验证功能
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, 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, 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); } }
在这个例子中,您可能会发现validate()
和import()
中两次调用了$this->hydrate($csvRow)
。有时这并不好。
如果从CSV行中初始化对象的成本非常高,您可以像下面这样通过Context
传递初始化后的对象。
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, ErrorList $errorList, Context $context): void { $user = $this->hydrate($csvRow); // pass hydrated $user $context['user'] = $user; // validate $user ... }
带预览功能
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\RowVisitorInterface; class UserRowVisitor implements RowVisitorInterface { // ... public function preview(array $csvRow, 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(); } } }
当然,您可以使用验证实现预览功能。
在这个例子中,如果App\UserRowVisitor
实现了ValidatableRowVisitorInterface
,则$preview
会自动持有所有验证错误。
参与其中
$ composer install
$ composer bin tools install
# Develop...
$ composer tests