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

FormatterManager::validateData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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