Completed
Pull Request — master (#25)
by Greg
03:19
created

FormatterManager::write()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 3
eloc 8
nc 2
nop 4
crap 12
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
79 28
        // At the moment, we only support automatic options for --format
80
        // and --fields, so exit if the command returns no data.
81 28
        if (!isset($dataType)) {
82 1
            return [];
83
        }
84
85 27
        $validFormats = $this->validFormats($dataType);
86 27
        if (empty($validFormats)) {
87 9
            return [];
88 9
        }
89 27
90
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS, [], false);
91
        $defaultFormat = $availableFields ? 'table' : 'yaml';
92 28
93
        if (count($validFormats) > 1) {
94 28
            // Make an input option for --format
95
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
96
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_OPTIONAL, $description, $defaultFormat);
97
        }
98
99
        if ($availableFields) {
100
            $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], implode(',', $availableFields));
0 ignored issues
show
Documentation introduced by
implode(',', $availableFields) is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
101
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
102
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields);
103
        }
104
105
        return $automaticOptions;
106
    }
107 24
108
    /**
109 24
     * Given a list of available fields, return a list of field descriptions.
110 14
     * @return string[]
111
     */
112 14
    protected function availableFieldsList($availableFields)
113
    {
114
        return array_map(
115
            function ($key) use ($availableFields) {
116
                return $availableFields[$key] . " ($key)";
117
            },
118
            array_keys($availableFields)
119
        );
120
    }
121
122 27
    /**
123
     * Return the identifiers for all valid data types that have been registered.
124
     *
125
     * @param mixed $dataType \ReflectionObject or other description of the produced data type
126 27
     * @return array
127 16
     */
128
    public function validFormats($dataType)
129
    {
130
        $validFormats = [];
131
        $atLeastOneValidFormat = false;
132 15
        foreach ($this->formatters as $formatId => $formatterName) {
133 4
            $formatter = $this->getFormatter($formatId);
134
            if (!empty($formatId) && $this->isValidFormatForSpecifiedDataType($formatter, $dataType)) {
135
                $validFormats[] = $formatId;
136 11
                $atLeastOneValidFormat = true;
137
            } elseif (!empty($formatId) && ($formatter instanceof ValidationInterface)) {
138
                // A formatter that supports NO valid data types (e.g. the
139
                // string formatter) can be used with any data type that
140
                // is usable with at least one other data formatter.
141
                $supportedTypes = $formatter->validDataTypes();
142
                if (empty($supportedTypes)) {
143
                    $validFormats[] = $formatId;
144
                }
145
            }
146
        }
147 27
        if (!$atLeastOneValidFormat) {
148
            return [];
149 27
        }
150 7
        sort($validFormats);
151
        return $validFormats;
152 20
    }
153
154
    public function isValidFormat(FormatterInterface $formatter, $dataType)
155
    {
156
        // We should instead have a method of ValidationInterface that
157
        // we can pass our inspected dataType to so that we do not need
158
        // to have a special 'universal format' convention.
159
        // @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...
160
        return
161
            $this->isValidFormatForSpecifiedDataType($formatter, $dataType) ||
162
            $this->isUniversalFormat($formatter);
163
    }
164
165
    public function isUniversalFormat(FormatterInterface $formatter)
166
    {
167
        if (!$formatter instanceof ValidationInterface) {
168 27
            return false;
169
        }
170 27
        $supportedTypes = $formatter->validDataTypes();
171 5
        return empty($supportedTypes);
172
    }
173 26
174
    public function isValidFormatForSpecifiedDataType(FormatterInterface $formatter, $dataType)
175
    {
176
        if (is_array($dataType)) {
177
            $dataType = new \ReflectionClass('\ArrayObject');
178
        }
179
        if (!$dataType instanceof \ReflectionClass) {
180
            $dataType = new \ReflectionClass($dataType);
181
        }
182
        if ($this->canSimplifyToArray($dataType)) {
183
            if ($this->isValidFormat($formatter, [])) {
184
                return true;
185
            }
186
        }
187
        // If the formatter does not implement ValidationInterface, then
188
        // it is presumed that the formatter only accepts arrays.
189
        if (!$formatter instanceof ValidationInterface) {
190
            return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
191
        }
192
        $supportedTypes = $formatter->validDataTypes();
193
        foreach ($supportedTypes as $supportedType) {
194
            if (($dataType->getName() == $supportedType->getName()) || $dataType->isSubclassOf($supportedType->getName())) {
195
                return true;
196
            }
197
        }
198
        return false;
199
    }
200
201
    /**
202
     * Format and write output
203
     *
204
     * @param OutputInterface $output Output stream to write to
205
     * @param string $format Data format to output in
206
     * @param mixed $structuredOutput Data to output
207
     * @param FormatterOptions $options Formatting options
208
     */
209
    public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
210
    {
211
        $formatter = $this->getFormatter((string)$format);
212
        if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
213
            $validFormats = $this->validFormats($structuredOutput);
214
            throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
215
        }
216
        // Give the formatter a chance to override the options
217
        $options = $this->overrideOptions($formatter, $structuredOutput, $options);
218
        $structuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
219
        $formatter->write($output, $structuredOutput, $options);
220
    }
221
222
    protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
