Completed
Push — master ( fa856c...54ad29 )
by Greg
04:32
created

FormatterManager::addDefaultFormatters()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
ccs 10
cts 10
cp 1
rs 8.8571
cc 3
eloc 20
nc 4
nop 0
crap 3
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
23 28
/**
24 28
 * Manage a collection of formatters; return one on request.
25 28
 */
26 28
class FormatterManager
27 28
{
28
    /** var FormatterInterface[] */
29
    protected $formatters = [];
30
    /** var SimplifyToArrayInterface[] */
31 28
    protected $arraySimplifiers = [];
32 28
33
    public function __construct()
34
    {
35
    }
36
37
    public function addDefaultFormatters()
38
    {
39
        $defaultFormatters = [
40
            'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
41
            'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
42
            'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
43 28
            'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
44
            'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
45 28
            'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
46 27
            'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
47 24
            'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
48 24
            'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
49
            'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
50 27
            'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
51
            'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
52
        ];
53
        if (class_exists('Symfony\Component\VarDumper\Dumper\CliDumper')) {
54 27
             $defaultFormatters['var_dump'] = '\Consolidation\OutputFormatters\Formatters\VarDumpFormatter';
55 27
        }
56 4
        foreach ($defaultFormatters as $id => $formatterClassname) {
57
            $formatter = new $formatterClassname;
58
            $this->addFormatter($id, $formatter);
59
        }
60 27
        $this->addFormatter('', $this->formatters['string']);
61
    }
62
63 27
    public function addDefaultSimplifiers()
64
    {
65
        // Add our default array simplifier (DOMDocument to array)
66
        $this->addSimplifier(new DomToArraySimplifier());
67 24
    }
68
69 24
    /**
70
     * Add a formatter
71
     *
72
     * @param string $key the identifier of the formatter to add
73
     * @param string $formatter the class name of the formatter to add
74
     * @return FormatterManager
75
     */
76
    public function addFormatter($key, FormatterInterface $formatter)
77
    {
78
        $this->formatters[$key] = $formatter;
79 28
        return $this;
80
    }
81 28
82 1
    /**
83
     * Add a simplifier
84
     *
85 27
     * @param SimplifyToArrayInterface $simplifier the array simplifier to add
86 27
     * @return FormatterManager
87 9
     */
88 9
    public function addSimplifier(SimplifyToArrayInterface $simplifier)
89 27
    {
90
        $this->arraySimplifiers[] = $simplifier;
91
        return $this;
92 28
    }
93
94 28
    /**
95
     * Return a set of InputOption based on the annotations of a command.
96
     * @param FormatterOptions $options
97
     * @return InputOption[]
98
     */
99
    public function automaticOptions(FormatterOptions $options, $dataType)
100
    {
101
        $automaticOptions = [];
102
103
        // At the moment, we only support automatic options for --format
104
        // and --fields, so exit if the command returns no data.
105
        if (!isset($dataType)) {
106
            return [];
107 24
        }
108
109 24
        $validFormats = $this->validFormats($dataType);
110 14
        if (empty($validFormats)) {
111
            return [];
112 14
        }
113
114
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS);
115
        $hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD);
116
        $defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml');
117
118
        if (count($validFormats) > 1) {
119
            // Make an input option for --format
120
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
121
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_REQUIRED, $description, $defaultFormat);
122 27
        }
123
124
        if ($availableFields) {
125
            $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
126 27
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
127 16
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, $description, $defaultFields);
128
            $automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_REQUIRED, "Select just one field, and force format to 'string'.", '');
129
        }
130
131
        return $automaticOptions;
132 15
    }
133 4
134
    /**
135
     * Given a list of available fields, return a list of field descriptions.
136 11
     * @return string[]
137
     */
138
    protected function availableFieldsList($availableFields)
139
    {
140
        return array_map(
141
            function ($key) use ($availableFields) {
142
                return $availableFields[$key] . " ($key)";
143
            },
144
            array_keys($availableFields)
145
        );
146
    }
147 27
148
    /**
149 27
     * Return the identifiers for all valid data types that have been registered.
150 7
     *
151
     * @param mixed $dataType \ReflectionObject or other description of the produced data type
152 20
     * @return array
153
     */
154
    public function validFormats($dataType)
155
    {
156
        $validFormats = [];
157
        foreach ($this->formatters as $formatId => $formatterName) {
158
            $formatter = $this->getFormatter($formatId);
159
            if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
160
                $validFormats[] = $formatId;
161
            }
162
        }
