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
================================================
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
================================================