Completed
Pull Request — master (#25)
by Greg
02:58
created

FormatterManager::automaticOptions()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.439
c 0
b 0
f 0
ccs 11
cts 11
cp 1
cc 5
eloc 17
nc 6
nop 2
crap 5
1
<?php
2
namespace Consolidation\OutputFormatters;
3
4
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
5
use Consolidation\OutputFormatters\Exception\InvalidFormatException;
6
use Consolidation\OutputFormatters\Exception\UnknownFormatException;
7
use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
8
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
9
use Consolidation\OutputFormatters\Transformations\DomToArraySimplifier;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\OutputInterface;
12
13
/**
14
 * Manage a collection of formatters; return one on request.
15
 */
16 28
class FormatterManager
17
{
18 28
    protected $formatters = [];
19 28
    protected $arraySimplifiers = [];
20 28
21 28
    public function __construct()
22 28
    {
23 28
        $this->formatters = [
24 28
            'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
25 28
            'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
26 28
            'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
27 28
            'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
28
            'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
29
            'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
30
            'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
31 28
            'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
32 28
            'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
33
            'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
34
            'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
35
            'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
36
        ];
37
38
        // Make the empty format an alias for the 'string' formatter.
39
        $this->addFormatter('', $this->formatters['string']);
40
41
        // Add our default array simplifier (DOMDocument to array)
42
        $this->addSimplifier(new DomToArraySimplifier());
43 28
    }
44
45 28
    /**
46 27
     * Add a formatter
47 24
     *
48 24
     * @param string $key the identifier of the formatter to add
49
     * @param string $formatterClassname the class name of the formatter to add
50 27
     * @return FormatterManager
51
     */
52
    public function addFormatter($key, $formatterClassname)
53
    {
54 27
        $this->formatters[$key] = $formatterClassname;
55 27
        return $this;
56 4
    }
57
58
    /**
59
     * Add a simplifier
60 27
     *
61
     * @param SimplifyToArrayInterface $simplifier the array simplifier to add
62
     * @return FormatterManager
63 27
     */
64
    public function addSimplifier(SimplifyToArrayInterface $simplifier)
65
    {
66
        $this->arraySimplifiers[] = $simplifier;
67 24
        return $this;
68
    }
69 24
70
    /**
71
     * Return a set of InputOption based on the annotations of a command.
72
     * @param FormatterOptions $options
73
     * @return InputOption[]
74
     */
75
    public function automaticOptions(FormatterOptions $options, $dataType)
76
    {
77
        $automaticOptions = [];
78
        $defaultFormat = 'yaml';
79 28
80
        // At the moment, we only support automatic options for --format
81 28
        // and --fields, so exit if the command returns no data.
82 1
        if (!isset($dataType)) {
83
            return [];
84
        }
85 27
86 27
        $validFormats = $this->validFormats($dataType);
87 9
        if (empty($validFormats)) {
88 9
            return [];
89 27
        }
90
91
        if (count($validFormats) > 1) {
92 28
            // Make an input option for --format
93
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
94 28
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_OPTIONAL, $description, $defaultFormat);
95
        }
96
97
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS, [], false);
98
        if ($availableFields) {
99
            // We have fields; that implies 'table', unless someone says something different
100
            $defaultFormat = 'table';
0 ignored issues
show
Unused Code introduced by
$defaultFormat is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
101
102
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
103
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields);
0 ignored issues
show
Bug introduced by
The variable $defaultFields does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
104
        }
105
106
        return $automaticOptions;
107 24
    }
108
109 24
    /**
110 14
     * Given a list of available fields, return a list of field descriptions.
111
     * @return string[]
112 14
     */
113
    protected function availableFieldsList($availableFields)
114
    {
115
        return array_map(
116
            function ($key) use ($availableFields) {
117
                return $availableFields[$key] . " ($key)";
118
            },
119
            array_keys($availableFields)
120
        );
121
    }
122 27
123
    /**
124
     * Return the identifiers for all valid data types that have been registered.
125
     *
126 27
     * @param mixed $dataType \ReflectionObject or other description of the produced data type
127 16
     * @return array
128
     */
129
    public function validFormats($dataType)
