├── art └── linen.png ├── composer.json ├── config └── linen.php ├── ide.json └── src ├── CsvReader.php ├── CsvWriter.php ├── ExcelReader.php ├── ExcelWriter.php ├── Facades ├── .gitkeep └── Linen.php ├── Reader.php ├── Support └── FileTypeHelper.php └── Writer.php /art/linen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glhd/linen/c1eab3da4278cd7dc480946842ab514475ea3617/art/linen.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glhd/linen", 3 | "description": "", 4 | "keywords": [ 5 | "laravel" 6 | ], 7 | "authors": [ 8 | { 9 | "name": "Chris Morrell", 10 | "homepage": "http://www.cmorrell.com" 11 | } 12 | ], 13 | "type": "library", 14 | "license": "MIT", 15 | "require": { 16 | "illuminate/support": "^10|^11|dev-master", 17 | "ext-json": "*", 18 | "openspout/openspout": "^4.24" 19 | }, 20 | "require-dev": { 21 | "orchestra/testbench": "^8|^9|10.x-dev|dev-master", 22 | "friendsofphp/php-cs-fixer": "^3.34", 23 | "mockery/mockery": "^1.6", 24 | "phpunit/phpunit": "^10.5" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Glhd\\Linen\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "classmap": [ 33 | "tests/TestCase.php" 34 | ], 35 | "psr-4": { 36 | "Glhd\\Linen\\Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "fix-style": "vendor/bin/php-cs-fixer fix", 41 | "check-style": "vendor/bin/php-cs-fixer fix --diff --dry-run" 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [] 46 | } 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true 50 | } 51 | -------------------------------------------------------------------------------- /config/linen.php: -------------------------------------------------------------------------------- 1 | getValue(); 18 | 19 | return match (true) { 20 | is_numeric($value) => (float) $value == (int) $value ? (int) $value : (float) $value, 21 | '' === $value => null, 22 | default => $value, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CsvWriter.php: -------------------------------------------------------------------------------- 1 | delimiter = $delimiter; 21 | 22 | return $this; 23 | } 24 | 25 | public function withEnclosure(string $enclosure): static 26 | { 27 | $this->enclosure = $enclosure; 28 | 29 | return $this; 30 | } 31 | 32 | public function withoutBom(): static 33 | { 34 | $this->bom = false; 35 | 36 | return $this; 37 | } 38 | 39 | public function withEmptyNewLineAtEndOfFile(): static 40 | { 41 | $this->empty_new_line = true; 42 | 43 | return $this; 44 | } 45 | 46 | public function withoutEmptyNewLineAtEndOfFile(): static 47 | { 48 | $this->empty_new_line = false; 49 | 50 | return $this; 51 | } 52 | 53 | public function write(string $path): string 54 | { 55 | parent::write($path); 56 | 57 | if (! $this->empty_new_line) { 58 | file_put_contents($path, rtrim(file_get_contents($path), PHP_EOL)); 59 | } 60 | 61 | return $path; 62 | } 63 | 64 | protected function writer(): WriterInterface 65 | { 66 | $options = new OpenSpout\Options(); 67 | $options->FIELD_DELIMITER = $this->delimiter; 68 | $options->FIELD_ENCLOSURE = $this->enclosure; 69 | $options->SHOULD_ADD_BOM = $this->bom; 70 | 71 | return new OpenSpout\Writer($options); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ExcelReader.php: -------------------------------------------------------------------------------- 1 | getValue()); 20 | } 21 | 22 | if ($cell instanceof Cell\EmptyCell) { 23 | return null; 24 | } 25 | 26 | return parent::castCell($cell); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ExcelWriter.php: -------------------------------------------------------------------------------- 1 | */ 16 | abstract class Reader implements IteratorAggregate 17 | { 18 | public static function from(string $path): static 19 | { 20 | return new static($path); 21 | } 22 | 23 | public static function read(string $path): LazyCollection 24 | { 25 | return static::from($path)->collect(); 26 | } 27 | 28 | public function __construct( 29 | protected string $path, 30 | ) { 31 | } 32 | 33 | public function getIterator(): Traversable 34 | { 35 | return $this->collect(); 36 | } 37 | 38 | public function collect(): LazyCollection 39 | { 40 | return new LazyCollection(function() { 41 | $reader = $this->reader(); 42 | $reader->open($this->path); 43 | 44 | try { 45 | foreach ($reader->getSheetIterator() as $sheet) { 46 | $columns = 0; 47 | $keys = null; 48 | 49 | foreach ($sheet->getRowIterator() as $row) { 50 | /** @var \OpenSpout\Common\Entity\Row $row */ 51 | if (null === $keys) { 52 | $keys = array_map($this->headerToKey(...), $row->toArray()); 53 | $columns = count($keys); 54 | continue; 55 | } 56 | 57 | $data = $this->castRow($row); 58 | $data_columns = count($data); 59 | 60 | if ($columns < $data_columns) { 61 | foreach (range(1, $data_columns) as $index => $column) { 62 | $keys[$index] ??= "column{$column}"; 63 | } 64 | $columns = count($keys); 65 | } 66 | 67 | if ($columns > $data_columns) { 68 | $data = array_merge($data, array_fill(0, $columns - $data_columns, null)); 69 | } 70 | 71 | yield Collection::make(array_combine($keys, $data)); 72 | } 73 | } 74 | } finally { 75 | $reader->close(); 76 | } 77 | }); 78 | } 79 | 80 | abstract protected function reader(): ReaderInterface; 81 | 82 | protected function castRow(Row $data): array 83 | { 84 | return array_map($this->castCell(...), $data->getCells()); 85 | } 86 | 87 | protected function castCell(Cell $cell): mixed 88 | { 89 | return $cell->getValue(); 90 | } 91 | 92 | protected function headerToKey(string $value): string 93 | { 94 | return Str::snake(strtolower($value)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Support/FileTypeHelper.php: -------------------------------------------------------------------------------- 1 | guessMimeType($path); 21 | 22 | return match ($mime) { 23 | 'application/msexcel', 24 | 'application/x-msexcel', 25 | 'zz-application/zz-winassoc-xls', 26 | 'application/vnd.ms-excel', 27 | 'application/vnd.ms-excel.sheet.binary.macroenabled.12', 28 | 'application/vnd.ms-excel.sheet.macroenabled.12', 29 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 30 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => ExcelReader::from($path), 31 | 'application/csv', 32 | 'text/csv', 33 | 'text/csv-schema', 34 | 'text/x-comma-separated-values', 35 | 'text/x-csv', 36 | 'text/plain' => CsvReader::from($path), 37 | default => throw new InvalidArgumentException("Unable to infer file type for '{$path}'"), 38 | }; 39 | } 40 | 41 | public function write(array|Enumerable|Generator|Builder $data, string $path): string 42 | { 43 | $extension = pathinfo($path, PATHINFO_EXTENSION); 44 | 45 | $writer = match ($extension) { 46 | 'xlsx', 'xls' => ExcelWriter::for($data), 47 | 'csv' => CsvWriter::for($data), 48 | default => throw new InvalidArgumentException("Unable to infer file type for '{$path}'"), 49 | }; 50 | 51 | return $writer->write($path); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Writer.php: -------------------------------------------------------------------------------- 1 | header_formatter = Str::headline(...); 32 | } 33 | 34 | public function withoutHeaders(): static 35 | { 36 | $this->headers = false; 37 | 38 | return $this; 39 | } 40 | 41 | public function withHeaderFormatter(Closure $header_formatter): static 42 | { 43 | $this->header_formatter = $header_formatter; 44 | 45 | return $this; 46 | } 47 | 48 | public function withOriginalKeysAsHeaders(): static 49 | { 50 | return $this->withHeaderFormatter(static fn($key) => $key); 51 | } 52 | 53 | public function write(string $path): string 54 | { 55 | $writer = $this->writer(); 56 | 57 | $writer->openToFile($path); 58 | 59 | foreach ($this->rows() as $row) { 60 | $writer->addRow(Row::fromValues($row->toArray())); 61 | } 62 | 63 | $writer->close(); 64 | 65 | return $path; 66 | } 67 | 68 | public function writeToHttpFile(): File 69 | { 70 | $path = $this->writeToTemporaryFile(); 71 | 72 | return new File($path); 73 | } 74 | 75 | public function writeToTemporaryFile(): string 76 | { 77 | $path = tempnam(sys_get_temp_dir(), 'glhd-linen-data'); 78 | 79 | App::terminating(fn() => @unlink($path)); 80 | 81 | return $this->write($path); 82 | } 83 | 84 | abstract protected function writer(): WriterInterface; 85 | 86 | /** @return Generator */ 87 | protected function rows(): Generator 88 | { 89 | $source = match (true) { 90 | $this->data instanceof Generator => LazyCollection::make($this->data), 91 | is_array($this->data) => Collection::make($this->data), 92 | $this->data instanceof Builder => $this->data->lazyById(), 93 | default => $this->data, 94 | }; 95 | 96 | $needs_headers = $this->headers; 97 | 98 | foreach ($source as $row) { 99 | $row = Collection::make($row); 100 | 101 | if ($needs_headers) { 102 | $needs_headers = false; 103 | yield $row->keys()->map($this->header_formatter); 104 | } 105 | 106 | yield $row; 107 | } 108 | } 109 | } 110 | --------------------------------------------------------------------------------