Completed
Push — master ( eb54eb...d2c0dd )
by Greg
02:25
created

FormatterManager   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 349
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 51
c 3
b 0
f 0
lcom 1
cbo 8
dl 0
loc 349
ccs 49
cts 49
cp 1
rs 8.3206

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 23 1
A addFormatter() 0 5 1
A addSimplifier() 0 5 1
B automaticOptions() 0 32 6
A availableFieldsList() 0 9 1
A validFormats() 0 12 4
B isValidFormat() 0 13 5
B isValidDataType() 0 14 5
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\FormatterInterface;
8
use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
9
use Consolidation\OutputFormatters\Options\FormatterOptions;
10
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
11
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
12
use Consolidation\OutputFormatters\Transformations\DomToArraySimplifier;
13
use Consolidation\OutputFormatters\Transformations\OverrideRestructureInterface;
14
use Consolidation\OutputFormatters\Transformations\SimplifyToArrayInterface;
15
use Consolidation\OutputFormatters\Validate\ValidationInterface;
16 28
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Output\OutputInterface;
18 28
19 28
/**
20 28
 * Manage a collection of formatters; return one on request.
21 28
 */
22 28
class FormatterManager
23 28
{
24 28
    protected $formatters = [];
25 28
    protected $arraySimplifiers = [];
26 28
27 28
    public function __construct()
28
    {
29
        $this->formatters = [
30
            'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
31 28
            'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
32 28
            'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
33
            'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
34
            'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
35
            'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
36
            'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
37
            'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
38
            'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
39
            'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
40
            'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
41
            'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
42
        ];
43 28
44
        // Make the empty format an alias for the 'string' formatter.
45 28
        $this->addFormatter('', $this->formatters['string']);
46 27
47 24
        // Add our default array simplifier (DOMDocument to array)
48 24
        $this->addSimplifier(new DomToArraySimplifier());
49
    }
50 27
51
    /**
52
     * Add a formatter
53
     *
54 27
     * @param string $key the identifier of the formatter to add
55 27
     * @param string $formatterClassname the class name of the formatter to add
56 4
     * @return FormatterManager
57
     */
58
    public function addFormatter($key, $formatterClassname)
59
    {
60 27
        $this->formatters[$key] = $formatterClassname;
61
        return $this;
62
    }
63 27
64
    /**
65
     * Add a simplifier
66
     *
67 24
     * @param SimplifyToArrayInterface $simplifier the array simplifier to add
68
     * @return FormatterManager
69 24
     */
70
    public function addSimplifier(SimplifyToArrayInterface $simplifier)
71
    {
72
        $this->arraySimplifiers[] = $simplifier;
73
        return $this;
74
    }
75
76
    /**
77
     * Return a set of InputOption based on the annotations of a command.
78
     * @param FormatterOptions $options
79 28
     * @return InputOption[]
80
     */
81 28
    public function automaticOptions(FormatterOptions $options, $dataType)
82 1
    {
83
        $automaticOptions = [];
84
85 27
        // At the moment, we only support automatic options for --format
86 27
        // and --fields, so exit if the command returns no data.
87 9
        if (!isset($dataType)) {
88 9
            return [];
89 27
        }
90
91
        $validFormats = $this->validFormats($dataType);
92 28
        if (empty($validFormats)) {
93
            return [];
94 28
        }
95
96
        $availableFields = $options->get(FormatterOptions::FIELD_LABELS, [], false);
97
        $defaultFormat = $availableFields ? 'table' : 'yaml';
98
99
        if (count($validFormats) > 1) {
100
            // Make an input option for --format
101
            $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
102
            $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_OPTIONAL, $description, $defaultFormat);
103
        }
104
105
        if ($availableFields) {
106
            $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], implode(',', $availableFields));
107 24
            $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
108
            $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields);
109 24
        }
110 14
111
        return $automaticOptions;
112 14
    }
113
114
    /**
115
     * Given a list of available fields, return a list of field descriptions.
116
     * @return string[]
117
     */
118
    protected function availableFieldsList($availableFields)
