Completed
Pull Request — master (#77)
by Greg
01:42
created

FormatterManager::write()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 0
cts 0
cp 0
rs 8.4586
c 0
b 0
f 0
cc 7
nc 9
nop 4
crap 56
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\FormatterAwareInterface;
8
use Consolidation\OutputFormatters\Formatters\FormatterInterface;
9
use Consolidation\OutputFormatters\Formatters\MetadataFormatterInterface;
10
use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
11
use Consolidation\OutputFormatters\Options\FormatterOptions;
12
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
13
use Consolidation\OutputFormatters\StructuredData\MetadataInterface;
14
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
15
use Consolidation\OutputFormatters\Transformations\DomToArraySimplifier;
16 28
use Consolidation\OutputFormatters\Transformations\OverrideRestructureInterface;
17
use Consolidation\OutputFormatters\Transformations\SimplifyToArrayInterface;
18 28
use Consolidation\OutputFormatters\Validate\ValidationInterface;
19 28
use Symfony\Component\Console\Input\InputOption;
20 28
use Symfony\Component\Console\Output\OutputInterface;
21 28
use Consolidation\OutputFormatters\StructuredData\OriginalDataInterface;
22 28
use Consolidation\OutputFormatters\StructuredData\ListDataFromKeys;
23 28
use Consolidation\OutputFormatters\StructuredData\ConversionInterface;
24 28
use Consolidation\OutputFormatters\Formatters\ImplicitEolInterface;
25 28
26 28
/**
27 28
 * Manage a collection of formatters; return one on request.
28
 */
29
class FormatterManager
30
{
31 28
    /** var FormatterInterface[] */
32 28
    protected $formatters = [];
33
    /** var SimplifyToArrayInterface[] */
34
    protected $arraySimplifiers = [];
35
36
    public function __construct()
37
    {
38
    }
39
40
    public function addDefaultFormatters()
41
    {
42
        $defaultFormatters = [
43 28
            'null' => '\Consolidation\OutputFormatters\Formatters\NoOutputFormatter',
44
            'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
45 28
            'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
46 27
            'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
47 24
            'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
48 24
            'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
49
            'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
50 27
            'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
51
            'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
52
            'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
53
            'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
54 27
            'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
55 27
            'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
56 4
        ];
57
        if (class_exists('Symfony\Component\VarDumper\Dumper\CliDumper')) {
58
             $defaultFormatters['var_dump'] = '\Consolidation\OutputFormatters\Formatters\VarDumpFormatter';
59
        }
60 27
        foreach ($defaultFormatters as $id => $formatterClassname) {
61
            $formatter = new $formatterClassname;
62
            $this->addFormatter($id, $formatter);
63 27
        }
64
        $this->addFormatter('', $this->formatters['string']);
65
    }
66
67 24
    public function addDefaultSimplifiers()
68
    {
69 24
        // Add our default array simplifier (DOMDocument to array)
70
        $this->addSimplifier(new DomToArraySimplifier());
71
    }
72
73
    /**
74
     * Add a formatter
75
     *
76
     * @param string $key the identifier of the formatter to add
77
     * @param string $formatter the class name of the formatter to add
78
     * @return FormatterManager
79 28
     */
80
    public function addFormatter($key, FormatterInterface $formatter)
81 28
    {
82 1
        $this->formatters[$key] = $formatter;
83
        return $this;
84
    }
85 27
86 27
    /**
87 9
     * Add a simplifier
88 9
     *
89 27
     * @param SimplifyToArrayInterface $simplifier the array simplifier to add
90
     * @return FormatterManager
91
     */
92 28
    public function addSimplifier(SimplifyToArrayInterface $simplifier)
93
    {
94 28
        $this->arraySimplifiers[] = $simplifier;
95
        return $this;
96
    }
97
98
    /**
99
     * Return a set of InputOption based on the annotations of a command.
100
     * @param FormatterOptions $options
101
     * @return InputOption[]
102
     */
103
    public function automaticOptions(FormatterOptions $options, $dataType)
104
    {
105
        $automaticOptions = [];
106
107 24
        // At the moment, we only support automatic options for --format
108
        // and --fields, so exit if the command returns no data.
109 24
        if (!isset($dataType)) {
110 14
            return [];
111
        }
112 14
113
        $validFormats = $this->validFormats($dataType);
114
        if (empty($validFormats)) {
115
            return [];
116
        }
117
118
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS);
119
        $hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD);
120
        $defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml');
121
122 27
        if (count($validFormats) > 1) {
123
            // Make an input option for --format
124
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
125
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_REQUIRED, $description, $defaultFormat);
126 27
        }
