Skip to content

Instantly share code, notes, and snippets.

@inxilpro
Created February 13, 2024 17:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save inxilpro/5367bdf60971f78b0310e32d9b1a9579 to your computer and use it in GitHub Desktop.
Save inxilpro/5367bdf60971f78b0310e32d9b1a9579 to your computer and use it in GitHub Desktop.
Simple wrapper for OpenSpout
<?php
// Reading
CsvReader::read($path)->each(function(array $row) {
// Do something with $row
});
// Writing
return CsvWriter::for($data)->writeToHttpFile();
<?php
namespace App\Support;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use OpenSpout\Reader\CSV\Reader;
use UnexpectedValueException;
class CsvReader
{
public static function read(string $path): LazyCollection
{
return (new static($path))->collect();
}
public function __construct(
protected string $path,
) {
}
public function collect(): LazyCollection
{
return new LazyCollection(function() {
$reader = new Reader();
$reader->open($this->path);
try {
foreach ($reader->getSheetIterator() as $sheet) {
$columns = 0;
$headers = null;
foreach ($sheet->getRowIterator() as $row) {
if (null === $headers) {
$headers = array_map(Str::snake(...), $row->toArray());
$columns = count($headers);
continue;
}
$data = $row->toArray();
$data_columns = count($data);
if ($columns < $data_columns) {
throw new UnexpectedValueException("Expected {$columns} columns of data but got {$data_columns}");
}
if ($columns > $data_columns) {
$data = array_merge($data, array_fill(0, $columns - $data_columns, null));
}
yield array_combine($headers, $data);
}
}
} finally {
$reader->close();
}
});
}
}
<?php
namespace App\Support;
use Closure;
use Generator;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Http\File;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\App;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\CSV\Options;
use OpenSpout\Writer\CSV\Writer;
class CsvWriter
{
protected Closure $header_formatter;
public static function for(array|Enumerable|Generator|Builder $data): static
{
return new static($data);
}
public function __construct(
protected array|Enumerable|Generator|Builder $data,
protected bool $headers = true,
protected string $delimiter = ',',
protected string $enclosure = '"',
protected bool $bom = true,
protected bool $withoutEmptyNewLine = false,
) {
$this->header_formatter = Str::headline(...);
}
public function withoutHeaders(): static
{
$this->headers = false;
return $this;
}
public function withOriginalKeysAsHeaders(): static
{
$this->header_formatter = static fn($key) => $key;
return $this;
}
public function withDelimiter(string $delimiter): static
{
$this->delimiter = $delimiter;
return $this;
}
public function withEnclosure(string $enclosure): static
{
$this->enclosure = $enclosure;
return $this;
}
public function withoutBom(): static
{
$this->bom = false;
return $this;
}
public function withoutEmptyNewLineAtEndOfFile(): static
{
$this->withoutEmptyNewLine = true;
return $this;
}
public function writeToHttpFile(): File
{
$path = $this->writeToTemporaryFile();
return new File($path);
}
public function writeToTemporaryFile(): string
{
$path = tempnam(sys_get_temp_dir(), 'csv-writer-data');
App::terminating(fn() => @unlink($path));
return $this->write($path);
}
public function write(string $path): string
{
$options = new Options();
$options->FIELD_DELIMITER = $this->delimiter;
$options->FIELD_ENCLOSURE = $this->enclosure;
$options->SHOULD_ADD_BOM = $this->bom;
$writer = new Writer($options);
$writer->openToFile($path);
foreach ($this->rows() as $row) {
$writer->addRow(Row::fromValues($row->toArray()));
}
$writer->close();
if ($this->withoutEmptyNewLine) {
file_put_contents($path, rtrim(file_get_contents($path), PHP_EOL));
}
return $path;
}
protected function rows(): Generator
{
$source = match (true) {
$this->data instanceof Generator => LazyCollection::make($this->data),
is_array($this->data) => Collection::make($this->data),
$this->data instanceof Builder => $this->data->lazyById(),
default => $this->data,
};
$needs_headers = $this->headers;
foreach ($source as $row) {
$row = Collection::make($row);
if ($needs_headers) {
$needs_headers = false;
yield $row->keys()->map($this->header_formatter);
}
yield $row;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment