1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TreeHouse\IoBundle\Command; |
4
|
|
|
|
5
|
|
|
use Symfony\Component\Console\Command\Command; |
6
|
|
|
use Symfony\Component\Console\Helper\ProgressBar; |
7
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
8
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
9
|
|
|
use Symfony\Component\Console\Input\InputOption; |
10
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
11
|
|
|
use TreeHouse\IoBundle\Export\FeedExporter; |
12
|
|
|
|
13
|
|
|
class ExportInspectCommand extends Command |
14
|
|
|
{ |
15
|
|
|
const EVALUATE_COUNT = 'count'; |
16
|
|
|
const EVALUATE_EXPRESSION = 'expression'; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @var FeedExporter |
20
|
|
|
*/ |
21
|
|
|
protected $exporter; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var \XMLReader |
25
|
|
|
*/ |
26
|
|
|
protected $reader; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @param FeedExporter $exporter |
30
|
|
|
*/ |
31
|
|
|
public function __construct(FeedExporter $exporter) |
32
|
|
|
{ |
33
|
|
|
$this->exporter = $exporter; |
34
|
|
|
|
35
|
|
|
parent::__construct(); |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @inheritdoc |
40
|
|
|
*/ |
41
|
|
|
protected function configure() |
42
|
|
|
{ |
43
|
|
|
$this->addArgument('type', InputArgument::REQUIRED, 'The type for which the feed needs to be inspected'); |
44
|
|
|
$this->addOption('filter', null, InputOption::VALUE_OPTIONAL, 'XPath expression to filter nodes, use <comment>x</comment> as the namespace alias', '//*'); |
45
|
|
|
$this->addOption('expression', null, InputOption::VALUE_OPTIONAL, 'XPath expression to evaluate nodes, use <comment>x</comment> as the namespace alias'); |
46
|
|
|
$this->addOption( |
47
|
|
|
'evaluate', |
48
|
|
|
null, |
49
|
|
|
InputOption::VALUE_OPTIONAL, |
50
|
|
|
sprintf( |
51
|
|
|
'Method to use for evaluation, supported methods are <info>%s</info> and <info>%s</info>. Defaults to <info>%s</info>', |
52
|
|
|
self::EVALUATE_COUNT, |
53
|
|
|
self::EVALUATE_EXPRESSION, |
54
|
|
|
self::EVALUATE_COUNT |
55
|
|
|
) |
56
|
|
|
); |
57
|
|
|
$this->setName('io:export:inspect'); |
58
|
|
|
$this->setDescription('Inspect/query an exported feed'); |
59
|
|
|
$this->setHelp(<<<EOT |
60
|
|
|
This command inspects an exported feed. It walks through the entire feed, |
61
|
|
|
optionally applying a filter to only include specific items. After collecting |
62
|
|
|
all matched items it evaluates the output. The evaluated result is then |
63
|
|
|
displayed in the console. |
64
|
|
|
|
65
|
|
|
Evaluation can be two options: |
66
|
|
|
|
67
|
|
|
1. <comment>count</comment>: counts the number of matched items. |
68
|
|
|
2. <comment>expression</comment>: executes a given XPath expression against each matched items. |
69
|
|
|
|
70
|
|
|
The expressions in the <comment>--filter</comment> and <comment>--expression</comment> options |
71
|
|
|
both have a namespace registered with the alias <comment>x</comment>, which you need to use |
72
|
|
|
when targeting nodes. |
73
|
|
|
|
74
|
|
|
<option=bold>Examples:</> |
75
|
|
|
|
76
|
|
|
Counting the number of items in the <comment>acme</comment> feed: |
77
|
|
|
|
78
|
|
|
$ <info>php app/console %command.name% acme</info> |
79
|
|
|
# 1234 |
80
|
|
|
|
81
|
|
|
Check if an item with a specific id attribute exists in the feed: |
82
|
|
|
|
83
|
|
|
$ <info>php app/console %command.name% acme --filter="@id=1234"</info> |
84
|
|
|
# 1 |
85
|
|
|
|
86
|
|
|
Counting all items without any photos (ie: an empty <comment>photos</comment> node, |
87
|
|
|
in which there would normally be one or more <comment>photo</comment> subnodes: |
88
|
|
|
|
89
|
|
|
$ <info>php app/console %command.name% acme --filter="count(x:photos/x:photo) < 1"</info> |
90
|
|
|
# 345 |
91
|
|
|
|
92
|
|
|
Selecting the id of all items with title "foo": |
93
|
|
|
|
94
|
|
|
$ <info>php app/console %command.name% acme --filter="x:title[.='foo']" --evaluate=expression --expression="@id"</info> |
95
|
|
|
# 34547 |
96
|
|
|
# 67878 |
97
|
|
|
# 12945 |
98
|
|
|
# 48784 |
99
|
|
|
# 56978 |
100
|
|
|
# ...etc |
101
|
|
|
|
102
|
|
|
EOT |
103
|
|
|
); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @inheritdoc |
108
|
|
|
*/ |
109
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
110
|
|
|
{ |
111
|
|
|
$type = $this->exporter->getType($input->getArgument('type')); |
|
|
|
|
112
|
|
|
$file = $this->exporter->getFeedFilename($type); |
113
|
|
|
|
114
|
|
|
if (!file_exists($file)) { |
115
|
|
|
$output->writeln(sprintf('<error>Feed "%s" has not yet been exported</error>', $type->getName())); |
116
|
|
|
|
117
|
|
|
return 1; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$evaluationMethod = $input->getOption('evaluate'); |
121
|
|
|
$evaluationExpression = $input->getOption('expression'); |
122
|
|
|
|
123
|
|
|
if (!$evaluationMethod && $evaluationExpression) { |
124
|
|
|
$evaluationMethod = self::EVALUATE_EXPRESSION; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
if (!$evaluationMethod) { |
128
|
|
|
$evaluationMethod = self::EVALUATE_COUNT; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
list($results, $total) = $this->inspect($output, new \SplFileInfo($file), $input->getOption('filter')); |
132
|
|
|
|
133
|
|
|
switch ($evaluationMethod) { |
134
|
|
|
case self::EVALUATE_COUNT: |
135
|
|
|
if ($evaluationExpression) { |
136
|
|
|
throw new \InvalidArgumentException('You cannot use an expression when using count evaluation'); |
137
|
|
|
} |
138
|
|
|
$this->evaluateCount($output, $results, $total); |
139
|
|
|
|
140
|
|
|
break; |
141
|
|
|
case self::EVALUATE_EXPRESSION: |
142
|
|
|
$this->evaluateResult($output, $evaluationExpression, $results, $total); |
143
|
|
|
|
144
|
|
|
break; |
145
|
|
|
|
146
|
|
|
default: |
147
|
|
|
throw new \InvalidArgumentException(sprintf('Invalid evaluation method: %s', $evaluationMethod)); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
return 0; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @param OutputInterface $output |
155
|
|
|
* @param \SplFileInfo $feed |
156
|
|
|
* @param string $filterExpression |
157
|
|
|
* |
158
|
|
|
* @return array<array, integer> |
|
|
|
|
159
|
|
|
*/ |
160
|
|
|
protected function inspect(OutputInterface $output, \SplFileInfo $feed, $filterExpression) |
161
|
|
|
{ |
162
|
|
|
$options = LIBXML_NOENT | LIBXML_NONET | LIBXML_COMPACT | LIBXML_PARSEHUGE | LIBXML_NOERROR | LIBXML_NOWARNING; |
163
|
|
|
$this->reader = new \XMLReader($options); |
|
|
|
|
164
|
|
|
$this->reader->open($feed->getPathname()); |
165
|
|
|
$this->reader->setParserProperty(\XMLReader::SUBST_ENTITIES, true); |
166
|
|
|
|
167
|
|
|
libxml_clear_errors(); |
168
|
|
|
libxml_use_internal_errors(true); |
169
|
|
|
libxml_disable_entity_loader(true); |
170
|
|
|
|
171
|
|
|
$total = 0; |
172
|
|
|
$results = []; |
173
|
|
|
|
174
|
|
|
$output->writeln( |
175
|
|
|
sprintf('Reading <comment>%s</comment>', $feed->getFilename()) |
176
|
|
|
); |
177
|
|
|
|
178
|
|
|
if ($filterExpression) { |
179
|
|
|
$output->writeln(sprintf('Filtering nodes with expression "<info>%s</info>"', $filterExpression)); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
$progress = new ProgressBar($output); |
183
|
|
|
$progress->start(); |
184
|
|
|
|
185
|
|
|
// go through the whole thing |
186
|
|
|
while ($this->reader->read()) { |
187
|
|
|
if ($this->reader->nodeType === \XMLReader::ELEMENT && $this->reader->name === 'listing') { |
188
|
|
|
$progress->advance(); |
189
|
|
|
++$total; |
190
|
|
|
|
191
|
|
|
$node = $this->reader->expand(); |
192
|
|
|
$doc = new \DOMDocument(); |
193
|
|
|
$doc->appendChild($node); |
194
|
|
|
|
195
|
|
|
$xpath = new \DOMXPath($doc); |
196
|
|
|
$xpath->registerNamespace('x', $doc->lookupNamespaceUri($doc->namespaceURI)); |
197
|
|
|
$query = $xpath->evaluate($filterExpression, $node); |
198
|
|
|
|
199
|
|
|
$result = $query instanceof \DOMNodeList ? $query->length : !empty($query); |
200
|
|
|
if ($result) { |
201
|
|
|
$results[] = $node; |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
$progress->finish(); |
207
|
|
|
$output->writeln(''); |
208
|
|
|
|
209
|
|
|
return [$results, $total]; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param OutputInterface $output |
214
|
|
|
* @param array $results |
215
|
|
|
* @param int $total |
216
|
|
|
*/ |
217
|
|
|
protected function evaluateCount(OutputInterface $output, array $results, $total) |
218
|
|
|
{ |
219
|
|
|
$results = sizeof($results); |
220
|
|
|
|
221
|
|
|
$output->writeln( |
222
|
|
|
sprintf( |
223
|
|
|
'<info>%d</info>/<info>%d</info> nodes (<info>%d%%</info>) match your expression:', |
224
|
|
|
$results, |
225
|
|
|
$total, |
226
|
|
|
round($results / $total * 100) |
227
|
|
|
) |
228
|
|
|
); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* @param OutputInterface $output |
233
|
|
|
* @param string $evaluationExpression |
234
|
|
|
* @param array $results |
235
|
|
|
* @param int $total |
236
|
|
|
*/ |
237
|
|
|
protected function evaluateResult(OutputInterface $output, $evaluationExpression, array $results, $total) |
238
|
|
|
{ |
239
|
|
|
$msg = sprintf( |
240
|
|
|
'<info>%d</info>/<info>%d</info> nodes (<info>%d%%</info>) match your expression:', |
241
|
|
|
sizeof($results), |
242
|
|
|
$total, |
243
|
|
|
round(sizeof($results) / $total * 100) |
244
|
|
|
); |
245
|
|
|
|
246
|
|
|
$output->writeln($msg); |
247
|
|
|
$output->writeln(str_pad('', strlen(strip_tags($msg)), '-', STR_PAD_LEFT)); |
248
|
|
|
|
249
|
|
|
/** @var \DOMNode $node */ |
250
|
|
|
foreach ($results as $node) { |
251
|
|
|
$xpath = new \DOMXPath($node->ownerDocument); |
252
|
|
|
$xpath->registerNamespace('x', $node->ownerDocument->lookupNamespaceUri($node->ownerDocument->namespaceURI)); |
253
|
|
|
$evaluation = $xpath->evaluate($evaluationExpression, $node); |
254
|
|
|
|
255
|
|
|
$output->writeln($this->serialize($evaluation)); |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param mixed $result |
261
|
|
|
* |
262
|
|
|
* @return string |
263
|
|
|
*/ |
264
|
|
|
protected function serialize($result) |
265
|
|
|
{ |
266
|
|
|
if ($result instanceof \DOMNodeList) { |
267
|
|
|
$serialized = []; |
268
|
|
|
foreach ($result as $node) { |
269
|
|
|
$serialized[] = $this->serialize($node); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$result = $serialized; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
if ($result instanceof \DOMAttr) { |
276
|
|
|
$result = $result->value; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
if ($result instanceof \DOMNode) { |
280
|
|
|
$result = preg_replace('/ xmlns(\:xsi)?="[^"]+"/', '', $result->C14N()); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
if (is_array($result) && is_numeric(key($result))) { |
284
|
|
|
return implode(', ', array_map([$this, 'serialize'], $result)); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return is_scalar($result) ? (string) $result : json_encode($result); |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.