127 16
128
        $dataTypeClass = ($dataType instanceof \ReflectionClass) ? $dataType : new \ReflectionClass($dataType);
129
130
        if ($availableFields) {
131
            $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
132 15
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
133 4
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, $description, $defaultFields);
134
        } elseif ($dataTypeClass->implementsInterface('Consolidation\OutputFormatters\StructuredData\RestructureInterface')) {
135
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, 'Limit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".', []);
136 11
        }
137
138
        if (isset($automaticOptions[FormatterOptions::FIELDS])) {
139
            $automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_REQUIRED, "Select just one field, and force format to 'string'.", '');
140
        }
141
142
        return $automaticOptions;
143
    }
144
145
    /**
146
     * Given a list of available fields, return a list of field descriptions.
147 27
     * @return string[]
148
     */
149 27
    protected function availableFieldsList($availableFields)
150 7
    {
151
        return array_map(
152 20
            function ($key) use ($availableFields) {
153
                return $availableFields[$key] . " ($key)";
154
            },
155
            array_keys($availableFields)
156
        );
157
    }
158
159
    /**
160
     * Return the identifiers for all valid data types that have been registered.
161
     *
162
     * @param mixed $dataType \ReflectionObject or other description of the produced data type
163
     * @return array
164
     */
165
    public function validFormats($dataType)
166
    {
167
        $validFormats = [];
168 27
        foreach ($this->formatters as $formatId => $formatterName) {
169
            $formatter = $this->getFormatter($formatId);
170 27
            if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
171 5
                $validFormats[] = $formatId;
172
            }
173 26
        }
174
        sort($validFormats);
175
        return $validFormats;
176
    }
177
178
    public function isValidFormat(FormatterInterface $formatter, $dataType)
179
    {
180
        if (is_array($dataType)) {
181
            $dataType = new \ReflectionClass('\ArrayObject');
182
        }
183
        if (!is_object($dataType) && !class_exists($dataType)) {
184
            return false;
185
        }
186
        if (!$dataType instanceof \ReflectionClass) {
187
            $dataType = new \ReflectionClass($dataType);
188
        }
189
        return $this->isValidDataType($formatter, $dataType);
190
    }
191
192
    public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType)
193
    {
194
        if ($this->canSimplifyToArray($dataType)) {
195
            if ($this->isValidFormat($formatter, [])) {
196
                return true;
197
            }
198
        }
199
        // If the formatter does not implement ValidationInterface, then
200
        // it is presumed that the formatter only accepts arrays.
201
        if (!$formatter instanceof ValidationInterface) {
202
            return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
203
        }
204
        return $formatter->isValidDataType($dataType);
205
    }
206
207
    /**
208
     * Format and write output
209
     *
210
     * @param OutputInterface $output Output stream to write to
211
     * @param string $format Data format to output in
212
     * @param mixed $structuredOutput Data to output
213
     * @param FormatterOptions $options Formatting options
214
     */
215
    public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