119
    {
120
        return array_map(
121
            function ($key) use ($availableFields) {
122 27
                return $availableFields[$key] . " ($key)";
123
            },
124
            array_keys($availableFields)
125
        );
126 27
    }
127 16
128
    /**
129
     * Return the identifiers for all valid data types that have been registered.
130
     *
131
     * @param mixed $dataType \ReflectionObject or other description of the produced data type
132 15
     * @return array
133 4
     */
134
    public function validFormats($dataType)
135
    {
136 11
        $validFormats = [];
137
        foreach ($this->formatters as $formatId => $formatterName) {
138
            $formatter = $this->getFormatter($formatId);
139
            if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
140
                $validFormats[] = $formatId;
141
            }
142
        }
143
        sort($validFormats);
144
        return $validFormats;
145
    }
146
147 27
    public function isValidFormat(FormatterInterface $formatter, $dataType)
148
    {
149 27
        if (is_array($dataType)) {
150 7
            $dataType = new \ReflectionClass('\ArrayObject');
151
        }
152 20
        if (!is_object($dataType) && !class_exists($dataType)) {
153
            return false;
154
        }
155
        if (!$dataType instanceof \ReflectionClass) {
156
            $dataType = new \ReflectionClass($dataType);
157
        }
158
        return $this->isValidDataType($formatter, $dataType);
159
    }
160
161
    public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType)
162
    {
163
        if ($this->canSimplifyToArray($dataType)) {
164
            if ($this->isValidFormat($formatter, [])) {
165
                return true;
166
            }
167
        }
168 27
        // If the formatter does not implement ValidationInterface, then
169
        // it is presumed that the formatter only accepts arrays.
170 27
        if (!$formatter instanceof ValidationInterface) {
171 5
            return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
172
        }
173 26
        return $formatter->isValidDataType($dataType);
174
    }
175
176
    /**
177
     * Format and write output
178
     *
179
     * @param OutputInterface $output Output stream to write to
180
     * @param string $format Data format to output in
181
     * @param mixed $structuredOutput Data to output
182
     * @param FormatterOptions $options Formatting options
183
     */
184
    public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
185
    {
186
        $formatter = $this->getFormatter((string)$format);
187
        if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
188
            $validFormats = $this->validFormats($structuredOutput);
189
            throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
190
        }
191
        // Give the formatter a chance to override the options
192
        $options = $this->overrideOptions($formatter, $structuredOutput, $options);
193
        $structuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
194
        $formatter->write($output, $structuredOutput, $options);
195
    }
196
197
    protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
198
    {
199
        // Give the formatter a chance to do something with the
200
        // raw data before it is restructured.
201
        $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
202
        if ($overrideRestructure) {
203
            return $overrideRestructure;
204
        }
205
206
        // Restructure the output data (e.g. select fields to display, etc.).
207
        $restructuredOutput = $this->restructureData($structuredOutput, $options);
208
209
        // Make sure that the provided data is in the correct format for the selected formatter.
210
        $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
211
212
        // Give the original data a chance to re-render the structured
213
        // output after it has been restructured and validated.
214
        $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
215
216
        return $restructuredOutput;
217
    }
218
219
    /**
220
     * Fetch the requested formatter.
221
     *
222
     * @param string $format Identifier for requested formatter
223
     * @return FormatterInterface
224
     */
225
    public function getFormatter($format)
226
    {
227
        if (!$this->hasFormatter($format)) {
228
            throw new UnknownFormatException($format);
229
        }
230
        $formatter = new $this->formatters[$format];
231
        return $formatter;
232
    }
233
234
    /**
235
     * Test to see if the stipulated format exists
236
     */
237
    public function hasFormatter($format)
238
    {
239
        return array_key_exists($format, $this->formatters);
240
    }
241
242
    /**
243
     * Render the data as necessary (e.g. to select or reorder fields).
244
     *
245
     * @param FormatterInterface $formatter
246
     * @param mixed $originalData
247
     * @param mixed $restructuredData
248
     * @param FormatterOptions $options Formatting options
249
     * @return mixed
250
     */