130
    {
131
        $validFormats = [];
132 15
        $atLeastOneValidFormat = false;
133 4
        foreach ($this->formatters as $formatId => $formatterName) {
134
            $formatter = $this->getFormatter($formatId);
135
            if (!empty($formatId) && $this->isValidFormatForSpecifiedDataType($formatter, $dataType)) {
136 11
                $validFormats[] = $formatId;
137
                $atLeastOneValidFormat = true;
138
            } elseif (!empty($formatId) && ($formatter instanceof ValidationInterface)) {
139
                // A formatter that supports NO valid data types (e.g. the
140
                // string formatter) can be used with any data type that
141
                // is usable with at least one other data formatter.
142
                $supportedTypes = $formatter->validDataTypes();
143
                if (empty($supportedTypes)) {
144
                    $validFormats[] = $formatId;
145
                }
146
            }
147 27
        }
148
        if (!$atLeastOneValidFormat) {
149 27
            return [];
150 7
        }
151
        sort($validFormats);
152 20
        return $validFormats;
153
    }
154
155
    public function isValidFormat(FormatterInterface $formatter, $dataType)
156
    {
157
        // We should instead have a method of ValidationInterface that
158
        // we can pass our inspected dataType to so that we do not need
159
        // to have a special 'universal format' convention.
160
        // @see ValidationInterface::validDataTypes()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
161
        return
162
            $this->isValidFormatForSpecifiedDataType($formatter, $dataType) ||
163
            $this->isUniversalFormat($formatter);
164
    }
165
166
    public function isUniversalFormat(FormatterInterface $formatter)
167
    {
168 27
        if (!$formatter instanceof ValidationInterface) {
169
            return false;
170 27
        }
171 5
        $supportedTypes = $formatter->validDataTypes();
172
        return empty($supportedTypes);
173 26
    }
174
175
    public function isValidFormatForSpecifiedDataType(FormatterInterface $formatter, $dataType)
176
    {
177
        if (is_array($dataType)) {
178
            $dataType = new \ReflectionClass('\ArrayObject');
179
        }
180
        if (!$dataType instanceof \ReflectionClass) {
181
            $dataType = new \ReflectionClass($dataType);
182
        }
183
        if ($this->canSimplifyToArray($dataType)) {
184
            if ($this->isValidFormat($formatter, [])) {
185
                return true;
186
            }
187
        }
188
        // If the formatter does not implement ValidationInterface, then
189
        // it is presumed that the formatter only accepts arrays.
190
        if (!$formatter instanceof ValidationInterface) {
191
            return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
192
        }
193
        $supportedTypes = $formatter->validDataTypes();
194
        foreach ($supportedTypes as $supportedType) {
195
            if (($dataType->getName() == $supportedType->getName()) || $dataType->isSubclassOf($supportedType->getName())) {
196
                return true;
197
            }
198
        }
199
        return false;
200
    }
201
202
    /**
203
     * Format and write output
204
     *
205
     * @param OutputInterface $output Output stream to write to
206
     * @param string $format Data format to output in
207
     * @param mixed $structuredOutput Data to output
208
     * @param FormatterOptions $options Formatting options
209
     */
210
    public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
211
    {
212
        $formatter = $this->getFormatter((string)$format);
213
        if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
214
            $validFormats = $this->validFormats($structuredOutput);
215
            throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
216
        }
217
        // Give the formatter a chance to override the options
218
        $options = $this->overrideOptions($formatter, $structuredOutput, $options);
219
        $structuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
220
        $formatter->write($output, $structuredOutput, $options);
221
    }
222
223
    protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
224
    {
225
        // Give the formatter a chance to do something with the
226
        // raw data before it is restructured.
227
        $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
228
        if ($overrideRestructure) {
229
            return $overrideRestructure;
230
        }
231
232
        // Restructure the output data (e.g. select fields to display, etc.).
233
        $restructuredOutput = $this->restructureData($structuredOutput, $options);
234
235
        // Make sure that the provided data is in the correct format for the selected formatter.
236
        $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
237
238
        // Give the original data a chance to re-render the structured
239
        // output after it has been restructured and validated.
240
        $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
241
242
        return $restructuredOutput;
243
    }
244
245
    /**
246
     * Fetch the requested formatter.
247
     *
248
     * @param string $format Identifier for requested formatter
249
     * @return FormatterInterface
250
     */
251
    public function getFormatter($format)
252
    {
253
        if (!$this->hasFormatter($format)) {
254
            throw new UnknownFormatException($format);
255
        }
256
        $formatter = new $this->formatters[$format];
257
        return $formatter;
258
    }
259
260
    /**
261
     * Test to see if the stipulated format exists
262
     */
263
    public function hasFormatter($format)
264
    {
265
        return array_key_exists($format, $this->formatters);
266
    }
267
268
    /**
269
     * Render the data as necessary (e.g. to select or reorder fields).
270
     *
271
     * @param FormatterInterface $formatter
272
     * @param mixed $originalData
273
     * @param mixed $restructuredData
274
     * @param FormatterOptions $options Formatting options
275
     * @return mixed
276
     */
277
    public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
278
    {
279
        if ($formatter instanceof RenderDataInterface) {
280
            return $formatter->renderData($originalData, $restructuredData, $options);
281
        }
282
        return $restructuredData;
283
    }
284
285
    /**
286
     * Determine if the provided data is compatible with the formatter being used.
287
     *
288
     * @param FormatterInterface $formatter Formatter being used
289
     * @param mixed $structuredOutput Data to validate
290
     * @return mixed
291
     */
292
    public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
293
    {
294
        // If the formatter implements ValidationInterface, then let it
295
        // test the data and throw or return an error
296
        if ($formatter instanceof ValidationInterface) {
297
            return $formatter->validate($structuredOutput);
298
        }
299
        // If the formatter does not implement ValidationInterface, then
300
        // it will never be passed an ArrayObject; we will always give
301
        // it a simple array.
302
        $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
303
        // If we could not simplify to an array, then throw an exception.
304
        // We will never give a formatter anything other than an array
305
        // unless it validates that it can accept the data type.
306
        if (!is_array($structuredOutput)) {
307
            throw new IncompatibleDataException(
308
                $formatter,
309
                $structuredOutput,
310
                []
311
            );
312
        }
313
        return $structuredOutput;
314
    }
315
316
    protected function simplifyToArray($structuredOutput, FormatterOptions $options)
317
    {
318
        // We can do nothing unless the provided data is an object.
319
        if (!is_object($structuredOutput)) {
320
            return $structuredOutput;
321
        }
322
        // Check to see if any of the simplifiers can convert the given data
323
        // set to an array.
324
        $outputDataType = new \ReflectionClass($structuredOutput);
325
        foreach ($this->arraySimplifiers as $simplifier) {
326
            if ($simplifier->canSimplify($outputDataType)) {
327
                $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
328
            }
329
        }
330
        // Convert \ArrayObjects to a simple array.
331
        if ($structuredOutput instanceof \ArrayObject) {
332
            return $structuredOutput->getArrayCopy();
333
        }
334
        return $structuredOutput;
335
    }
336
337
    protected function canSimplifyToArray($structuredOutput)
338
    {
339
        foreach ($this->arraySimplifiers as $simplifier) {
340
            if ($simplifier->canSimplify($structuredOutput)) {
341
                return true;
342
            }
343
        }
344
        return false;
345
    }
346
347
    /**
348
     * Restructure the data as necessary (e.g. to select or reorder fields).
349
     *
350
     * @param mixed $structuredOutput
351
     * @param FormatterOptions $options
352
     * @return mixed
353
     */
354
    public function restructureData($structuredOutput, FormatterOptions $options)
355
    {
356
        if ($structuredOutput instanceof RestructureInterface) {
357
            return $structuredOutput->restructure($options);
358
        }
359
        return $structuredOutput;
360
    }
361
362
    /**
363
     * Allow the formatter access to the raw structured data prior
364
     * to restructuring.  For example, the 'list' formatter may wish
365
     * to display the row keys when provided table output.  If this
366
     * function returns a result that does not evaluate to 'false',
367
     * then that result will be used as-is, and restructuring and
368
     * validation will not occur.
369
     *
370
     * @param mixed $structuredOutput
371
     * @param FormatterOptions $options
372
     * @return mixed
373
     */
374
    public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
375
    {
376
        if ($formatter instanceof OverrideRestructureInterface) {
377
            return $formatter->overrideRestructure($structuredOutput, $options);
378
        }
379
    }
380
381
    /**
382
     * Allow the formatter to mess with the configuration options before any
383
     * transformations et. al. get underway.
384
     * @param FormatterInterface $formatter
385
     * @param mixed $structuredOutput
386
     * @param FormatterOptions $options
387
     * @return FormatterOptions
388
     */
389
    public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
390
    {
391
        if ($formatter instanceof OverrideOptionsInterface) {
392
            return $formatter->overrideOptions($structuredOutput, $options);
393
        }
394
        return $options;
395
    }
396
}
397