ExportInspectCommand   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 28
lcom 2
cbo 6
dl 0
loc 277
ccs 0
cts 113
cp 0
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B configure() 0 64 1
B execute() 0 43 8
B inspect() 0 51 7
A evaluateCount() 0 13 1
A evaluateResult() 0 21 2
B serialize() 0 25 8
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'));
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument('type') targeting Symfony\Component\Consol...nterface::getArgument() can also be of type array<integer,string> or null; however, TreeHouse\IoBundle\Export\FeedExporter::getType() does only seem to accept string, maybe add an additional type check?

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.

Loading history...
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>
0 ignored issues
show
Documentation introduced by
The doc-type array<array, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
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);
0 ignored issues
show
Unused Code introduced by
The call to XMLReader::__construct() has too many arguments starting with $options.

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.

Loading history...
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