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

FormatterManager   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 58
lcom 1
cbo 8
dl 0
loc 382
rs 6.3005
c 3
b 0
f 0
ccs 54
cts 54
cp 1

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
A addFormatter() 0 5 1
A addSimplifier() 0 5 1
B automaticOptions() 0 33 5
A availableFieldsList() 0 9 1
C validFormats() 0 26 8
A isValidFormat() 0 10 2
A isUniversalFormat() 0 8 2
D isValidFormatForSpecifiedDataType() 0 26 10
A write() 0 12 3
A validateAndRestructure() 0 21 2
A getFormatter() 0 8 2
A hasFormatter() 0 4 1
A renderData() 0 7 2
A validateData() 0 23 3
B simplifyToArray() 0 20 5
A canSimplifyToArray() 0 9 3
A restructureData() 0 7 2
A overrideRestructure() 0 6 2
A overrideOptions() 0 7 2

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\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
        $defaultFormat = 'yaml';
79 28
80
        // At the moment, we only support automatic options for --format
81 28
        // and --fields, so exit if the command returns no data.
82 1
        if (!isset($dataType)) {
83
            return [];
84
        }
85 27
86 27
        $validFormats = $this->validFormats($dataType);
87 9
        if (empty($validFormats)) {
88 9
            return [];
89 27
        }
90
91
        if (count($validFormats) > 1) {
92 28
            // Make an input option for --format
93
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
94 28
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_OPTIONAL, $description, $defaultFormat);
95
        }
96
97
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS, [], false);
98
        if ($availableFields) {
99
            // We have fields; that implies 'table', unless someone says something different
100
            $defaultFormat = 'table';
0 ignored issues
show
Unused Code introduced by
$defaultFormat is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
101
102
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
103
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields);
0 ignored issues
show
Bug introduced by
The variable $defaultFields does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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