FormatterManager   F
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 66
lcom 1
cbo 11
dl 0
loc 416
ccs 43
cts 43
cp 1
rs 3.12
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A addDefaultFormatters() 0 26 3
A addDefaultSimplifiers() 0 5 1
A addFormatter() 0 5 1
A addSimplifier() 0 5 1
B automaticOptions() 0 41 10
A availableFieldsList() 0 9 1
A validFormats() 0 12 4
A isValidFormat() 0 13 5
A isValidDataType() 0 14 5
A write() 0 28 5
A validateAndRestructure() 0 21 2
A getFormatter() 0 15 3
A hasFormatter() 0 4 1
A renderData() 0 7 2
A validateData() 0 23 3
B simplifyToArray() 0 24 6
A canSimplifyToArray() 0 9 3
A convertData() 0 7 2
A restructureData() 0 7 2
A overrideRestructure() 0 6 2
A overrideOptions() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like FormatterManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FormatterManager, and based on these observations, apply Extract Interface, too.

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