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