Skip to content

Commit 66f4fb7

Browse files
committed
Add a new Google Sheet loader
1 parent 1f29435 commit 66f4fb7

17 files changed

+581
-37
lines changed

src/adapter/etl-adapter-google-sheet/src/Flow/ETL/Adapter/GoogleSheet/GoogleSheetExtractor.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public function extract(FlowContext $context) : \Generator
3838
$cellsRange = new SheetRange($this->columnRange, 1, $this->rowsPerPage);
3939
$headers = [];
4040

41-
/** @var Sheets\ValueRange $response */
4241
$response = $this->service->spreadsheets_values->get(
4342
$this->spreadsheetId,
4443
$cellsRange->toString(),
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Adapter\GoogleSheet;
6+
7+
use Flow\ETL\{Adapter\GoogleSheet\RowsNormalizer\EntryNormalizer, FlowContext, Loader, Row\Entry, Rows};
8+
use Google\Service\Sheets;
9+
use Google\Service\Sheets\ValueRange;
10+
11+
final class GoogleSheetLoader implements Loader
12+
{
13+
private string $dateTimeFormat = \DateTimeInterface::ATOM;
14+
15+
/**
16+
* @param array{dateTimeRenderOption?: string, majorDimension?: string, valueRenderOption?: string} $options
17+
*/
18+
private array $options = [];
19+
20+
private bool $withHeader = true;
21+
22+
public function __construct(
23+
private readonly Sheets $service,
24+
private readonly string $spreadsheetId,
25+
private readonly string $sheetName,
26+
private readonly ValueInputOption $inputOption = ValueInputOption::RAW,
27+
) {
28+
}
29+
30+
public function load(Rows $rows, FlowContext $context) : void
31+
{
32+
if (!$rows->count()) {
33+
return;
34+
}
35+
36+
$sheetRange = SheetRangeCalculator::calculate(
37+
count($rows->entries()),
38+
$rows->count() + ($this->withHeader ? 1 : 0),
39+
$this->sheetName
40+
);
41+
42+
$normalizer = new RowsNormalizer(new EntryNormalizer($context->config->caster(), $this->dateTimeFormat));
43+
44+
$this->service->spreadsheets_values->update(
45+
$this->spreadsheetId,
46+
$sheetRange->toString(),
47+
new ValueRange(
48+
[
49+
'values' => $this->values($rows, $normalizer),
50+
'majorDimension' => 'ROWS',
51+
]
52+
),
53+
\array_merge($this->inputOption->toArray(), $this->options)
54+
);
55+
}
56+
57+
public function withDateTimeFormat(string $dateTimeFormat) : self
58+
{
59+
$this->dateTimeFormat = $dateTimeFormat;
60+
61+
return $this;
62+
}
63+
64+
public function withHeader(bool $withHeader) : self
65+
{
66+
$this->withHeader = $withHeader;
67+
68+
return $this;
69+
}
70+
71+
/**
72+
* @param array{dateTimeRenderOption?: string, majorDimension?: string, valueRenderOption?: string} $options
73+
*/
74+
public function withOptions(array $options) : self
75+
{
76+
$this->options = $options;
77+
78+
return $this;
79+
}
80+
81+
private function values(Rows $rows, RowsNormalizer $normalizer) : array
82+
{
83+
$values = [];
84+
85+
if ($this->withHeader) {
86+
$values[] = $rows->first()->entries()->map(fn (Entry $entry) => $entry->name());
87+
}
88+
89+
foreach ($normalizer->normalize($rows) as $normalizedRow) {
90+
$values[] = $normalizedRow;
91+
}
92+
93+
return $values;
94+
}
95+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Adapter\GoogleSheet;
6+
7+
use Flow\ETL\Adapter\GoogleSheet\RowsNormalizer\EntryNormalizer;
8+
use Flow\ETL\Rows;
9+
10+
final readonly class RowsNormalizer
11+
{
12+
public function __construct(private EntryNormalizer $entryNormalizer)
13+
{
14+
}
15+
16+
/**
17+
* @return \Generator<array<null|bool|float|int|string>>
18+
*/
19+
public function normalize(Rows $rows) : \Generator
20+
{
21+
foreach ($rows as $row) {
22+
$normalizedRow = [];
23+
24+
foreach ($row->entries() as $entry) {
25+
$normalizedRow[] = $this->entryNormalizer->normalize($entry);
26+
}
27+
28+
yield $normalizedRow;
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Adapter\GoogleSheet\RowsNormalizer;
6+
7+
use function Flow\ETL\DSL\{date_interval_to_microseconds, type_json};
8+
use Flow\ETL\PHP\Type\Caster;
9+
use Flow\ETL\Row\Entry;
10+
11+
final readonly class EntryNormalizer
12+
{
13+
public function __construct(
14+
private Caster $caster,
15+
private string $dateTimeFormat = \DateTimeInterface::ATOM,
16+
private string $dateFormat = 'Y-m-d',
17+
) {
18+
}
19+
20+
/**
21+
* @param Entry<mixed, mixed> $entry
22+
*/
23+
public function normalize(Entry $entry) : string|float|int|bool|null
24+
{
25+
return match ($entry::class) {
26+
Entry\UuidEntry::class,
27+
Entry\XMLElementEntry::class,
28+
Entry\XMLEntry::class => $entry->toString(),
29+
Entry\DateTimeEntry::class => $entry->value()?->format($this->dateTimeFormat),
30+
Entry\DateEntry::class => $entry->value()?->format($this->dateFormat),
31+
Entry\TimeEntry::class => $entry->value() ? date_interval_to_microseconds($entry->value()) : null,
32+
Entry\EnumEntry::class => $entry->value()?->name,
33+
Entry\ListEntry::class,
34+
Entry\MapEntry::class,
35+
Entry\StructureEntry::class,
36+
Entry\JsonEntry::class => $this->caster->to(type_json())->value($entry->value()),
37+
default => $entry->value(),
38+
};
39+
}
40+
}

src/adapter/etl-adapter-google-sheet/src/Flow/ETL/Adapter/GoogleSheet/SheetRange.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function nextRows(int $count) : self
4242
public function toString() : string
4343
{
4444
return \sprintf(
45-
'%s!%s%d:%s%d',
45+
"'%s'!%s%d:%s%d",
4646
$this->columnRange->sheetName,
4747
$this->columnRange->startColumn,
4848
$this->startRow,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Adapter\GoogleSheet;
6+
7+
use Flow\ETL\Rows;
8+
9+
final readonly class SheetRangeCalculator
10+
{
11+
private ?string $endCell;
12+
13+
private string $startCell;
14+
15+
private function __construct(
16+
string $startCell = 'A1',
17+
?string $endCell = null,
18+
private ?string $sheetName = null,
19+
) {
20+
$this->startCell = strtoupper($startCell);
21+
$this->endCell = $endCell ? strtoupper($endCell) : null;
22+
}
23+
24+
public static function calculate(int $columns, int $rows, string $sheetName) : self
25+
{
26+
$startColumn = 'A';
27+
$startRow = 1;
28+
29+
$startColumnNumber = 0;
30+
31+
for ($i = 0, $length = strlen($startColumn); $i < $length; $i++) {
32+
$startColumnNumber = $startColumnNumber * 26 + (ord($startColumn[$i]) - 64);
33+
}
34+
35+
$endColumnNumber = $startColumnNumber + $columns - 1;
36+
$endColumn = '';
37+
38+
while ($endColumnNumber > 0) {
39+
$remainder = ($endColumnNumber - 1) % 26;
40+
$endColumn = chr(65 + $remainder) . $endColumn;
41+
$endColumnNumber = floor(($endColumnNumber - $remainder) / 26);
42+
}
43+
44+
return new self(
45+
$startColumn . $startRow,
46+
$endColumn . ($startRow + $rows - 1),
47+
$sheetName
48+
);
49+
}
50+
51+
public function getEndCell() : ?string
52+
{
53+
return $this->endCell;
54+
}
55+
56+
public function getEndCellColumn() : string
57+
{
58+
return (string) preg_replace('/[0-9]/', '', (string) $this->endCell);
59+
}
60+
61+
public function getEndCellRow() : int
62+
{
63+
return (int) preg_replace('/[A-Z]/', '', (string) $this->endCell);
64+
}
65+
66+
public function getSheetName() : ?string
67+
{
68+
return $this->sheetName;
69+
}
70+
71+
public function getStartCell() : string
72+
{
73+
return $this->startCell;
74+
}
75+
76+
public function getStartCellColumn() : string
77+
{
78+
return (string) preg_replace('/[0-9]/', '', $this->startCell);
79+
}
80+
81+
public function getStartCellRow() : int
82+
{
83+
return (int) preg_replace('/[A-Z]/', '', $this->startCell);
84+
}
85+
86+
public function skipRows(Rows $rows) : self
87+
{
88+
if (!$rows->count()) {
89+
return $this;
90+
}
91+
92+
if ($this->endCell === null) {
93+
return new self(
94+
$this->getStartCellColumn() . ($this->getStartCellRow() + $rows->count())
95+
);
96+
}
97+
98+
return new self(
99+
$this->getStartCellColumn() . $this->getStartCellRow(),
100+
$this->getEndCellColumn() . ($this->getEndCellRow() + $rows->count())
101+
);
102+
}
103+
104+
public function toString() : string
105+
{
106+
return ($this->sheetName ? ("'" . $this->sheetName . "'!") : '') . $this->startCell . ($this->endCell ? ':' . $this->endCell : '');
107+
}
108+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\ETL\Adapter\GoogleSheet;
6+
7+
enum ValueInputOption : string
8+
{
9+
case RAW = 'RAW';
10+
case USER_ENTERED = 'USER_ENTERED';
11+
12+
public function toArray() : array
13+
{
14+
return ['valueInputOption' => $this->value];
15+
}
16+
}

src/adapter/etl-adapter-google-sheet/src/Flow/ETL/Adapter/GoogleSheet/functions.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,25 @@ function from_google_sheet_columns(
8181
->withRowsPerPage($rows_per_page)
8282
->withOptions($options);
8383
}
84+
85+
#[DocumentationDSL(module: Module::GOOGLE_SHEET, type: Type::LOADER)]
86+
function to_google_sheet(
87+
array|Sheets $auth_config,
88+
string $spreadsheet_id,
89+
string $sheet_name,
90+
bool $with_header = true,
91+
array $options = [],
92+
) : GoogleSheetLoader {
93+
if ($auth_config instanceof Sheets) {
94+
$sheets = $auth_config;
95+
} else {
96+
$client = new Client();
97+
$client->setScopes(Sheets::SPREADSHEETS);
98+
$client->setAuthConfig($auth_config);
99+
$sheets = new Sheets($client);
100+
}
101+
102+
return (new GoogleSheetLoader($sheets, $spreadsheet_id, $sheet_name))
103+
->withHeader($with_header)
104+
->withOptions($options);
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"spreadsheetId": "1234567890",
3+
"updatedRange": "Sheet!A1:D4",
4+
"updatedRows": 4,
5+
"updatedColumns": 2,
6+
"updatedCells": 8,
7+
"updatedData": {
8+
"range": "Sheet!A1:C4",
9+
"majorDimension": "ROWS",
10+
"values": [
11+
["id", "name"],
12+
[12345, "Norbert"],
13+
[54321, "Joseph"],
14+
[666, "Dominik"]
15+
]
16+
}
17+
}

0 commit comments

Comments
 (0)