Repository: glhd/linen Branch: main Commit: b427bdb64f1e Files: 44 Total size: 74.2 KB Directory structure: gitextract_gpyxuo6q/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── php-cs-fixer.yml │ ├── phpunit.yml │ └── update-changelog.yml ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── blade.xml │ ├── inspectionProfiles/ │ │ └── Project_Default.xml │ ├── laravel-idea.xml │ ├── linen.iml │ ├── modules.xml │ ├── php-test-framework.xml │ ├── php.xml │ └── vcs.xml ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config/ │ └── linen.php ├── ide.json ├── phpunit.xml.dist ├── src/ │ ├── CsvReader.php │ ├── CsvWriter.php │ ├── ExcelReader.php │ ├── ExcelWriter.php │ ├── Facades/ │ │ ├── .gitkeep │ │ └── Linen.php │ ├── Reader.php │ ├── Support/ │ │ ├── FileTypeHelper.php │ │ └── WriteIterator.php │ ├── Writer.php │ └── helpers.php └── tests/ ├── Feature/ │ ├── CsvReaderTest.php │ ├── CsvWriterTest.php │ ├── ExcelReaderTest.php │ ├── ExcelWriterTest.php │ └── FacadeTest.php ├── TestCase.php └── fixtures/ ├── basic.csv ├── basic.xlsx └── more-columns-than-headers.csv ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # This file was generated by the lean package validator (http://git.io/lean-package-validator). * text=auto eol=lf .codeclimate.yml export-ignore .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore .idea/ export-ignore .php-cs-fixer.dist.php export-ignore CHANGELOG.md export-ignore LICENSE export-ignore phpunit.xml.dist export-ignore README.md export-ignore tests/ export-ignore ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **What version does this affect?** - Laravel Version: [e.g. 10.0.0] - Package Version: [e.g. 1.5.0] **To Reproduce** Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest a new feature idea or improvement title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/php-cs-fixer.yml ================================================ name: Code Style on: [ pull_request, push ] jobs: coverage: runs-on: ubuntu-latest name: Run code style checks steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv coverage: none - name: Get composer cache directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies env: COMPOSER_DISCARD_CHANGES: true run: composer require --no-progress --no-interaction --prefer-dist --update-with-all-dependencies "laravel/framework:12.*" - name: Run PHP CS Fixer run: ./vendor/bin/php-cs-fixer fix --diff --dry-run ================================================ FILE: .github/workflows/phpunit.yml ================================================ name: PHPUnit on: push: pull_request: schedule: - cron: '0 14 * * 3' # Run Wednesdays at 2pm EST jobs: php-tests: runs-on: ubuntu-latest strategy: matrix: php: [ 8.1, 8.2, 8.3, 8.4, 8.5 ] laravel: [ 13.*, 12.*, 11.*, 10.* ] dependency-version: [ stable, lowest ] exclude: - { laravel: 13.*, php: 8.2 } - { laravel: 13.*, php: 8.1 } - { laravel: 12.*, php: 8.1 } - { laravel: 11.*, php: 8.5 } - { laravel: 11.*, php: 8.1 } - { laravel: 10.*, php: 8.5 } - { laravel: 10.*, php: 8.4 } timeout-minutes: 10 name: "${{ matrix.php }} / ${{ matrix.laravel }} (${{ matrix.dependency-version }})" steps: - name: Checkout code uses: actions/checkout@v5 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv tools: composer:v2 - name: Register composer cache directory id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies run: | composer require --no-interaction --prefer-dist --prefer-${{ matrix.dependency-version }} --update-with-all-dependencies "laravel/framework:${{ matrix.laravel }}" - name: Execute tests run: vendor/bin/phpunit ================================================ FILE: .github/workflows/update-changelog.yml ================================================ name: Update Changelog on: release: types: [ published ] jobs: update-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: repository: ${{ github.event.repository.full_name }} ref: 'main' - name: Update changelog uses: thomaseizinger/keep-a-changelog-new-release@v2 with: version: ${{ github.event.release.tag_name }} - name: Commit changelog back to repo uses: EndBug/add-and-commit@v9 with: add: 'CHANGELOG.md' message: ${{ github.event.release.tag_name }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ vendor/ composer.phar composer.lock phpunit.xml .phpunit.result.cache .php_cs.cache .php-cs-fixer.cache .env .DS_Store .phpstorm.meta.php _ide_helper.php node_modules mix-manifest.json yarn-error.log ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml # Datasource local storage ignored files /dataSources/ /dataSources.local.xml # Editor-based HTTP Client requests /httpRequests/ laravel-idea-personal.xml ================================================ FILE: .idea/blade.xml ================================================ ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: .idea/laravel-idea.xml ================================================ ================================================ FILE: .idea/linen.iml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/php-test-framework.xml ================================================ ================================================ FILE: .idea/php.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: .php-cs-fixer.dist.php ================================================ setRiskyAllowed(true) ->setIndent("\t") ->setLineEnding("\n") ->setRules([ '@PSR2' => true, 'function_declaration' => [ 'closure_function_spacing' => 'none', 'closure_fn_spacing' => 'none', ], 'ordered_imports' => [ 'sort_algorithm' => 'alpha', ], 'array_indentation' => true, 'braces' => [ 'allow_single_line_closure' => true, ], 'no_break_comment' => false, 'return_type_declaration' => [ 'space_before' => 'none', ], 'blank_line_after_opening_tag' => true, 'compact_nullable_typehint' => true, 'cast_spaces' => true, 'concat_space' => [ 'spacing' => 'none', ], 'declare_equal_normalize' => [ 'space' => 'none', ], 'function_typehint_space' => true, 'new_with_braces' => true, 'method_argument_space' => true, 'no_empty_statement' => true, 'no_empty_comment' => true, 'no_empty_phpdoc' => true, 'no_extra_blank_lines' => [ 'tokens' => [ 'extra', 'use', 'use_trait', 'return', ], ], 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_whitespace_in_blank_line' => false, 'no_whitespace_before_comma_in_array' => true, 'no_useless_else' => true, 'no_useless_return' => true, 'single_trait_insert_per_statement' => true, 'psr_autoloading' => true, 'dir_constant' => true, 'single_line_comment_style' => [ 'comment_types' => ['hash'], ], 'include' => true, 'is_null' => true, 'linebreak_after_opening_tag' => true, 'lowercase_cast' => true, 'lowercase_static_reference' => true, 'magic_constant_casing' => true, 'magic_method_casing' => true, 'class_attributes_separation' => [ // TODO: This can be reverted when https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/5869 is merged 'elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one'], ], 'modernize_types_casting' => true, 'native_function_casing' => true, 'native_function_type_declaration_casing' => true, 'no_alias_functions' => true, 'no_multiline_whitespace_around_double_arrow' => true, 'multiline_whitespace_before_semicolons' => true, 'no_short_bool_cast' => true, 'no_unused_imports' => true, 'no_php4_constructor' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_around_offset' => true, 'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_singleline_array' => true, 'normalize_index_brace' => true, 'object_operator_without_whitespace' => true, 'phpdoc_annotation_without_dot' => true, 'phpdoc_indent' => true, 'phpdoc_no_package' => true, 'phpdoc_no_access' => true, 'phpdoc_no_useless_inheritdoc' => true, 'phpdoc_single_line_var_spacing' => true, 'phpdoc_trim' => true, 'phpdoc_types' => true, 'semicolon_after_instruction' => true, 'array_syntax' => [ 'syntax' => 'short', ], 'list_syntax' => [ 'syntax' => 'short', ], 'short_scalar_cast' => true, 'single_blank_line_before_namespace' => true, 'single_quote' => true, 'standardize_not_equals' => true, 'ternary_operator_spaces' => true, 'whitespace_after_comma_in_array' => true, 'not_operator_with_successor_space' => true, 'trailing_comma_in_multiline' => true, 'trim_array_spaces' => true, 'binary_operator_spaces' => true, 'unary_operator_spaces' => true, 'php_unit_method_casing' => [ 'case' => 'snake_case', ], 'php_unit_test_annotation' => [ 'style' => 'prefix', ], ]) ->setFinder( PhpCsFixer\Finder::create() ->exclude('.circleci') ->exclude('bin') ->exclude('node_modules') ->exclude('vendor') ->notPath('.phpstorm.meta.php') ->notPath('_ide_helper.php') ->notPath('artisan') ->in(__DIR__) ); ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes will be documented in this file following the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.1.0] - 2026-03-23 ## [0.0.4] - 2025-11-11 ## [0.0.3] - 2025-07-16 ## [0.0.2] - 2024-08-02 ## [0.0.1] - 2024-07-25 ## [0.0.1] # Keep a Changelog Syntax - `Added` for new features. - `Changed` for changes in existing functionality. - `Deprecated` for soon-to-be removed features. - `Removed` for now removed features. - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. [Unreleased]: https://github.com/glhd/linen/compare/0.1.0...HEAD [0.1.0]: https://github.com/glhd/linen/compare/0.0.4...0.1.0 [0.0.4]: https://github.com/glhd/linen/compare/0.0.3...0.0.4 [0.0.3]: https://github.com/glhd/linen/compare/0.0.2...0.0.3 [0.0.2]: https://github.com/glhd/linen/compare/0.0.1...0.0.2 [0.0.1]: https://github.com/glhd/linen/compare/0.0.1...0.0.1 [0.0.1]: https://github.com/glhd/linen/compare/0.0.1...0.0.1 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Galahad, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Build Status Latest Stable Release MIT Licensed Follow @cmorrell.com on bsky

