Skip to content

Commit 4e6874a

Browse files
Merge pull request #22 from botble/21-add-command-to-execute-importexport-from-command-line
2 parents be61180 + db5f435 commit 4e6874a

File tree

4 files changed

+320
-2
lines changed

4 files changed

+320
-2
lines changed

src/Commands/ExportCommand.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace Botble\DataSynchronize\Commands;
4+
5+
use Botble\DataSynchronize\Exporter\Exporter;
6+
use Exception;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Contracts\Console\PromptsForMissingInput;
9+
10+
use function Laravel\Prompts\search;
11+
use function Laravel\Prompts\select;
12+
13+
use function Laravel\Prompts\text;
14+
15+
use SplFileInfo;
16+
use Symfony\Component\Console\Attribute\AsCommand;
17+
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Finder\Finder;
21+
22+
#[AsCommand(name: 'data-synchronize:export', description: 'Export data from database to Excel/Csv file')]
23+
class ExportCommand extends Command implements PromptsForMissingInput
24+
{
25+
public function handle(): void
26+
{
27+
$exporter = $this->argument('exporter') ?: search(
28+
label: 'Which exporter do you want to use?',
29+
options: fn (string $value) => array_filter(
30+
$this->possibleExporters(),
31+
fn (string $exporter) => str_contains(strtolower($exporter), strtolower($value))
32+
),
33+
);
34+
$path = $this->argument('path') ?: text(
35+
'Where do you want to save the file?',
36+
'E.g. storage/app/exports',
37+
);
38+
$format = $this->option('format') ?: select(
39+
label: 'Which format do you want to export?',
40+
options: ['csv' => 'CSV', 'xlsx' => 'XLSX'],
41+
default: 'csv',
42+
);
43+
44+
if (! class_exists($exporter)) {
45+
$this->components->error('Exporter class does not exist');
46+
47+
exit(self::FAILURE);
48+
}
49+
50+
$exporter = new $exporter();
51+
52+
if (! $exporter instanceof Exporter) {
53+
$this->components->error('Exporter class must be an instance of ' . Exporter::class);
54+
55+
exit(self::FAILURE);
56+
}
57+
58+
$exporter->format($format);
59+
60+
$this->components->info("Exporting {$exporter->getLabel()} to <comment>{$path}</comment>");
61+
62+
try {
63+
$exporter->export()
64+
->getFile()
65+
->move($path, $exporter->getExportFileName());
66+
} catch (Exception $e) {
67+
$this->components->error($e->getMessage());
68+
69+
exit(self::FAILURE);
70+
}
71+
72+
$this->components->info(
73+
"{$exporter->getLabel()} has been exported to <comment>{$path}/{$exporter->getExportFileName()}</comment>"
74+
);
75+
76+
exit(self::SUCCESS);
77+
}
78+
79+
protected function getOptions(): array
80+
{
81+
return [
82+
['format', null, InputOption::VALUE_OPTIONAL, 'The format of the file (csv, xls, xlsx)'],
83+
];
84+
}
85+
86+
protected function getArguments(): array
87+
{
88+
return [
89+
['exporter', InputArgument::OPTIONAL, 'The exporter class name'],
90+
['path', InputArgument::OPTIONAL, 'The path to save the file'],
91+
];
92+
}
93+
94+
protected function promptForMissingArgumentsUsing(): array
95+
{
96+
return [
97+
'exporter' => ['What is the exporter class name?', 'E.g. Botble\Blog\Exporters\PostExporter'],
98+
'path' => ['Where do you want to save the file?', 'E.g. storage/app/exports'],
99+
];
100+
}
101+
102+
protected function possibleExporters(): array
103+
{
104+
$exporters = [];
105+
106+
collect(
107+
Finder::create()
108+
->files()
109+
->in(platform_path())
110+
->name('*.php')
111+
->contains(Exporter::class)
112+
)
113+
->map(function (SplFileInfo $file) use (&$exporters) {
114+
$class = $this->resolveExporterNamespace($file->getPathname());
115+
116+
if (
117+
class_exists($class)
118+
&& is_subclass_of($class, Exporter::class)
119+
) {
120+
$exporters[] = $class;
121+
}
122+
});
123+
124+
return array_combine($exporters, $exporters);
125+
}
126+
127+
protected function resolveExporterNamespace(string $path): string
128+
{
129+
$content = file_get_contents($path);
130+
$namespace = str($content)->after('namespace ')->before(';')->trim();
131+
$basename = basename($path, '.php');
132+
133+
return $namespace . '\\' . $basename;
134+
}
135+
}