163
        sort($validFormats);
164
        return $validFormats;
165
    }
166
167
    public function isValidFormat(FormatterInterface $formatter, $dataType)
168 27
    {
169
        if (is_array($dataType)) {
170 27
            $dataType = new \ReflectionClass('\ArrayObject');
171 5
        }
172
        if (!is_object($dataType) && !class_exists($dataType)) {
173 26
            return false;
174
        }
175
        if (!$dataType instanceof \ReflectionClass) {
176
            $dataType = new \ReflectionClass($dataType);
177
        }
178
        return $this->isValidDataType($formatter, $dataType);
179
    }
180
181
    public function isValidDataType(FormatterInterface $formatter, \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
        return $formatter->isValidDataType($dataType);
194
    }
195
196
    /**
197
     * Format and write output
198
     *
199
     * @param OutputInterface $output Output stream to write to
200
     * @param string $format Data format to output in
201
     * @param mixed $structuredOutput Data to output
202
     * @param FormatterOptions $options Formatting options
203
     */
204
    public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
205
    {
206
        $formatter = $this->getFormatter((string)$format);
207
        if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
208
            $validFormats = $this->validFormats($structuredOutput);
209
            throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
210
        }
211
        if ($structuredOutput instanceof FormatterAwareInterface) {
212
            $structuredOutput->setFormatter($formatter);
213
        }
214
        // Give the formatter a chance to override the options
215
        $options = $this->overrideOptions($formatter, $structuredOutput, $options);
216
        $restructuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
217
        if ($formatter instanceof MetadataFormatterInterface) {
218
            $formatter->writeMetadata($output, $structuredOutput, $options);
219
        }
220
        $formatter->write($output, $restructuredOutput, $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
        // The client must inject at least one formatter before asking for
254
        // any formatters; if not, we will provide all of the usual defaults
255
        // as a convenience.
256
        if (empty($this->formatters)) {
257
            $this->addDefaultFormatters();
258
            $this->addDefaultSimplifiers();
259
        }
260
        if (!$this->hasFormatter($format)) {
261
            throw new UnknownFormatException($format);
262
        }
263
        $formatter = $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 data structure back into its original form, if necessary.
338
        if ($structuredOutput instanceof OriginalDataInterface) {
339
            return $structuredOutput->getOriginalData();
340
        }
341
        // Convert \ArrayObjects to a simple array.
342
        if ($structuredOutput instanceof \ArrayObject) {
343
            return $structuredOutput->getArrayCopy();
344
        }
345
        return $structuredOutput;
346
    }
347
348
    protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
349
    {
350
        foreach ($this->arraySimplifiers as $simplifier) {
351
            if ($simplifier->canSimplify($structuredOutput)) {
352
                return true;
353
            }
354
        }
355
        return false;
356
    }
357
358
    /**
359
     * Restructure the data as necessary (e.g. to select or reorder fields).
360
     *
361
     * @param mixed $structuredOutput
362
     * @param FormatterOptions $options
363
     * @return mixed
364
     */
365
    public function restructureData($structuredOutput, FormatterOptions $options)
366
    {
367
        if ($structuredOutput instanceof RestructureInterface) {
368
            return $structuredOutput->restructure($options);
369
        }
370
        return $structuredOutput;
371
    }
372
373
    /**
374
     * Allow the formatter access to the raw structured data prior
375
     * to restructuring.  For example, the 'list' formatter may wish
376
     * to display the row keys when provided table output.  If this
377
     * function returns a result that does not evaluate to 'false',
378
     * then that result will be used as-is, and restructuring and
379
     * validation will not occur.
380
     *
381
     * @param mixed $structuredOutput
382
     * @param FormatterOptions $options
383
     * @return mixed
384
     */
385
    public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
386
    {
387
        if ($formatter instanceof OverrideRestructureInterface) {
388
            return $formatter->overrideRestructure($structuredOutput, $options);
389
        }
390
    }
391
392
    /**
393
     * Allow the formatter to mess with the configuration options before any
394
     * transformations et. al. get underway.
395
     * @param FormatterInterface $formatter
396
     * @param mixed $structuredOutput
397
     * @param FormatterOptions $options
398
     * @return FormatterOptions
399
     */
400
    public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
401
    {
402
        if ($formatter instanceof OverrideOptionsInterface) {
403
            return $formatter->overrideOptions($structuredOutput, $options);
404
        }
405
        return $options;
406
    }
407
}
408