Linen

Linen is a lightweight spreadsheet utility for Laravel. It's a simple wrapper for [openspout](https://github.com/openspout/openspout) with some data normalization conveniences. ## Installation ```shell composer require glhd/linen ``` ## Usage To read a spreadsheet: ```php foreach (Linen::read('path/to/your.xlsx') as $row) { // $row is a collection, keyed by the headers in snake_case } ``` To write a spreadsheet: ```php // $data can be any iterable/Enumerable/etc $path = Linen::write($data, 'path/to/your.xlsx'); ``` ================================================ FILE: composer.json ================================================ { "name": "glhd/linen", "description": "", "keywords": [ "laravel" ], "authors": [ { "name": "Chris Morrell", "homepage": "http://www.cmorrell.com" } ], "type": "library", "license": "MIT", "require": { "illuminate/support": "^10|^11|^12|^13|dev-master", "ext-json": "*", "openspout/openspout": "^4.24" }, "require-dev": { "orchestra/testbench": "^8.37|^9.17|^10.11|^11.0|^12.x-dev", "friendsofphp/php-cs-fixer": "^3.94", "mockery/mockery": "^1.6", "phpunit/phpunit": "^10.5|^11.5|^12.5|^13.0" }, "autoload": { "psr-4": { "Glhd\\Linen\\": "src/" }, "files": [ "src/helpers.php" ] }, "autoload-dev": { "classmap": [ "tests/TestCase.php" ], "psr-4": { "Glhd\\Linen\\Tests\\": "tests/" } }, "scripts": { "fix-style": "vendor/bin/php-cs-fixer fix", "check-style": "vendor/bin/php-cs-fixer fix --diff --dry-run" }, "extra": { "laravel": { "providers": [] } }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: config/linen.php ================================================ ./tests ./src ================================================ FILE: src/CsvReader.php ================================================ getValue(); return match (true) { is_numeric($value) => (float) $value == (int) $value ? (int) $value : (float) $value, '' === $value => null, default => $value, }; } } ================================================ FILE: src/CsvWriter.php ================================================ 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 withEmptyNewLineAtEndOfFile(): static { $this->empty_new_line = true; return $this; } public function withoutEmptyNewLineAtEndOfFile(): static { $this->empty_new_line = false; return $this; } public function getIterator(?string $path = null): WriteIterator { $path ??= tempnam_with_cleanup(); return new WriteIterator( path: $path, generator: $this->rows(), writer: $this->writer(), cleanup: $this->cleanupCallback(), ); } protected function writer(): WriterInterface { $options = new OpenSpout\Options(); $options->FIELD_DELIMITER = $this->delimiter; $options->FIELD_ENCLOSURE = $this->enclosure; $options->SHOULD_ADD_BOM = $this->bom; return new OpenSpout\Writer($options); } protected function cleanupCallback(): ?Closure { if (! $this->empty_new_line) { return fn($path) => file_put_contents($path, rtrim(file_get_contents($path), PHP_EOL)); } return null; } } ================================================ FILE: src/ExcelReader.php ================================================ getValue()); } if ($cell instanceof Cell\EmptyCell) { return null; } return parent::castCell($cell); } } ================================================ FILE: src/ExcelWriter.php ================================================ */ abstract class Reader implements IteratorAggregate { public static function from(string $path): static { return new static($path); } public static function read(string $path): LazyCollection { return static::from($path)->collect(); } public function __construct( protected string $path, ) { } public function getIterator(): Traversable { return $this->collect(); } public function collect(): LazyCollection { return new LazyCollection(function() { $reader = $this->reader(); $reader->open($this->path); try { foreach ($reader->getSheetIterator() as $sheet) { $columns = 0; $keys = null; foreach ($sheet->getRowIterator() as $row) { /** @var \OpenSpout\Common\Entity\Row $row */ if (null === $keys) { $keys = array_map($this->headerToKey(...), $row->toArray()); $columns = count($keys); continue; } $data = $this->castRow($row); $data_columns = count($data); if ($columns < $data_columns) { foreach (range(1, $data_columns) as $index => $column) { $keys[$index] ??= "column{$column}"; } $columns = count($keys); } if ($columns > $data_columns) { $data = array_merge($data, array_fill(0, $columns - $data_columns, null)); } yield Collection::make(array_combine($keys, $data)); } } } finally { $reader->close(); } }); } abstract protected function reader(): ReaderInterface; protected function castRow(Row $data): array { return array_map($this->castCell(...), $data->getCells()); } protected function castCell(Cell $cell): mixed { return $cell->getValue(); } protected function headerToKey(string $value): string { return Str::snake(strtolower($value)); } } ================================================ FILE: src/Support/FileTypeHelper.php ================================================ guessMimeType($path); return match ($mime) { 'application/msexcel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls', 'application/vnd.ms-excel', 'application/vnd.ms-excel.sheet.binary.macroenabled.12', 'application/vnd.ms-excel.sheet.macroenabled.12', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => ExcelReader::from($path), 'application/csv', 'text/csv', 'text/csv-schema', 'text/x-comma-separated-values', 'text/x-csv', 'text/plain' => CsvReader::from($path), default => throw new InvalidArgumentException("Unable to infer file type for '{$path}'"), }; } public function write(array|Enumerable|Generator|Builder $data, string $path): string { $extension = pathinfo($path, PATHINFO_EXTENSION); $writer = match ($extension) { 'xlsx', 'xls' => ExcelWriter::for($data), 'csv' => CsvWriter::for($data), default => throw new InvalidArgumentException("Unable to infer file type for '{$path}'"), }; return $writer->write($path); } } ================================================ FILE: src/Support/WriteIterator.php ================================================ open) { $this->rewind(); } while ($this->valid()) { $this->writeCurrentRow(); $this->next(); } return $this->path; } public function rewind(): void { $this->written = 0; $this->pending = true; $this->generator->rewind(); $this->openWriter(); } public function key(): mixed { return $this->generator->key(); } /** * To prevent unintended memory issues, we're only going to return the write count * from the iterator. The iterator is just for handling progress/stepping, and not * for accessing the underlying data. * * @return int */ public function current(): int { $this->writeCurrentRow(); return $this->written; } public function next(): void { if (! $this->open) { return; } $this->generator->next(); $this->pending = true; } public function valid(): bool { if (! $valid = $this->generator->valid()) { $this->closeWriter(); } return $valid; } public function __destruct() { $this->closeWriter(); } protected function openWriter(): void { if (! $this->open) { $this->writer->openToFile($this->path); $this->open = true; } } protected function writeCurrentRow(): void { if ($this->pending) { $this->writer->addRow( Row::fromValues($this->generator->current()->toArray()) ); $this->written++; $this->pending = false; } } protected function closeWriter(): void { if ($this->open) { $this->writer->close(); if ($this->cleanup) { call_user_func($this->cleanup, $this->path); } $this->open = false; } } } ================================================ FILE: src/Writer.php ================================================ header_formatter = Str::headline(...); } public function withoutHeaders(): static { $this->headers = false; return $this; } public function withHeaderFormatter(Closure $header_formatter): static { $this->header_formatter = $header_formatter; return $this; } public function withOriginalKeysAsHeaders(): static { return $this->withHeaderFormatter(static fn($key) => $key); } public function getIterator(?string $path = null): WriteIterator { $path ??= tempnam_with_cleanup(); return new WriteIterator($path, $this->rows(), $this->writer()); } public function write(string $path): string { return $this->getIterator($path)->drain(); } public function writeToHttpFile(): File { return new File($this->writeToTemporaryFile()); } public function writeToTemporaryFile(): string { return $this->write(tempnam_with_cleanup()); } abstract protected function writer(): WriterInterface; /** @return Generator */ protected function rows(): Generator { $source = match (true) { $this->data instanceof Closure => 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; } } } ================================================ FILE: src/helpers.php ================================================ @unlink($path)); return $path; } ================================================ FILE: tests/Feature/CsvReaderTest.php ================================================ fixture('basic.csv')); foreach ($reader as $index => $row) { $this->assertSame(match ($index) { 0 => ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], 1 => ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], }, $row->toArray()); } } public function test_it_can_read_a_basic_csv_file_as_a_collection(): void { $collection = CsvReader::from($this->fixture('basic.csv'))->collect(); foreach ($collection as $index => $row) { $this->assertSame(match ($index) { 0 => ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], 1 => ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], }, $row->toArray()); } } public function test_if_headers_are_missing_column_numbers_are_used_as_keys(): void { $collection = CsvReader::from($this->fixture('more-columns-than-headers.csv'))->collect(); foreach ($collection as $index => $row) { $this->assertSame(match ($index) { 0 => ['user_id' => 1, 'name' => 'Chris', 'column3' => null, 'column4' => 40.2, 'column5' => null, 'column6' => null, 'column7' => null], 1 => ['user_id' => 10, 'name' => 'Bogdan', 'column3' => 'not null', 'column4' => -37, 'column5' => null, 'column6' => null, 'column7' => null], }, $row->toArray()); } } } ================================================ FILE: tests/Feature/CsvWriterTest.php ================================================ 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $path = CsvWriter::for($data)->writeToTemporaryFile(); $written = file_get_contents($path); $expected = <<assertSame($expected, $written); } public function test_it_can_write_to_a_csv_with_a_new_line_at_end(): void { $data = [ ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $path = CsvWriter::for($data)->withEmptyNewLineAtEndOfFile()->writeToTemporaryFile(); $written = file_get_contents($path); $expected = <<assertSame($expected, $written); } public function test_it_can_write_with_an_iterator(): void { $data = [ ['user_id' => 1, 'name' => 'Chris'], ['user_id' => 10, 'name' => 'Skyler'], ]; $iterator = CsvWriter::for($data)->getIterator(tempnam_with_cleanup()); // Iterator returns [original key] => [rows written] $this->assertEquals( [0 => 1, 1 => 2, 2 => 3], iterator_to_array($iterator) ); $written = file_get_contents($iterator->path); $expected = <<assertSame($expected, $written); } } ================================================ FILE: tests/Feature/ExcelReaderTest.php ================================================ fixture('basic.xlsx')); foreach ($reader as $index => $row) { if (0 === $index) { $this->assertEquals(1, $row['user_id']); $this->assertEquals('Chris', $row['name']); $this->assertEquals('2024-07-25', $row['date']->format('Y-m-d')); $this->assertEquals(40.20, $row['number']); } elseif (1 === $index) { $this->assertEquals(10, $row['user_id']); $this->assertEquals('Bogdan', $row['name']); $this->assertEquals('2024-07-20', $row['date']->format('Y-m-d')); $this->assertEquals(-37.0, $row['number']); } } } public function test_it_can_read_a_basic_excel_file_as_a_collection(): void { $collection = ExcelReader::from($this->fixture('basic.xlsx'))->collect(); foreach ($collection as $index => $row) { if (0 === $index) { $this->assertEquals(1, $row['user_id']); $this->assertEquals('Chris', $row['name']); $this->assertEquals('2024-07-25', $row['date']->format('Y-m-d')); $this->assertEquals(40.20, $row['number']); } elseif (1 === $index) { $this->assertEquals(10, $row['user_id']); $this->assertEquals('Bogdan', $row['name']); $this->assertEquals('2024-07-20', $row['date']->format('Y-m-d')); $this->assertEquals(-37.0, $row['number']); } } } } ================================================ FILE: tests/Feature/ExcelWriterTest.php ================================================ 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $tempfile = ExcelWriter::for($data)->writeToTemporaryFile(); $read = ExcelReader::read($tempfile)->toArray(); $this->assertSame($data, $read); } public function test_it_can_write_to_an_excel_file_with_iterator(): void { $data = [ ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $iterator = ExcelWriter::for($data)->getIterator(); $this->assertEquals( [0 => 1, 1 => 2, 2 => 3], iterator_to_array($iterator), ); $read = ExcelReader::read($iterator->path)->toArray(); $this->assertSame($data, $read); } } ================================================ FILE: tests/Feature/FacadeTest.php ================================================ fixture('basic.csv')); foreach ($read as $index => $row) { $this->assertSame(match ($index) { 0 => ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], 1 => ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], }, $row->toArray()); } } public function test_it_can_read_a_basic_excel_file_via_the_facade(): void { $read = Linen::read($this->fixture('basic.xlsx')); foreach ($read as $index => $row) { if (0 === $index) { $this->assertEquals(1, $row['user_id']); $this->assertEquals('Chris', $row['name']); $this->assertEquals('2024-07-25', $row['date']->format('Y-m-d')); $this->assertEquals(40.20, $row['number']); } elseif (1 === $index) { $this->assertEquals(10, $row['user_id']); $this->assertEquals('Bogdan', $row['name']); $this->assertEquals('2024-07-20', $row['date']->format('Y-m-d')); $this->assertEquals(-37.0, $row['number']); } } } public function test_it_can_write_a_basic_csv_file_via_the_facade(): void { $data = [ ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $path = tempnam(sys_get_temp_dir(), 'glhd-linen-data').'.csv'; $written = file_get_contents(Linen::write($data, $path)); $expected = <<assertSame($expected, $written); unlink($path); } public function test_it_can_write_a_basic_excel_file_via_the_facade(): void { $data = [ ['user_id' => 1, 'name' => 'Chris', 'nullable' => null, 'number' => 40.2], ['user_id' => 10, 'name' => 'Bogdan', 'nullable' => 'not null', 'number' => -37], ]; $path = tempnam(sys_get_temp_dir(), 'glhd-linen-data').'.xlsx'; Linen::write($data, $path); $read = ExcelReader::read($path)->toArray(); $this->assertSame($data, $read); @unlink($path); } } ================================================ FILE: tests/TestCase.php ================================================