216
    {
217
        // Convert the data to another format (e.g. converting from RowsOfFields to
218
        // UnstructuredListData when the fields indicate an unstructured transformation
219
        // is requested).
220
        $structuredOutput = $this->convertData($structuredOutput, $options);
221
222
        // TODO: If the $format is the default format (not selected by the user), and
223
        // if `convertData` switched us to unstructured data, then select a new default
224
        // format (e.g. yaml) if the selected format cannot render the converted data.
225
        $formatter = $this->getFormatter((string)$format);
226
227
        // If the data format is not applicable for the selected formatter, throw an error.
228
        if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
229
            $validFormats = $this->validFormats($structuredOutput);
230
            throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
231
        }
232
        if ($structuredOutput instanceof FormatterAwareInterface) {
233
            $structuredOutput->setFormatter($formatter);
234
        }
235
        // Give the formatter a chance to override the options
236
        $options = $this->overrideOptions($formatter, $structuredOutput, $options);
237
        $restructuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
238
        if ($formatter instanceof MetadataFormatterInterface) {
239
            $formatter->writeMetadata($output, $structuredOutput, $options);
240
        }
241
        $formatter->write($output, $restructuredOutput, $options);
242
        // In interactive mode, write an extra newline after the output,
243
        // but only if the formatter has not already implicitly done so.
244
        if ($options->shouldAppendNewline($output) && !$formatter instanceof ImplicitEolInterface) {
245
            $output->writeln();
0 ignored issues
show
Bug introduced by
The call to writeln() misses a required argument $messages.

This check looks for function calls that miss required arguments.

Loading history...
246
        }
247
    }
248
249
    protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
250
    {
251
        // Give the formatter a chance to do something with the
252
        // raw data before it is restructured.
253
        $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
254
        if ($overrideRestructure) {
255
            return $overrideRestructure;
256
        }
257
258
        // Restructure the output data (e.g. select fields to display, etc.).
259
        $restructuredOutput = $this->restructureData($structuredOutput, $options);
260
261
        // Make sure that the provided data is in the correct format for the selected formatter.
262
        $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
263
264
        // Give the original data a chance to re-render the structured
265
        // output after it has been restructured and validated.
266
        $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
267
268
        return $restructuredOutput;
269
    }
270
271
    /**
272
     * Fetch the requested formatter.
273
     *
274
     * @param string $format Identifier for requested formatter
275
     * @return FormatterInterface
276
     */
277
    public function getFormatter($format)
278
    {
279
        // The client must inject at least one formatter before asking for
280
        // any formatters; if not, we will provide all of the usual defaults
281
        // as a convenience.
282
        if (empty($this->formatters)) {
283
            $this->addDefaultFormatters();
284
            $this->addDefaultSimplifiers();
285
        }
286
        if (!$this->hasFormatter($format)) {
287
            throw new UnknownFormatException($format);
288
        }
289
        $formatter = $this->formatters[$format];
290
        return $formatter;
291
    }
292
293
    /**
294
     * Test to see if the stipulated format exists
295
     */
296
    public function hasFormatter($format)
297
    {
298
        return array_key_exists($format, $this->formatters);
299
    }
300
301
    /**
302
     * Render the data as necessary (e.g. to select or reorder fields).
303
     *
304
     * @param FormatterInterface $formatter
305
     * @param mixed $originalData
306
     * @param mixed $restructuredData
307
     * @param FormatterOptions $options Formatting options
308
     * @return mixed
309
     */
310
    public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
311
    {
312
        if ($formatter instanceof RenderDataInterface) {
313
            return $formatter->renderData($originalData, $restructuredData, $options);
314
        }
315
        return $restructuredData;
316
    }
317
318
    /**
319
     * Determine if the provided data is compatible with the formatter being used.
320
     *
321
     * @param FormatterInterface $formatter Formatter being used
322
     * @param mixed $structuredOutput Data to validate
323
     * @return mixed
324
     */
325
    public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
