Completed
Push — master ( d2c0dd...9f4048 )
by Greg
02:22
created

FormatterManager::automaticOptions()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

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