Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 126 additions & 23 deletions src/Importable.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Rap2hpoutre\FastExcel;

use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Reader\SheetInterface;
Expand Down Expand Up @@ -39,7 +40,7 @@ abstract protected function setOptions(&$options);
*
* @return Collection
*/
public function import($path, callable $callback = null)
public function import($path, ?callable $callback = null)
{
$reader = $this->reader($path);

Expand All @@ -54,6 +55,42 @@ public function import($path, callable $callback = null)
return collect($collection ?? []);
}

/**
* Import file lazily using LazyCollection for memory efficiency.
*
* @param string $path
* @param callable|null $callback
*
* @throws \OpenSpout\Common\Exception\UnsupportedTypeException
* @throws \OpenSpout\Reader\Exception\ReaderNotOpenedException
* @throws \OpenSpout\Common\Exception\IOException
*
* @return LazyCollection
*/
public function importLazy($path, ?callable $callback = null)
{
return new LazyCollection(function () use ($path, $callback) {
$reader = $this->reader($path);

try {
foreach ($reader->getSheetIterator() as $key => $sheet) {
if ($this->sheet_number != $key) {
continue;
}
if ($this->transpose) {
// Fallback to non-lazy processing when transposing
throw new \Exception('Transposing is not supported with lazy import.');
Copy link

Copilot AI Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should be more descriptive and use a specific exception type. Consider using InvalidArgumentException with a message like 'Transpose functionality is not compatible with lazy importing. Use import() method instead.'

Suggested change
throw new \Exception('Transposing is not supported with lazy import.');
// Transpose functionality is not compatible with lazy importing
throw new \InvalidArgumentException('Transpose functionality is not compatible with lazy importing. Use import() method instead.');

Copilot uses AI. Check for mistakes.
}

yield from $this->importSheetGenerator($sheet, $callback);
break;
}
} finally {
$reader->close();
}
});
}

/**
* @param string $path
* @param callable|null $callback
Expand All @@ -64,7 +101,7 @@ public function import($path, callable $callback = null)
*
* @return Collection
*/
public function importSheets($path, callable $callback = null)
public function importSheets($path, ?callable $callback = null)
{
$reader = $this->reader($path);

Expand Down Expand Up @@ -136,46 +173,75 @@ private function transposeCollection(array $array)
return $collection;
}

/**
* Normalize a row according to start_row and headers.
* - Updates $headers and $count_header when encountering header row.
* - Pads/truncates rows to header size when headers exist.
* - Returns combined associative row when headers exist, or the raw row when not.
* - Returns null to skip processing (before start_row or header row itself).
*
* @param int $key
* @param array $row
* @param array $headers
* @param int $count_header
*
* @return array|null
Copy link

Copilot AI Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizeRow method lacks proper PHPDoc documentation. It should include @param and @return annotations with type information and descriptions for the reference parameters.

Suggested change
* @return array|null
* @param int $k The current row index.
* @param array $row The row data to normalize.
* @param array &$headers Reference to the headers array, updated when header row is encountered.
* @param int &$count_header Reference to the header count, updated when header row is encountered.
*
* @return array|null Returns an associative array if headers exist, the raw row if not, or null to skip processing.

Copilot uses AI. Check for mistakes.
*/
private function normalizeRow(int $key, array $row, array &$headers, int &$count_header): ?array
{
if ($key < $this->start_row) {
return null;
}

if ($this->with_header) {
if ($key == $this->start_row) {
$headers = $this->toStrings($row);
$count_header = count($headers);

return null; // skip header row
}

if ($count_header > $count_row = count($row)) {
$row = array_merge($row, array_fill(0, $count_header - $count_row, null));
} elseif ($count_header < $count_row = count($row)) {
$row = array_slice($row, 0, $count_header);
}
}

return empty($headers) ? $row : array_combine($headers, $row);
}