src/Commands/ImportCommand.php

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
namespace Botble\DataSynchronize\Commands;
4+
5+
use Botble\DataSynchronize\Importer\Importer;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Contracts\Console\PromptsForMissingInput;
8+
use Illuminate\Support\Facades\Storage;
9+
10+
use function Laravel\Prompts\search;
11+
use function Laravel\Prompts\text;
12+
13+
use SplFileInfo;
14+
15+
use Symfony\Component\Console\Attribute\AsCommand;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Finder\Finder;
19+
20+
#[AsCommand(name: 'data-synchronize:import', description: 'Import data from Excel/CSV file')]
21+
class ImportCommand extends Command implements PromptsForMissingInput
22+
{
23+
public function handle(): void
24+
{
25+
$importer = $this->argument('importer') ?: search(
26+
label: 'Which importer do you want to use?',
27+
options: fn (string $value) => array_filter(
28+
$this->possibleImporters(),
29+
fn (string $importer) => str_contains(strtolower($importer), strtolower($value))
30+
),
31+
);
32+
$path = $this->argument('path') ?: text(
33+
'Where is the source Excel/CSV file?',
34+
);
35+
$limit = (int) $this->option('limit') ?: 100;
36+
37+
if (! file_exists($path)) {
38+
$this->components->error('File does not exist');
39+
40+
exit(self::FAILURE);
41+
}
42+
43+
if (! class_exists($importer)) {
44+
$this->components->error('Importer class does not exist');
45+
46+
exit(self::FAILURE);
47+
}
48+
49+
$importer = new $importer();
50+
51+
if (! $importer instanceof Importer) {
52+
$this->components->error('Importer class must be an instance of ' . Importer::class);
53+
54+
exit(self::FAILURE);
55+
}
56+
57+
$basename = basename($path);
58+
$storage = Storage::disk('local');
59+
$storagePath = config('packages.data-synchronize.data-synchronize.storage.path');
60+
$filePath = sprintf('%s/%s', $storagePath, $basename);
61+
62+
$storage->put($filePath, file_get_contents($path));
63+
64+
$this->validateData($importer, $basename, $limit);
65+
66+
$this->importData($importer, $basename, $limit);
67+
68+
$storage->delete($filePath);
69+
70+
exit(self::SUCCESS);
71+
}
72+
73+
protected function validateData(Importer $importer, string $basename, int $limit = 100): void
74+
{
75+
$offset = 0;
76+
77+
$this->components->info('Validating data...');
78+
79+
do {
80+
$response = $importer->validate($basename, $offset, $limit);
81+
$offset = $response->getNextOffset();
82+
83+
$this->components->info("Validated data from {$response->getFromOffset()} to {$response->getNextOffset()}");
84+
} while ($response->getNextOffset() < $response->total);
85+
86+
$this->components->info('Validated data successfully');
87+
}
88+
89+
protected function importData(Importer $importer, string $basename, int $limit = 100): void
90+
{
91+
$this->components->info('Importing data...');
92+
93+
$total = 0;
94+
$offset = 0;
95+
96+
do {
97+
$response = $importer->import($basename, $offset, $limit);
98+
$offset = $response->getNextOffset();
99+
$total += $response->imported;
100+
101+
$from = $response->getFromOffset();
102+
$to = $response->getNextOffset();
103+
104+
if ($from > $to) {
105+
if ($total > 0) {
106+
$this->components->info($importer->getDoneMessage($total));
107+
} else {
108+
$this->components->info('Your data is up to date');
109+
}
110+
} else {
111+
$this->components->info("Imported {$importer->getLabel()} from {$from} to {$to}");
112+
}
113+
} while ($from <= $to);
114+
}
115+
116+
protected function getOptions(): array
117+
{
118+
return [
119+
'limit' => ['limit', null, InputOption::VALUE_OPTIONAL, 'The limit of records to import'],
120+
];
121+
}
122+
123+
protected function getArguments(): array
124+
{
125+
return [
126+
['importer', InputArgument::OPTIONAL, 'The exporter class name'],
127+
['path', InputArgument::OPTIONAL, 'The path to the source Excel/CSV file'],
128+
];
129+
}
130+
131+
protected function promptForMissingArgumentsUsing(): array
132+
{
133+
return [
134+
'importer' => ['What is the importer class name?', 'E.g. Botble\Blog\Importers\PostImporter'],
135+
'path' => ['Where is the source Excel/CSV file?', 'E.g. ~/Downloads/posts.xlsx'],
136+
];
137+
}
138+
139+
protected function possibleImporters(): array
140+
{
141+
$importers = [];
142+
143+
collect(
144+
Finder::create()
145+
->files()
146+
->in(platform_path())
147+
->name('*.php')
148+
->contains(Importer::class)
149+
)
150+
->map(function (SplFileInfo $file) use (&$importers) {
151+
$class = $this->resolveExporterNamespace($file->getPathname());
152+
153+
if (
154+
class_exists($class)
155+
&& is_subclass_of($class, Importer::class)
156+
) {
157+
$importers[] = $class;
158+
}
159+
});
160+
161+
return array_combine($importers, $importers);
162+
}
163+
164+
protected function resolveExporterNamespace(string $path): string
165+
{
166+
$content = file_get_contents($path);
167+
$namespace = str($content)->after('namespace ')->before(';')->trim();
168+
$basename = basename($path, '.php');
169+
170+
return $namespace . '\\' . $basename;
171+
}
172+
}

