Skip to content

Commit 6e2f601

Browse files
committed
feat: Add support for LazyCollection and use query cursor
1 parent 9a10720 commit 6e2f601

File tree

7 files changed

+139
-58
lines changed

7 files changed

+139
-58
lines changed

src/Concerns/FromCollection.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
namespace Vitorccs\LaravelCsv\Concerns;
44

55
use Illuminate\Support\Collection;
6+
use Illuminate\Support\LazyCollection;
67

78
interface FromCollection
89
{
910
/**
10-
* @return Collection
11+
* @return Collection|LazyCollection
1112
*/
12-
public function collection(): Collection;
13+
public function collection();
1314
}

src/Concerns/FromQueryCursor.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Vitorccs\LaravelCsv\Concerns;
4+
5+
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
6+
use Illuminate\Database\Query\Builder;
7+
8+
interface FromQueryCursor
9+
{
10+
/**
11+
* @return Builder|EloquentBuilder
12+
*/
13+
public function query();
14+
}

src/Services/ExportableService.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Vitorccs\LaravelCsv\Concerns\FromArray;
1111
use Vitorccs\LaravelCsv\Concerns\FromCollection;
1212
use Vitorccs\LaravelCsv\Concerns\FromQuery;
13+
use Vitorccs\LaravelCsv\Concerns\FromQueryCursor;
1314
use Vitorccs\LaravelCsv\Entities\CsvConfig;
1415
use Vitorccs\LaravelCsv\Exceptions\InvalidCellValueException;
1516
use Vitorccs\LaravelCsv\Handlers\ArrayHandler;
@@ -62,7 +63,7 @@ public function count(object $exportable): int
6263
return $exportable->collection()->count();
6364
}
6465

65-
if ($exportable instanceof FromQuery) {
66+
if ($exportable instanceof FromQuery || $exportable instanceof FromQueryCursor) {
6667
return $exportable->query()->count();
6768
}
6869

src/Services/Writer.php

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Support\Collection;
7+
use Throwable;
78
use Vitorccs\LaravelCsv\Concerns\FromArray;
89
use Vitorccs\LaravelCsv\Concerns\FromCollection;
910
use Vitorccs\LaravelCsv\Concerns\FromQuery;
11+
use Vitorccs\LaravelCsv\Concerns\FromQueryCursor;
1012
use Vitorccs\LaravelCsv\Concerns\WithColumnFormatting;
1113
use Vitorccs\LaravelCsv\Concerns\WithHeadings;
1214
use Vitorccs\LaravelCsv\Concerns\WithMapping;
@@ -70,6 +72,10 @@ public function generate(object $exportable)
7072
);
7173
}
7274

75+
if ($exportable instanceof FromQueryCursor) {
76+
$this->iterateRows($exportable, $exportable->query()->cursor());
77+
}
78+
7379
return $this->handler->getResource();
7480
}
7581