/**
* @param SheetInterface $sheet
* @param callable|null $callback
*
* @return array
*/
private function importSheet(SheetInterface $sheet, callable $callback = null)
private function importSheet(SheetInterface $sheet, ?callable $callback = null)
{
$headers = [];
$collection = [];
$count_header = 0;

foreach ($sheet->getRowIterator() as $k => $rowAsObject) {
foreach ($sheet->getRowIterator() as $key => $rowAsObject) {
$row = array_map(function (Cell $cell) {
return match (true) {
$cell instanceof Cell\FormulaCell => $cell->getComputedValue(),
default => $cell->getValue(),
};
}, $rowAsObject->getCells());

if ($k >= $this->start_row) {
if ($this->with_header) {
if ($k == $this->start_row) {
$headers = $this->toStrings($row);
$count_header = count($headers);
continue;
}
if ($count_header > $count_row = count($row)) {
$row = array_merge($row, array_fill(0, $count_header - $count_row, null));
} elseif ($count_header < $count_row = count($row)) {
$row = array_slice($row, 0, $count_header);
}
}
if ($callback) {
if ($result = $callback(empty($headers) ? $row : array_combine($headers, $row))) {
$collection[] = $result;
}
} else {
$collection[] = empty($headers) ? $row : array_combine($headers, $row);
$current = $this->normalizeRow($key, $row, $headers, $count_header);
if ($current === null) {
continue;
}

if ($callback) {
if ($result = $callback($current)) {
$collection[] = $result;
}
} else {
$collection[] = $current;
}
}

Expand All @@ -186,6 +252,43 @@ private function importSheet(SheetInterface $sheet, callable $callback = null)
return $collection;
}

/**
* Create a generator that lazily yields imported rows from a sheet.
*
* @param SheetInterface $sheet
* @param callable|null $callback
*
* @return \Generator
*/
private function importSheetGenerator(SheetInterface $sheet, ?callable $callback = null): \Generator
{
$headers = [];
$count_header = 0;

foreach ($sheet->getRowIterator() as $key => $rowAsObject) {
$row = array_map(function (Cell $cell) {
return match (true) {
$cell instanceof Cell\FormulaCell => $cell->getComputedValue(),
default => $cell->getValue(),
};
}, $rowAsObject->getCells());
Copy link

Copilot AI Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cell processing logic is duplicated between importSheet() and importSheetGenerator(). Consider extracting this into a private method like processCellValue(Cell $cell) to reduce code duplication.

Suggested change
}, $rowAsObject->getCells());
$row = array_map([$this, 'processCellValue'], $rowAsObject->getCells());

Copilot uses AI. Check for mistakes.

$current = $this->normalizeRow($key, $row, $headers, $count_header);
if ($current === null) {
continue;
}

if ($callback) {
$result = $callback($current);
if ($result) {
yield $result;
}
} else {
yield $current;
}
}
}

/**
* @param array $values
*
Expand Down
44 changes: 44 additions & 0 deletions tests/LazyImportTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Rap2hpoutre\FastExcel\Tests;

use Illuminate\Support\LazyCollection;
use Rap2hpoutre\FastExcel\FastExcel;

class LazyImportTest extends TestCase
{
/**
* Ensure importLazy returns a LazyCollection and yields same data as import.
*/
public function testImportLazyXlsx()
{
$fe = new FastExcel();
$lazy = $fe->importLazy(__DIR__.'/test1.xlsx');
$this->assertInstanceOf(LazyCollection::class, $lazy);
// Materialize to compare with existing helper collection()
$this->assertEquals($this->collection(), $lazy->collect());
}

/**
* Ensure importLazy supports callback mapping similar to import.
*/
public function testImportLazyWithCallback()
{
$fe = new FastExcel();
$lazy = $fe->importLazy(__DIR__.'/test1.xlsx', function ($row) {
return [
'col1' => $row['col1'],
'col2' => $row['col2'],
];
});

$expected = (new FastExcel())->import(__DIR__.'/test1.xlsx', function ($row) {
return [
'col1' => $row['col1'],
'col2' => $row['col2'],
];
});

$this->assertEquals($expected, $lazy->collect());
}
}