src/Exporter/Exporter.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
use Botble\Base\Facades\BaseHelper;
77
use Botble\DataSynchronize\Concerns\Exporter\HasEmptyState;
88
use Botble\DataSynchronize\Enums\ExportColumnType;
9+
use Carbon\Carbon;
910
use Illuminate\Contracts\View\View;
11+
use Illuminate\Support\Str;
1012
use Maatwebsite\Excel\Concerns\FromCollection;
1113
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
1214
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
@@ -173,7 +175,12 @@ public function getColumns(): array
173175

174176
public function getExportFileName(): string
175177
{
176-
return str_replace(' ', '-', $this->getLabel());
178+
return sprintf(
179+
'%s-%s.%s',
180+
Str::slug($this->getLabel()),
181+
BaseHelper::formatDateTime(Carbon::now(), 'Y-m-d-H-i-s'),
182+
$this->format
183+
);
177184
}
178185

179186
public function render(): View
@@ -201,7 +208,7 @@ public function export(): BinaryFileResponse
201208
},
202209
];
203210

204-
return ExcelFacade::download($this, "{$this->getExportFileName()}.{$this->format}", $writeType, $headers);
211+
return ExcelFacade::download($this, $this->getExportFileName(), $writeType, $headers);
205212
}
206213

207214
public function acceptedColumns(?array $columns): self

src/Providers/DataSynchronizeServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use Botble\Base\Supports\ServiceProvider;
88
use Botble\Base\Traits\LoadAndPublishDataTrait;
99
use Botble\DataSynchronize\Commands\ClearChunksCommand;
10+
use Botble\DataSynchronize\Commands\ExportCommand;
1011
use Botble\DataSynchronize\Commands\ExportControllerMakeCommand;
1112
use Botble\DataSynchronize\Commands\ExporterMakeCommand;
13+
use Botble\DataSynchronize\Commands\ImportCommand;
1214
use Botble\DataSynchronize\Commands\ImportControllerMakeCommand;
1315
use Botble\DataSynchronize\Commands\ImporterMakeCommand;
1416
use Botble\DataSynchronize\PanelSections\ExportPanelSection;
@@ -38,6 +40,8 @@ public function boot(): void
3840
ImportControllerMakeCommand::class,
3941
ExportControllerMakeCommand::class,
4042
ClearChunksCommand::class,
43+
ExportCommand::class,
44+
ImportCommand::class,
4145
]);
4246

4347
$this->app->afterResolving(Schedule::class, function (Schedule $schedule) {

0 commit comments

Comments
 (0)