Completed
Push — master ( e38a63...c3a6af )
by Greg
10s
created

FormatterManager::validateData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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