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

FormatterManager   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

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

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