From 8cbaa97ae2d911c0965cffc0f6c8b54c68ea0e5a Mon Sep 17 00:00:00 2001 From: aVadim Date: Mon, 3 Jul 2023 20:44:16 +0300 Subject: [PATCH] Mapping import/export data --- README.md | 65 +++++++++++++++----- src/FastExcelLaravel/ExcelReader.php | 24 ++++++++ src/FastExcelLaravel/SheetReader.php | 56 +++++++++++++++--- src/FastExcelLaravel/SheetWriter.php | 21 +++++++ tests/FakeData.php | 8 +++ tests/FakeModel.php | 23 ++++++++ tests/FastExcelLaravelTest.php | 88 +++++++++++++++++++++++----- 7 files changed, 245 insertions(+), 40 deletions(-) create mode 100644 tests/FakeData.php diff --git a/README.md b/README.md index 1f28894..0dd9c3b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Using this library, you can export arrays, collections and models to a XLSX-file * You can set the height of the rows and the width of the columns (including auto width calculation) * Import workbooks and worksheets to Eloquent models very quickly and with minimal memory usage * Automatic field detection from imported table headers +* Mapping import/export data ## Installation @@ -34,7 +35,7 @@ And then you can use facade ```Excel``` $excel = \Excel::create(); // export model... -$sheet->->withHeadings()->exportModel(Users::class); +$sheet->withHeadings()->exportModel(Users::class); // and save XLSX-file to default storage $excel->saveTo('path/file.xlsx'); @@ -46,17 +47,19 @@ $excel->store('disk', 'path/file.xlsx'); $excel = \Excel::open(storage_path('path/file.xlsx')); // import records to database -$excel->importModel(User::class); +$excel->withHeadings()->importModel(User::class); ``` Jump To: * [Export Data](#export-data) * [Export a Model](#export-a-model) * [Export Any Collections and Arrays](#export-any-collections-and-arrays) + * [Mapping Export Data](#mapping-export-data) * [Advanced Usage for Data Export](#advanced-usage-for-data-export) * [Import Data](#import-data) * [Import a Model](#import-a-model) * [Advanced Usage for Data Import](#advanced-usage-for-data-import) + * [Mapping Import Data](#mapping-import-data) * [More Features](#more-features) * [Do you want to support FastExcelLaravel?](#do-you-want-to-support-fastexcellaravel) @@ -64,7 +67,7 @@ Jump To: ## Export Data ### Export a Model -Easy and fast export of a model. This way you export only model data without any styling +Easy and fast export of a model. This way you export only model data without headers and without any styling ```php // Create workbook with sheet named 'Users' @@ -91,6 +94,21 @@ $sheet->withHeadings() $excel->saveTo('path/file.xlsx'); ``` +### Mapping Export Data + +You can map the data that needs to be added as row + +```php +$sheet = $excel->getSheet(); +$sheet->mapping(function($model) { + return [ + 'id' => $model->id, 'date' => $model->created_at, 'name' => $model->first_name . $model->last_name, + ]; +})->exportModel(User::class); +$excel->save($testFileName); + +``` + ### Export Any Collections and Arrays ```php // Create workbook with sheet named 'Users' @@ -99,6 +117,11 @@ $excel = \Excel::create('Users'); $sheet = $excel->getSheet(); // Get users as collection $users = User::where('age', '>', 35)->get(); + +// Write attribute names +$sheet->writeRow(array_keys(User::getAttributes())); + +// Write all selected records $sheet->writeData($users); $sheet = $excel->makeSheet('Records'); @@ -186,7 +209,7 @@ $excel->saveTo($testFileName); ### Import a Model To import models, you can use method ```importModel()```. -By default, the first row is considered to contain the names of the fields +If the first row contains the names of the fields you can apply these using method ```withHeadings()``` ![import.jpg](import.jpg) @@ -194,35 +217,45 @@ By default, the first row is considered to contain the names of the fields // Open XLSX-file $excel = Excel::open($file); -// Import row to User model -$excel->importModel(User::class); +// Import a workbook to User model using the first row as attribute names +$excel->withHeadings()->importModel(User::class); // Done!!! ``` You can define the columns or cells from which you will import -![import2.jpg](import2.jpg) ```php -// Import row to User model from columns range B:E -$excel->importModel(User::class, 'B:D'); +// Import row to User model from columns range A:B - only 'name' and 'birthday' +$excel->withHeadings()->importModel(User::class, 'A:B'); +``` +![import2.jpg](import2.jpg) +```php // Import from cells range -$excel->importModel(User::class, 'B4:D7'); +$excel->withHeadings()->importModel(User::class, 'B4:D7'); // Define top left cell only -$excel->importModel(User::class, 'B4'); +$excel->withHeadings()->importModel(User::class, 'B4'); ``` -In the last two examples, we also assume that the first row of imported data (row 3) -is the names of the fields. +In the last two examples, we also assume that the first row of imported data (row 4) +is the names of the attributes. -However, you can set the correspondence between columns and field names yourself. -Then the first line of the imported data will be records for the model. +### Mapping Import Data + +However, you can set the correspondence between columns and field names yourself. ```php // Import row to User model from columns range B:E -$excel->importModel(User::class, 'B:D', ['B' => 'name', 'C' => 'birthday', 'D' => 'random']); +$excel->mapping(function ($record) { + return [ + 'id' => $record['A'], 'name' => $record['B'], 'birthday' => $record['C'], 'random' => $record['D'], + ]; +})->importModel(User::class, 'B:D'); // Define top left cell only +$excel->mapping(['B' => 'name', 'C' => 'birthday', 'D' => 'random'])->importModel(User::class, 'B5'); + +// Define top left cell only (shorter way) $excel->importModel(User::class, 'B5', ['B' => 'name', 'C' => 'birthday', 'D' => 'random']); ``` diff --git a/src/FastExcelLaravel/ExcelReader.php b/src/FastExcelLaravel/ExcelReader.php index 076ea72..2a8e119 100644 --- a/src/FastExcelLaravel/ExcelReader.php +++ b/src/FastExcelLaravel/ExcelReader.php @@ -21,6 +21,30 @@ public static function open(string $file): ExcelReader return new self($file); } + /** + * @param array|null $headers + * + * @return $this + */ + public function withHeadings(?array $headers = []): ExcelReader + { + $this->sheet()->withHeadings($headers); + + return $this; + } + + /** + * @param $callback + * + * @return $this + */ + public function mapping($callback): ExcelReader + { + $this->sheet()->mapping($callback); + + return $this; + } + /** * @param string $modelClass * @param string|bool|null $address diff --git a/src/FastExcelLaravel/SheetReader.php b/src/FastExcelLaravel/SheetReader.php index 3a185e3..2ec3e85 100644 --- a/src/FastExcelLaravel/SheetReader.php +++ b/src/FastExcelLaravel/SheetReader.php @@ -6,6 +6,48 @@ class SheetReader extends \avadim\FastExcelReader\Sheet { + private int $resultMode = 0; + private $mappingCallback = null; + + /** + * @param array|null $headers + * + * @return $this + */ + public function withHeadings(?array $headers = []): SheetReader + { + $this->resultMode = \avadim\FastExcelReader\Excel::KEYS_FIRST_ROW; + + return $this; + } + + /** + * @param $callback + * + * @return $this + */ + public function mapping($callback): SheetReader + { + if (is_array($callback)) { + $mapArray = $callback; + $callback = function ($row) use($mapArray) { + $record = []; + foreach ($row as $col => $value) { + if (isset($mapArray[$col])) { + $record[$mapArray[$col]] = $value; + } + else { + $record[$col] = $value; + } + } + return $record; + }; + } + $this->mappingCallback = $callback; + + return $this; + } + /** * Load models from Excel * loadModels(User::class) @@ -22,23 +64,19 @@ class SheetReader extends \avadim\FastExcelReader\Sheet */ public function importModel($modelClass, $address = null, $columns = null): SheetReader { - $resultMode = \avadim\FastExcelReader\Excel::KEYS_FIRST_ROW; - if ($columns === false) { - $resultMode = 0; - $columns = []; - } - elseif ($columns) { - $resultMode = 0; - } if ($address && is_string($address)) { $this->setReadArea($address); } - foreach ($this->nextRow($columns, $resultMode) as $rowData) { + foreach ($this->nextRow($columns, $this->resultMode) as $rowData) { /** @var Model $model */ $model = new $modelClass; + if ($this->mappingCallback) { + $rowData = call_user_func($this->mappingCallback, $rowData); + } $model->fill($rowData); $model->save(); } + $this->resultMode = 0; return $this; } diff --git a/src/FastExcelLaravel/SheetWriter.php b/src/FastExcelLaravel/SheetWriter.php index 28c0c70..7f93434 100644 --- a/src/FastExcelLaravel/SheetWriter.php +++ b/src/FastExcelLaravel/SheetWriter.php @@ -8,6 +8,8 @@ class SheetWriter extends Sheet { + private $mappingCallback = null; + private array $headers = []; private int $dataRowCount = 0; @@ -77,6 +79,9 @@ public function writeData($data, array $rowStyle = null, array $colStyles = null if ($this->dataRowCount === 0 && $this->headers) { $this->_writeHeader($record); } + if ($this->mappingCallback) { + $record = call_user_func($this->mappingCallback, $record); + } $this->writeRow($this->_toArray($record), $rowStyle, $colStyles); ++$this->dataRowCount; } @@ -86,6 +91,9 @@ public function writeData($data, array $rowStyle = null, array $colStyles = null if ($this->dataRowCount === 0 && $this->headers) { $this->_writeHeader($record); } + if ($this->mappingCallback) { + $record = call_user_func($this->mappingCallback, $record); + } $this->writeRow($this->_toArray($record), $rowStyle, $colStyles); ++$this->dataRowCount; } @@ -108,6 +116,7 @@ public function exportModel($model, array $rowStyle = null, array $colStyles = n yield $user; } }, $rowStyle, $colStyles); + $this->headers = []; return $this; } @@ -138,4 +147,16 @@ public function withHeadings(?array $headers = [], ?array $rowStyle = [], ?array return $this; } + /** + * @param $callback + * + * @return $this + */ + public function mapping($callback): SheetWriter + { + $this->mappingCallback = $callback; + + return $this; + } + } diff --git a/tests/FakeData.php b/tests/FakeData.php new file mode 100644 index 0000000..cf953f9 --- /dev/null +++ b/tests/FakeData.php @@ -0,0 +1,8 @@ + ++$id, 'integer' => 4573, 'date' => '1900-02-14', 'name' => 'James Bond'], + ['id' => ++$id, 'integer' => 982630, 'date' => '2179-08-12', 'name' => 'Ellen Louise Ripley'], + ['id' => ++$id, 'integer' => 7239, 'date' => '1753-01-31', 'name' => 'Captain Jack Sparrow'], +]; diff --git a/tests/FakeModel.php b/tests/FakeModel.php index ad1e8e9..208988d 100644 --- a/tests/FakeModel.php +++ b/tests/FakeModel.php @@ -19,6 +19,14 @@ public static function create(array $attributes = []): FakeModel return new self($attributes); } + public static function cursor(): \Generator + { + $data = include __DIR__ . '/FakeData.php'; + foreach ($data as $record) { + yield new self($record); + } + } + public function fill(array $attributes): FakeModel { $this->attributes = $attributes; @@ -37,4 +45,19 @@ public function __get($name) { return $this->attributes[$name] ?? null; } + + public function toArray(): array + { + return $this->attributes; + } + + public static function storageArray(): array + { + $result = []; + foreach (FakeModel::$storage as $item) { + $result[] = $item->toArray(); + } + + return $result; + } } \ No newline at end of file diff --git a/tests/FastExcelLaravelTest.php b/tests/FastExcelLaravelTest.php index 35c1609..32eed59 100644 --- a/tests/FastExcelLaravelTest.php +++ b/tests/FastExcelLaravelTest.php @@ -84,12 +84,7 @@ protected function getStyle($cell, $flat = false) protected function getDataArray(): array { - $id = 0; - return [ - ['id' => ++$id, 'integer' => 4573, 'date' => '1900-02-14', 'name' => 'James Bond'], - ['id' => ++$id, 'integer' => 982630, 'date' => '2179-08-12', 'name' => 'Ellen Louise Ripley'], - ['id' => ++$id, 'integer' => 7239, 'date' => '1753-01-31', 'name' => 'Captain Jack Sparrow'], - ]; + return include __DIR__ . '/FakeData.php'; } protected function getDataCollectionStd(): Collection @@ -120,6 +115,7 @@ protected function startExportTest($testFileName, $sheets = []): ExcelWriter elseif (file_exists(storage_path($testFileName))) { unlink(storage_path($testFileName)); } + FakeModel::$storage = []; return Excel::create($sheets); } @@ -315,20 +311,20 @@ public function testImportModel() $this->assertEquals('Sheet1', $excel->sheet()->name()); FakeModel::$storage = []; - $excel->importModel(FakeModel::class); + $excel->withHeadings()->importModel(FakeModel::class); $this->assertCount(3, FakeModel::$storage); $this->assertEquals('James Bond', FakeModel::$storage[0]->name); FakeModel::$storage = []; $excel->setDateFormat('Y-m-d'); - $excel->importModel(FakeModel::class, 'B4', ['A' => 'foo', 'B' => 'bar', 'C' => 'int']); + $excel->mapping(['A' => 'foo', 'B' => 'bar', 'C' => 'int'])->importModel(FakeModel::class, 'B4'); $this->assertEquals('1753-01-31', FakeModel::$storage[0]->bar); $testFileName = 'test_model2.xlsx'; $excel = Excel::open(storage_path($testFileName)); FakeModel::$storage = []; - $excel->importModel(FakeModel::class, 'b4'); + $excel->withHeadings()->importModel(FakeModel::class, 'b4'); $this->assertCount(3, FakeModel::$storage); $this->assertEquals('James Bond', FakeModel::$storage[0]->name); @@ -359,21 +355,83 @@ public function testImportModel() } - public function testExportArray0() + public function testExportImport() { - $testFileName = __DIR__ . '/test0.xlsx'; + $data = $this->getDataArray(); + $testFileName = __DIR__ . '/test_io.xlsx'; + + // ** 1 ** mapping import + FakeModel::$storage = []; $excel = $this->startExportTest($testFileName); + $sheet = $excel->getSheet(); - /** @var SheetWriter $sheet */ + $sheet->exportModel(FakeModel::class); + $excel->save($testFileName); + + $this->assertTrue(file_exists($testFileName)); + + $excel = Excel::open($testFileName); + $sheet = $excel->getSheet(); + $sheet->mapping(function ($record) { + return [ + 'id' => $record['A'], 'integer' => $record['B'], 'date' => $record['C'], 'name' => $record['D'], + ]; + })->importModel(FakeModel::class); + $this->assertEquals($data, FakeModel::storageArray()); + + // ** 2 ** mapping export/import + FakeModel::$storage = []; + $excel = $this->startExportTest($testFileName); $sheet = $excel->getSheet(); - $data = $this->getDataArray(); - $sheet->withHeadings()->setFieldFormats(['date' => '@date'])->writeData($data); + $sheet->mapping(function($model) { + return [ + 'id' => $model->id, 'integer' => $model->integer, 'date' => $model->date, 'name' => $model->name, + ]; + })->exportModel(FakeModel::class); + $excel->save($testFileName); + + $this->assertTrue(file_exists($testFileName)); + + $excel = Excel::open($testFileName); + $sheet = $excel->getSheet(); + $sheet->mapping(function ($record) { + return [ + 'id' => $record['A'], 'integer' => $record['B'], 'date' => $record['C'], 'name' => $record['D'], + ]; + })->importModel(FakeModel::class); + $this->assertEquals($data, FakeModel::storageArray()); + + // ** 3 ** export/import with heading + $excel = $this->startExportTest($testFileName); + + $sheet = $excel->getSheet(); + $sheet->withHeadings()->exportModel(FakeModel::class); + $excel->save($testFileName); + + $this->assertTrue(file_exists($testFileName)); + + $excel = Excel::open($testFileName); + $sheet = $excel->getSheet(); + $sheet->withHeadings()->importModel(FakeModel::class); + $this->assertEquals($data, FakeModel::storageArray()); + + // ** 4 ** format dates + $excel = $this->startExportTest($testFileName); + + $sheet = $excel->getSheet(); + $sheet->withHeadings()->setFieldFormats(['date' => '@date'])->exportModel(FakeModel::class); $excel->save($testFileName); $this->assertTrue(file_exists($testFileName)); - //$this->endExportTest($testFileName); + $excel = Excel::open($testFileName); + $excel->setDateFormat('Y-m-d'); + $sheet = $excel->getSheet(); + $sheet->withHeadings()->importModel(FakeModel::class); + $this->assertEquals($data, FakeModel::storageArray()); + + $this->endExportTest($testFileName); } } \ No newline at end of file