@@ -81,67 +87,76 @@ public function generate(object $exportable)
8187
*/
8288
private function iterateRows(object $exportable, $rows): void
8389
{
84-
if ($rows instanceof Collection) {
85-
$rows = iterator_to_array($rows->values());
90+
$formats = $exportable instanceof WithColumnFormatting ? $exportable->columnFormats() : [];
91+
$withMapping = $exportable instanceof WithMapping;
92+
$row_index = 1;
93+
foreach ($rows as $row) {
94+
$mappedRow = $withMapping ? $exportable->map($row) : $row;
95+
$normalizedRow = $this->normalizeRow($mappedRow);
96+
$formattedRow = $this->applyFormatting($normalizedRow, $formats, $row_index++);
97+
$this->writeRow($formattedRow);
8698
}
99+
}
87100

88-
if ($exportable instanceof WithMapping) {
89-
$rows = array_map(fn($row) => $exportable->map($row), $rows);
101+
private function normalizeRow($row): array
102+
{
103+
if ($row instanceof Model) {
104+
$row = ModelHelper::toArrayValues($row);
105+
}
106+
if (is_object($row)) {
107+
$row = (array)$row;
90108
}
109+
if (is_array($row)) {
110+
$row = array_values($row);
111+
}
112+
return $row;
113+
}
91114

92-
$rows = array_map(function ($row) {
93-
if ($row instanceof Model) {
94-
$row = ModelHelper::toArrayValues($row);
95-
}
96-
if (is_object($row)) {
97-
$row = (array)$row;
98-
}
99-
if (is_array($row)) {
100-
$row = array_values($row);
101-
}
102-
return $row;
103-
}, $rows);
104-
105-
$rows = array_map(function ($row, int $iRow) use ($exportable) {
106-
return array_map(function ($column, int $iColumn) use ($exportable, $iRow) {
107-
$formats = $exportable instanceof WithColumnFormatting
108-
? $exportable->columnFormats()
109-
: [];
110-
$columnLetter = CsvHelper::getColumnLetter($iColumn + 1);
111-
$format = $formats[$columnLetter] ?? null;
112-
113-
if ($format === CellFormat::DATE) {
114-
115-
$column = $this->formatter->date($column);
116-
}
117-
118-
if ($format === CellFormat::DATETIME) {
119-
$column = $this->formatter->datetime($column);
120-
}
121-
122-
if ($format === CellFormat::DECIMAL) {
123-
$column = $this->formatter->decimal($column);
124-
}
125-
126-
if ($format === CellFormat::INTEGER) {
127-
$column = $this->formatter->integer($column);
128-
}
129-
130-
try {
131-
if (!is_string($column)) {
132-
$column = (string)$column;
133-
}
134-
} catch (\Throwable $e) {
135-
throw new InvalidCellValueException("{$columnLetter}{$iRow}");
136-
}
137-
138-
return $column;
139-
}, $row, array_keys($row));
140-
}, $rows, array_keys($rows));
115+
/**
116+
* @throws InvalidCellValueException
117+
*/
118+
private function applyFormatting(array $row, array $formats, int $row_index): array
119+
{
120+
return array_map(
121+
fn ($value, int $column_index) => $this->formatCellValue($value, $formats, $row_index, $column_index),
122+
$row,
123+
array_keys($row)
124+
);
125+
}
141126

142-
foreach ($rows as $row) {
143-
$this->writeRow($row);
127+
/**
128+
* @throws InvalidCellValueException
129+
*/
130+
private function formatCellValue($value, array $formats, int $row_index, int $column_index): string
131+
{
132+
$columnLetter = CsvHelper::getColumnLetter($column_index + 1);
133+
$format = $formats[$columnLetter] ?? null;
134+
135+
if ($format === CellFormat::DATE) {
136+
return $this->formatter->date($value);
137+
}
138+
139+
if ($format === CellFormat::DATETIME) {
140+
return $this->formatter->datetime($value);
144141
}
142+
143+
if ($format === CellFormat::DECIMAL) {
144+
return $this->formatter->decimal($value);
145+
}
146+
147+
if ($format === CellFormat::INTEGER) {
148+
return $this->formatter->integer($value);
149+
}
150+
151+
try {
152+
if (! is_string($value)) {
153+
return (string)$value;
154+
}
155+
} catch (Throwable $e) {
156+
throw new InvalidCellValueException("{$columnLetter}{$row_index}");
157+
}
158+
159+
return $value;
145160
}
146161

147162
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Vitorccs\LaravelCsv\Tests\Concerns;
4+
5+
use Vitorccs\LaravelCsv\Tests\Data\Database\Seeders\TestUsersSeeder;
6+
use Vitorccs\LaravelCsv\Tests\Data\Exports\FromQueryCursorExport;
7+
use Vitorccs\LaravelCsv\Tests\TestCase;
8+
9+
class FromQueryCursorTest extends TestCase
10+
{
11+
protected string $filename = 'from_query.csv';
12+
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
$this->seed(TestUsersSeeder::class);
18+
}
19+
20+
public function test_from_query()
21+
{
22+
$export = new FromQueryCursorExport();
23+
24+
$export->store($this->filename);
25+
$contents = $this->readFromDisk($this->filename);
26+
27+
$this->assertEquals($export->toArray(), $contents);
28+
}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Vitorccs\LaravelCsv\Tests\Data\Exports;
4+
5+
use Vitorccs\LaravelCsv\Concerns\Exportable;
6+
use Vitorccs\LaravelCsv\Concerns\FromQueryCursor;
7+
use Vitorccs\LaravelCsv\Tests\Data\Stubs\TestUser;
8+
9+
class FromQueryCursorExport implements FromQueryCursor
10+
{
11+
use Exportable;
12+
13+
public function query()
14+
{
15+
return TestUser::query();
16+
}
17+
}

tests/Services/ExportableServiceTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Vitorccs\LaravelCsv\Tests\Data\Database\Seeders\TestUsersSeeder;
1313
use Vitorccs\LaravelCsv\Tests\Data\Exports\FromArrayExport;
1414
use Vitorccs\LaravelCsv\Tests\Data\Exports\FromCollectionExport;
15+
use Vitorccs\LaravelCsv\Tests\Data\Exports\FromQueryCursorExport;
1516
use Vitorccs\LaravelCsv\Tests\Data\Exports\FromQueryExport;
1617
use Vitorccs\LaravelCsv\Tests\Data\Exports\WithMappingExportSimple;
1718
use Vitorccs\LaravelCsv\Tests\Data\Helpers\FakerHelper;
@@ -39,6 +40,7 @@ public function test_count()
3940
$arrayExport = new FromArrayExport();
4041
$collectionExport = new FromCollectionExport();
4142
$queryExport = new FromQueryExport();
43+
$queryCursorExport = new FromQueryCursorExport();
4244

4345
$this->assertSame(
4446
$this->service->count($arrayExport),
@@ -54,6 +56,8 @@ public function test_count()
5456
$this->service->count($queryExport),
5557
TestUsersSeeder::$amount
5658
);
59+
60+
$this->assertSame(TestUsersSeeder::$amount, $this->service->count($queryCursorExport));
5761
}
5862

5963
public function test_queue()

0 commit comments

Comments
 (0)