223
    {
224
        // Give the formatter a chance to do something with the
225
        // raw data before it is restructured.
226
        $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
227
        if ($overrideRestructure) {
228
            return $overrideRestructure;
229
        }
230
231
        // Restructure the output data (e.g. select fields to display, etc.).
232
        $restructuredOutput = $this->restructureData($structuredOutput, $options);
233
234
        // Make sure that the provided data is in the correct format for the selected formatter.
235
        $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
236
237
        // Give the original data a chance to re-render the structured
238
        // output after it has been restructured and validated.
239
        $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
240
241
        return $restructuredOutput;
242
    }
243
244
    /**
245
     * Fetch the requested formatter.
246
     *
247
     * @param string $format Identifier for requested formatter
248
     * @return FormatterInterface
249
     */
250
    public function getFormatter($format)
251
    {
252
        if (!$this->hasFormatter($format)) {
253
            throw new UnknownFormatException($format);
254
        }
255
        $formatter = new $this->formatters[$format];
256
        return $formatter;
257
    }
258
259
    /**
260
     * Test to see if the stipulated format exists
261
     */
262
    public function hasFormatter($format)
263
    {
264
        return array_key_exists($format, $this->formatters);
265
    }
266
267
    /**
268
     * Render the data as necessary (e.g. to select or reorder fields).
269
     *
270
     * @param FormatterInterface $formatter
271
     * @param mixed $originalData
272
     * @param mixed $restructuredData
273
     * @param FormatterOptions $options Formatting options
274
     * @return mixed
275
     */
276
    public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
277
    {
278
        if ($formatter instanceof RenderDataInterface) {
279
            return $formatter->renderData($originalData, $restructuredData, $options);
280
        }
281
        return $restructuredData;
282
    }
283
284
    /**
285
     * Determine if the provided data is compatible with the formatter being used.
286
     *
287
     * @param FormatterInterface $formatter Formatter being used
288
     * @param mixed $structuredOutput Data to validate
289
     * @return mixed
290
     */
291
    public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
292
    {
293
        // If the formatter implements ValidationInterface, then let it
294
        // test the data and throw or return an error
295
        if ($formatter instanceof ValidationInterface) {
296
            return $formatter->validate($structuredOutput);
297
        }
298
        // If the formatter does not implement ValidationInterface, then
299
        // it will never be passed an ArrayObject; we will always give
300
        // it a simple array.
301
        $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
302
        // If we could not simplify to an array, then throw an exception.
303
        // We will never give a formatter anything other than an array
304
        // unless it validates that it can accept the data type.
305
        if (!is_array($structuredOutput)) {
306
            throw new IncompatibleDataException(
307
                $formatter,
308
                $structuredOutput,
309
                []
310
            );
311
        }
312
        return $structuredOutput;
313
    }
314
315
    protected function simplifyToArray($structuredOutput, FormatterOptions $options)
316
    {
317
        // We can do nothing unless the provided data is an object.
318
        if (!is_object($structuredOutput)) {
319
            return $structuredOutput;
320
        }
321
        // Check to see if any of the simplifiers can convert the given data
322
        // set to an array.
323
        $outputDataType = new \ReflectionClass($structuredOutput);
324
        foreach ($this->arraySimplifiers as $simplifier) {
325
            if ($simplifier->canSimplify($outputDataType)) {
326
                $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
327
            }
328
        }
329
        // Convert \ArrayObjects to a simple array.
330
        if ($structuredOutput instanceof \ArrayObject) {
331
            return $structuredOutput->getArrayCopy();
332
        }
333
        return $structuredOutput;
334
    }
335
336
    protected function canSimplifyToArray($structuredOutput)
337
    {
338
        foreach ($this->arraySimplifiers as $simplifier) {
339
            if ($simplifier->canSimplify($structuredOutput)) {
340
                return true;
341
            }
342
        }
343
        return false;
344
    }
345
346
    /**
347
     * Restructure the data as necessary (e.g. to select or reorder fields).
348
     *
349
     * @param mixed $structuredOutput
350
     * @param FormatterOptions $options
351
     * @return mixed
352
     */
353
    public function restructureData($structuredOutput, FormatterOptions $options)
354
    {
355
        if ($structuredOutput instanceof RestructureInterface) {
356
            return $structuredOutput->restructure($options);
357
        }
358
        return $structuredOutput;
359
    }
360
361
    /**
362
     * Allow the formatter access to the raw structured data prior
363
     * to restructuring.  For example, the 'list' formatter may wish
364
     * to display the row keys when provided table output.  If this
365
     * function returns a result that does not evaluate to 'false',
366
     * then that result will be used as-is, and restructuring and
367
     * validation will not occur.
368
     *
369
     * @param mixed $structuredOutput
370
     * @param FormatterOptions $options
371
     * @return mixed
372
     */
373
    public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
374
    {
375
        if ($formatter instanceof OverrideRestructureInterface) {
376
            return $formatter->overrideRestructure($structuredOutput, $options);
377
        }
378
    }
379
380
    /**
381
     * Allow the formatter to mess with the configuration options before any
382
     * transformations et. al. get underway.
383
     * @param FormatterInterface $formatter
384
     * @param mixed $structuredOutput
385
     * @param FormatterOptions $options
386
     * @return FormatterOptions
387
     */
388
    public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
389
    {
390
        if ($formatter instanceof OverrideOptionsInterface) {
391
            return $formatter->overrideOptions($structuredOutput, $options);
392
        }
393
        return $options;
394
    }
395
}
396