1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Mathielen\ImportEngineBundle\Command; |
4
|
|
|
|
5
|
|
|
use Ddeboer\DataImport\Filter\OffsetFilter; |
6
|
|
|
use Mathielen\DataImport\Event\ImportItemEvent; |
7
|
|
|
use Mathielen\ImportEngine\Event\ImportConfigureEvent; |
8
|
|
|
use Mathielen\ImportEngine\Event\ImportRequestEvent; |
9
|
|
|
use Mathielen\ImportEngine\Import\ImportBuilder; |
10
|
|
|
use Mathielen\ImportEngine\Import\Run\ImportRunner; |
11
|
|
|
use Mathielen\ImportEngine\ValueObject\ImportRequest; |
12
|
|
|
use Mathielen\ImportEngine\ValueObject\ImportRun; |
13
|
|
|
use Mathielen\ImportEngineBundle\Utils; |
14
|
|
|
use Symfony\Component\Console\Command\Command; |
15
|
|
|
use Symfony\Component\Console\Helper\ProgressBar; |
16
|
|
|
use Symfony\Component\Console\Helper\Table; |
17
|
|
|
use Symfony\Component\Console\Helper\TableSeparator; |
18
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
19
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
20
|
|
|
use Symfony\Component\Console\Input\InputOption; |
21
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
22
|
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
23
|
|
|
use Symfony\Component\Validator\ConstraintViolation; |
24
|
|
|
|
25
|
|
|
class ImportCommand extends Command |
26
|
|
|
{ |
27
|
|
|
const MAX_VIOLATION_ERRORS = 10; |
28
|
|
|
|
29
|
|
|
/** |
30
|
4 |
|
* @var ImportBuilder |
31
|
|
|
*/ |
32
|
4 |
|
private $importBuilder; |
33
|
4 |
|
|
34
|
4 |
|
/** |
35
|
4 |
|
* @var ImportRunner |
36
|
4 |
|
*/ |
37
|
4 |
|
private $importRunner; |
38
|
4 |
|
|
39
|
4 |
|
/** |
40
|
|
|
* @var EventDispatcherInterface |
41
|
4 |
|
*/ |
42
|
|
|
private $eventDispatcher; |
43
|
4 |
|
|
44
|
|
|
public function __construct( |
45
|
4 |
|
ImportBuilder $importBuilder, |
46
|
4 |
|
ImportRunner $importRunner, |
47
|
|
|
EventDispatcherInterface $eventDispatcher) |
48
|
|
|
{ |
49
|
4 |
|
parent::__construct('importengine:import'); |
50
|
|
|
|
51
|
4 |
|
$this->importBuilder = $importBuilder; |
52
|
|
|
$this->importRunner = $importRunner; |
53
|
4 |
|
$this->eventDispatcher = $eventDispatcher; |
54
|
|
|
} |
55
|
4 |
|
|
56
|
4 |
|
protected function configure() |
57
|
4 |
|
{ |
58
|
4 |
|
$this |
59
|
4 |
|
->setDescription('Imports data with a definied importer') |
60
|
|
|
->addArgument('source_id', InputArgument::OPTIONAL, "id of source. Different StorageProviders need different id data.\n- upload, directory: \"<path/to/file>\"\n- doctrine: \"<id of query>\"\n- service: \"<service>.<method>[?arguments_like_url_query]\"") |
61
|
|
|
->addArgument('source_provider', InputArgument::OPTIONAL, 'id of source provider', 'default') |
62
|
|
|
->addOption('importer', 'i', InputOption::VALUE_REQUIRED, 'id/name of importer') |
63
|
|
|
->addOption('context', 'c', InputOption::VALUE_REQUIRED, 'Supply optional context information to import. Supply key-value data in query style: key=value&otherkey=othervalue&...') |
64
|
|
|
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit imported rows') |
65
|
4 |
|
->addOption('dryrun', 'd', InputOption::VALUE_NONE, 'Do not import - Validation only') |
66
|
|
|
->addOption('validate-and-run', null, InputOption::VALUE_NONE, 'Validate and run if no error') |
67
|
4 |
|
; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
71
|
4 |
|
{ |
72
|
4 |
|
$importerId = $input->getOption('importer'); |
73
|
|
|
$sourceProviderId = $input->getArgument('source_provider'); |
74
|
4 |
|
$sourceId = $input->getArgument('source_id'); |
75
|
|
|
$isDryrun = $input->getOption('dryrun'); |
76
|
4 |
|
$isValidateAndRun = $input->getOption('validate-and-run'); |
77
|
|
|
if ($isDryrun && $isValidateAndRun) { |
78
|
4 |
|
throw new \InvalidArgumentException("Cannot invoke with dryrun and validate-and-run"); |
79
|
4 |
|
} |
80
|
|
|
$runMode = $isDryrun ? 'dryrun' : $isValidateAndRun ? 'validate_and_run' : 'run'; |
81
|
|
|
if ($context = $input->getOption('context')) { |
82
|
4 |
|
//parse key=value&key=value string to array |
83
|
1 |
|
if (strpos($context, '=') !== false) { |
84
|
|
|
parse_str($input->getOption('context'), $context); |
85
|
|
|
} |
86
|
|
|
} |
87
|
1 |
|
$limit = $input->getOption('limit'); |
88
|
|
|
|
89
|
|
|
if (empty($importerId) && empty($sourceId)) { |
90
|
|
|
throw new \InvalidArgumentException('There must be at least an importerId with a configured source-definition given or a sourceId which can be automatically recognized by pre-conditions.'); |
91
|
4 |
|
} |
92
|
|
|
|
93
|
|
|
$this->import($output, $importerId, $sourceProviderId, $sourceId, $context, $limit, $runMode); |
94
|
|
|
} |
95
|
2 |
|
|
96
|
|
|
protected function import(OutputInterface $output, $importerId, $sourceProviderId, $sourceId, $context = null, $limit = null, $runMode = 'run') |
97
|
|
|
{ |
98
|
|
|
$output->writeln("Commencing import with mode <comment>$runMode</comment> using importer ".(empty($importerId) ? '<comment>unknown</comment>' : "<info>$importerId</info>")." with source provider <info>$sourceProviderId</info> and source id <info>$sourceId</info>"); |
99
|
4 |
|
|
100
|
|
|
$sourceId = Utils::parseSourceId($sourceId); |
101
|
4 |
|
$progress = new ProgressBar($output); |
102
|
|
|
|
103
|
4 |
|
//set limit |
104
|
|
|
if ($limit) { |
105
|
|
|
$output->writeln("Limiting import to <info>$limit</info> rows."); |
106
|
4 |
|
|
107
|
|
|
$this->eventDispatcher->addListener(ImportConfigureEvent::AFTER_BUILD, function (ImportConfigureEvent $event) use ($limit) { |
108
|
|
|
$event->getImport()->importer()->filters()->add(new OffsetFilter(0, $limit)); |
109
|
4 |
|
}); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
//show discovered importer id |
113
|
|
|
if (empty($importerId)) { |
114
|
|
|
$this->eventDispatcher->addListener(ImportRequestEvent::DISCOVERED, function (ImportRequestEvent $event) use ($output) { |
115
|
|
|
$importerId = $event->getImportRequest()->getImporterId(); |
116
|
|
|
$output->writeln("Importer discovered: <info>$importerId</info>"); |
117
|
|
|
}); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$importRequest = new ImportRequest($sourceId, $sourceProviderId, $importerId, Utils::whoAmI().'@CLI', $context); |
|
|
|
|
121
|
|
|
|
122
|
4 |
|
$import = $this->importBuilder->buildFromRequest($importRequest); |
123
|
|
|
|
124
|
|
|
//apply context info from commandline |
125
|
4 |
|
$importRun = $import->getRun(); |
126
|
4 |
|
|
127
|
1 |
|
//status callback |
128
|
|
|
$this->eventDispatcher->addListener(ImportItemEvent::AFTER_READ, function (ImportItemEvent $event) use ($output, &$progress) { |
129
|
3 |
|
/** @var ImportRun $importRun */ |
130
|
|
|
$importRun = $event->getContext()->getRun(); |
131
|
|
|
$stats = $importRun->getStatistics(); |
132
|
4 |
|
$processed = isset($stats['processed']) ? $stats['processed'] : 0; |
133
|
4 |
|
$max = $importRun->getInfo()['count']; |
134
|
4 |
|
|
135
|
4 |
|
if ($progress->getMaxSteps() != $max) { |
136
|
|
|
$progress = new ProgressBar($output, $max); |
137
|
4 |
|
$progress->start(); |
138
|
|
|
} |
139
|
4 |
|
|
140
|
|
|
$progress->setProgress($processed); |
141
|
4 |
|
}); |
142
|
4 |
|
|
143
|
4 |
|
if ($runMode === 'dryrun') { |
144
|
4 |
|
$this->importRunner->dryRun($import); |
145
|
|
|
} elseif ($runMode === 'validate_and_run') { |
146
|
4 |
|
$this->importRunner->dryRun($import); |
147
|
4 |
|
$this->importRunner->run($import); |
148
|
|
|
} else { |
149
|
4 |
|
$this->importRunner->run($import); |
150
|
|
|
} |
151
|
4 |
|
|
152
|
4 |
|
$progress->finish(); |
153
|
|
|
$output->writeln(''); |
154
|
|
|
$output->writeln('<info>Import done</info>'); |
155
|
|
|
$output->writeln(''); |
156
|
|
|
|
157
|
|
|
$this->writeStatistics($importRun->getStatistics(), new Table($output)); |
158
|
|
|
|
159
|
|
|
$this->writeValidationViolations( |
160
|
|
|
$import |
161
|
|
|
->importer() |
162
|
|
|
->validation() |
163
|
|
|
->getViolations(), |
164
|
|
|
new Table($output)); |
165
|
|
|
|
166
|
|
|
$output->writeln(''); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
protected function writeValidationViolations(array $violations, Table $table) |
170
|
|
|
{ |
171
|
|
|
if (empty($violations)) { |
172
|
|
|
return; |
173
|
|
|
} |
174
|
|
|
$violations = $violations['source'] + $violations['target']; |
175
|
|
|
|
176
|
|
|
$table |
177
|
|
|
->setHeaders(array('Constraint', 'Occurrences (lines)')) |
178
|
|
|
; |
179
|
|
|
|
180
|
|
|
$tree = []; |
181
|
|
|
foreach ($violations as $line => $validations) { |
182
|
|
|
/** @var ConstraintViolation $validation */ |
183
|
|
|
foreach ($validations as $validation) { |
184
|
|
|
$key = $validation->__toString(); |
185
|
|
|
if (!isset($tree[$key])) { |
186
|
|
|
$tree[$key] = []; |
187
|
|
|
} |
188
|
|
|
$tree[$key][] = $line; |
189
|
|
|
} |
190
|
4 |
|
} |
191
|
|
|
|
192
|
4 |
|
$i = 0; |
193
|
4 |
|
foreach ($tree as $violation => $lines) { |
194
|
|
|
$table->addRow([$violation, implode(', ', Utils::numbersToRangeText($lines))]); |
195
|
|
|
++$i; |
196
|
|
|
|
197
|
|
|
if ($i === self::MAX_VIOLATION_ERRORS) { |
198
|
4 |
|
$table->addRow(new TableSeparator()); |
199
|
4 |
|
$table->addRow(array(null, 'There are more errors...')); |
200
|
|
|
|
201
|
4 |
|
break; |
202
|
4 |
|
} |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
if ($i > 0) { |
206
|
|
|
$table->render(); |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
protected function writeStatistics(array $statistics, Table $table) |
211
|
|
|
{ |
212
|
|
|
$rows = []; |
213
|
|
|
foreach ($statistics as $k => $v) { |
214
|
|
|
$rows[] = [$k, $v]; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$table |
218
|
|
|
->setHeaders(array('Statistics')) |
219
|
|
|
->setRows($rows) |
220
|
|
|
; |
221
|
|
|
$table->render(); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.