251
    public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
252
    {
253
        if ($formatter instanceof RenderDataInterface) {
254
            return $formatter->renderData($originalData, $restructuredData, $options);
255
        }
256
        return $restructuredData;
257
    }
258
259
    /**
260
     * Determine if the provided data is compatible with the formatter being used.
261
     *
262
     * @param FormatterInterface $formatter Formatter being used
263
     * @param mixed $structuredOutput Data to validate
264
     * @return mixed
265
     */
266
    public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
267
    {
268
        // If the formatter implements ValidationInterface, then let it
269
        // test the data and throw or return an error
270
        if ($formatter instanceof ValidationInterface) {
271
            return $formatter->validate($structuredOutput);
272
        }
273
        // If the formatter does not implement ValidationInterface, then
274
        // it will never be passed an ArrayObject; we will always give
275
        // it a simple array.
276
        $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
277
        // If we could not simplify to an array, then throw an exception.
278
        // We will never give a formatter anything other than an array
279
        // unless it validates that it can accept the data type.
280
        if (!is_array($structuredOutput)) {
281
            throw new IncompatibleDataException(
282
                $formatter,
283
                $structuredOutput,
284
                []
285
            );
286
        }
287
        return $structuredOutput;
288
    }
289
290
    protected function simplifyToArray($structuredOutput, FormatterOptions $options)
291
    {
292
        // We can do nothing unless the provided data is an object.
293
        if (!is_object($structuredOutput)) {
294
            return $structuredOutput;
295
        }
296
        // Check to see if any of the simplifiers can convert the given data
297
        // set to an array.
298
        $outputDataType = new \ReflectionClass($structuredOutput);
299
        foreach ($this->arraySimplifiers as $simplifier) {
300
            if ($simplifier->canSimplify($outputDataType)) {
301
                $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
302
            }
303
        }
304
        // Convert \ArrayObjects to a simple array.
305
        if ($structuredOutput instanceof \ArrayObject) {
306
            return $structuredOutput->getArrayCopy();
307
        }
308
        return $structuredOutput;
309
    }
310
311
    protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
312
    {
313
        foreach ($this->arraySimplifiers as $simplifier) {
314
            if ($simplifier->canSimplify($structuredOutput)) {
315
                return true;
316
            }
317
        }
318
        return false;
319
    }
320
321
    /**
322
     * Restructure the data as necessary (e.g. to select or reorder fields).
323
     *
324
     * @param mixed $structuredOutput
325
     * @param FormatterOptions $options
326
     * @return mixed
327
     */
328
    public function restructureData($structuredOutput, FormatterOptions $options)
329
    {
330
        if ($structuredOutput instanceof RestructureInterface) {
331
            return $structuredOutput->restructure($options);
332
        }
333
        return $structuredOutput;
334
    }
335
336
    /**
337
     * Allow the formatter access to the raw structured data prior
338
     * to restructuring.  For example, the 'list' formatter may wish
339
     * to display the row keys when provided table output.  If this
340
     * function returns a result that does not evaluate to 'false',
341
     * then that result will be used as-is, and restructuring and
342
     * validation will not occur.
343
     *
344
     * @param mixed $structuredOutput
345
     * @param FormatterOptions $options
346
     * @return mixed
347
     */
348
    public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
349
    {
350
        if ($formatter instanceof OverrideRestructureInterface) {
351
            return $formatter->overrideRestructure($structuredOutput, $options);
352
        }
353
    }
354
355
    /**
356
     * Allow the formatter to mess with the configuration options before any
357
     * transformations et. al. get underway.
358
     * @param FormatterInterface $formatter
359
     * @param mixed $structuredOutput
360
     * @param FormatterOptions $options
361
     * @return FormatterOptions
362
     */
363
    public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
364
    {
365
        if ($formatter instanceof OverrideOptionsInterface) {
366
            return $formatter->overrideOptions($structuredOutput, $options);
367
        }
368
        return $options;
369
    }
370
}
371