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

FormatterManager::availableFieldsList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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