326
    {
327
        // If the formatter implements ValidationInterface, then let it
328
        // test the data and throw or return an error
329
        if ($formatter instanceof ValidationInterface) {
330
            return $formatter->validate($structuredOutput);
331
        }
332
        // If the formatter does not implement ValidationInterface, then
333
        // it will never be passed an ArrayObject; we will always give
334
        // it a simple array.
335
        $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
336
        // If we could not simplify to an array, then throw an exception.
337
        // We will never give a formatter anything other than an array
338
        // unless it validates that it can accept the data type.
339
        if (!is_array($structuredOutput)) {
340
            throw new IncompatibleDataException(
341
                $formatter,
342
                $structuredOutput,
343
                []
344
            );
345
        }
346
        return $structuredOutput;
347
    }
348
349
    protected function simplifyToArray($structuredOutput, FormatterOptions $options)
350
    {
351
        // We can do nothing unless the provided data is an object.
352
        if (!is_object($structuredOutput)) {
353
            return $structuredOutput;
354
        }
355
        // Check to see if any of the simplifiers can convert the given data
356
        // set to an array.
357
        $outputDataType = new \ReflectionClass($structuredOutput);
358
        foreach ($this->arraySimplifiers as $simplifier) {
359
            if ($simplifier->canSimplify($outputDataType)) {
360
                $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
361
            }
362
        }
363
        // Convert data structure back into its original form, if necessary.
364
        if ($structuredOutput instanceof OriginalDataInterface) {
365
            return $structuredOutput->getOriginalData();
366
        }
367
        // Convert \ArrayObjects to a simple array.
368
        if ($structuredOutput instanceof \ArrayObject) {
369
            return $structuredOutput->getArrayCopy();
370
        }
371
        return $structuredOutput;
372
    }
373
374
    protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
375
    {
376
        foreach ($this->arraySimplifiers as $simplifier) {
377
            if ($simplifier->canSimplify($structuredOutput)) {
378
                return true;
379
            }
380
        }
381
        return false;
382
    }
383
384
    /**
385
     * Convert from one format to another if necessary prior to restructuring.
386
     */
387
    public function convertData($structuredOutput, FormatterOptions $options)
388
    {
389
        if ($structuredOutput instanceof ConversionInterface) {
390
            return $structuredOutput->convert($options);
391
        }
392
        return $structuredOutput;
393
    }
394
395
    /**
396
     * Restructure the data as necessary (e.g. to select or reorder fields).
397
     *
398
     * @param mixed $structuredOutput
399
     * @param FormatterOptions $options
400
     * @return mixed
401
     */
402
    public function restructureData($structuredOutput, FormatterOptions $options)
403
    {
404
        if ($structuredOutput instanceof RestructureInterface) {
405
            return $structuredOutput->restructure($options);
406
        }
407
        return $structuredOutput;
408
    }
409
410
    /**
411
     * Allow the formatter access to the raw structured data prior
412
     * to restructuring.  For example, the 'list' formatter may wish
413
     * to display the row keys when provided table output.  If this
414
     * function returns a result that does not evaluate to 'false',
415
     * then that result will be used as-is, and restructuring and
416
     * validation will not occur.
417
     *
418
     * @param mixed $structuredOutput
419
     * @param FormatterOptions $options
420
     * @return mixed
421
     */
422
    public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
423
    {
424
        if ($formatter instanceof OverrideRestructureInterface) {
425
            return $formatter->overrideRestructure($structuredOutput, $options);
426
        }
427
    }
428
429
    /**
430
     * Allow the formatter to mess with the configuration options before any
431
     * transformations et. al. get underway.
432
     * @param FormatterInterface $formatter
433
     * @param mixed $structuredOutput
434
     * @param FormatterOptions $options
435
     * @return FormatterOptions
436
     */
437
    public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
438
    {
439
        if ($formatter instanceof OverrideOptionsInterface) {
440
            return $formatter->overrideOptions($structuredOutput, $options);
441
        }
442
        return $options;
443
    }
444
}
445