Completed
Pull Request — master (#27)
by Greg
02:20
created

FormatterManager   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

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

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A addDefaultFormatters() 0 22 2
A addDefaultSimplifiers() 0 5 1
A addFormatter() 0 5 1
A addSimplifier() 0 5 1
C automaticOptions() 0 34 7
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 15 3
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
    }
30
31 28
    public function addDefaultFormatters()
32 28
    {
33
        $defaultFormatters = [
34
            'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
35
            'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
36
            'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
37
            'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
38
            'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
39
            'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
40
            'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
41
            'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
42
            'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
43 28
            'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
44
            'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
45 28
            'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
46 27
        ];
47 24
        foreach ($defaultFormatters as $id => $formatterClassname) {
48 24
            $formatter = new $formatterClassname;
49
            $this->addFormatter($id, $formatter);
50 27
        }
51
        $this->addFormatter('', $this->formatters['string']);
52
    }
53
54 27
    public function addDefaultSimplifiers()
55 27
    {
56 4
        // Add our default array simplifier (DOMDocument to array)
57
        $this->addSimplifier(new DomToArraySimplifier());
58
    }
59
60 27
    /**
61
     * Add a formatter
62
     *
63 27
     * @param string $key the identifier of the formatter to add
64
     * @param string $formatterClassname the class name of the formatter to add
0 ignored issues
show
Documentation introduced by
There is no parameter named $formatterClassname. Did you maybe mean $formatter?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

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