2 namespace Consolidation\OutputFormatters;
4 use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
5 use Consolidation\OutputFormatters\Exception\InvalidFormatException;
6 use Consolidation\OutputFormatters\Exception\UnknownFormatException;
7 use Consolidation\OutputFormatters\Formatters\FormatterAwareInterface;
8 use Consolidation\OutputFormatters\Formatters\FormatterInterface;
9 use Consolidation\OutputFormatters\Formatters\MetadataFormatterInterface;
10 use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
11 use Consolidation\OutputFormatters\Options\FormatterOptions;
12 use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
13 use Consolidation\OutputFormatters\StructuredData\MetadataInterface;
14 use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
15 use Consolidation\OutputFormatters\Transformations\DomToArraySimplifier;
16 use Consolidation\OutputFormatters\Transformations\OverrideRestructureInterface;
17 use Consolidation\OutputFormatters\Transformations\SimplifyToArrayInterface;
18 use Consolidation\OutputFormatters\Validate\ValidationInterface;
19 use Symfony\Component\Console\Input\InputOption;
20 use Symfony\Component\Console\Output\OutputInterface;
21 use Consolidation\OutputFormatters\StructuredData\OriginalDataInterface;
22 use Consolidation\OutputFormatters\StructuredData\ListDataFromKeys;
23 use Consolidation\OutputFormatters\StructuredData\ConversionInterface;
26 * Manage a collection of formatters; return one on request.
28 class FormatterManager
30 /** var FormatterInterface[] */
31 protected $formatters = [];
32 /** var SimplifyToArrayInterface[] */
33 protected $arraySimplifiers = [];
35 public function __construct()
39 public function addDefaultFormatters()
41 $defaultFormatters = [
42 'null' => '\Consolidation\OutputFormatters\Formatters\NoOutputFormatter',
43 'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
44 'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
45 'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
46 'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
47 'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
48 'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
49 'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
50 'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
51 'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
52 'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
53 'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
54 'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
56 if (class_exists('Symfony\Component\VarDumper\Dumper\CliDumper')) {
57 $defaultFormatters['var_dump'] = '\Consolidation\OutputFormatters\Formatters\VarDumpFormatter';
59 foreach ($defaultFormatters as $id => $formatterClassname) {
60 $formatter = new $formatterClassname;
61 $this->addFormatter($id, $formatter);
63 $this->addFormatter('', $this->formatters['string']);
66 public function addDefaultSimplifiers()
68 // Add our default array simplifier (DOMDocument to array)
69 $this->addSimplifier(new DomToArraySimplifier());
75 * @param string $key the identifier of the formatter to add
76 * @param string $formatter the class name of the formatter to add
77 * @return FormatterManager
79 public function addFormatter($key, FormatterInterface $formatter)
81 $this->formatters[$key] = $formatter;
88 * @param SimplifyToArrayInterface $simplifier the array simplifier to add
89 * @return FormatterManager
91 public function addSimplifier(SimplifyToArrayInterface $simplifier)
93 $this->arraySimplifiers[] = $simplifier;
98 * Return a set of InputOption based on the annotations of a command.
99 * @param FormatterOptions $options
100 * @return InputOption[]
102 public function automaticOptions(FormatterOptions $options, $dataType)
104 $automaticOptions = [];
106 // At the moment, we only support automatic options for --format
107 // and --fields, so exit if the command returns no data.
108 if (!isset($dataType)) {
112 $validFormats = $this->validFormats($dataType);
113 if (empty($validFormats)) {
117 $availableFields = $options->get(FormatterOptions::FIELD_LABELS);
118 $hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD);
119 $defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml');
121 if (count($validFormats) > 1) {
122 // Make an input option for --format
123 $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
124 $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_REQUIRED, $description, $defaultFormat);
127 $dataTypeClass = ($dataType instanceof \ReflectionClass) ? $dataType : new \ReflectionClass($dataType);
129 if ($availableFields) {
130 $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
131 $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
132 $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, $description, $defaultFields);
133 } elseif ($dataTypeClass->implementsInterface('Consolidation\OutputFormatters\StructuredData\RestructureInterface')) {
134 $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, 'Limit output to only the listed elements. Name top-level elements by key, e.g. "--fields=name,date", or use dot notation to select a nested element, e.g. "--fields=a.b.c as example".', []);
137 if (isset($automaticOptions[FormatterOptions::FIELDS])) {
138 $automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_REQUIRED, "Select just one field, and force format to 'string'.", '');
141 return $automaticOptions;
145 * Given a list of available fields, return a list of field descriptions.
148 protected function availableFieldsList($availableFields)
151 function ($key) use ($availableFields) {
152 return $availableFields[$key] . " ($key)";
154 array_keys($availableFields)
159 * Return the identifiers for all valid data types that have been registered.
161 * @param mixed $dataType \ReflectionObject or other description of the produced data type
164 public function validFormats($dataType)
167 foreach ($this->formatters as $formatId => $formatterName) {
168 $formatter = $this->getFormatter($formatId);
169 if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
170 $validFormats[] = $formatId;
174 return $validFormats;
177 public function isValidFormat(FormatterInterface $formatter, $dataType)
179 if (is_array($dataType)) {
180 $dataType = new \ReflectionClass('\ArrayObject');
182 if (!is_object($dataType) && !class_exists($dataType)) {
185 if (!$dataType instanceof \ReflectionClass) {
186 $dataType = new \ReflectionClass($dataType);
188 return $this->isValidDataType($formatter, $dataType);
191 public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType)
193 if ($this->canSimplifyToArray($dataType)) {
194 if ($this->isValidFormat($formatter, [])) {
198 // If the formatter does not implement ValidationInterface, then
199 // it is presumed that the formatter only accepts arrays.
200 if (!$formatter instanceof ValidationInterface) {
201 return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
203 return $formatter->isValidDataType($dataType);
207 * Format and write output
209 * @param OutputInterface $output Output stream to write to
210 * @param string $format Data format to output in
211 * @param mixed $structuredOutput Data to output
212 * @param FormatterOptions $options Formatting options
214 public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
216 // Convert the data to another format (e.g. converting from RowsOfFields to
217 // UnstructuredListData when the fields indicate an unstructured transformation
219 $structuredOutput = $this->convertData($structuredOutput, $options);
221 // TODO: If the $format is the default format (not selected by the user), and
222 // if `convertData` switched us to unstructured data, then select a new default
223 // format (e.g. yaml) if the selected format cannot render the converted data.
224 $formatter = $this->getFormatter((string)$format);
226 // If the data format is not applicable for the selected formatter, throw an error.
227 if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
228 $validFormats = $this->validFormats($structuredOutput);
229 throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
231 if ($structuredOutput instanceof FormatterAwareInterface) {
232 $structuredOutput->setFormatter($formatter);
234 // Give the formatter a chance to override the options
235 $options = $this->overrideOptions($formatter, $structuredOutput, $options);
236 $restructuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
237 if ($formatter instanceof MetadataFormatterInterface) {
238 $formatter->writeMetadata($output, $structuredOutput, $options);
240 $formatter->write($output, $restructuredOutput, $options);
243 protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
245 // Give the formatter a chance to do something with the
246 // raw data before it is restructured.
247 $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
248 if ($overrideRestructure) {
249 return $overrideRestructure;
252 // Restructure the output data (e.g. select fields to display, etc.).
253 $restructuredOutput = $this->restructureData($structuredOutput, $options);
255 // Make sure that the provided data is in the correct format for the selected formatter.
256 $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
258 // Give the original data a chance to re-render the structured
259 // output after it has been restructured and validated.
260 $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
262 return $restructuredOutput;
266 * Fetch the requested formatter.
268 * @param string $format Identifier for requested formatter
269 * @return FormatterInterface
271 public function getFormatter($format)
273 // The client must inject at least one formatter before asking for
274 // any formatters; if not, we will provide all of the usual defaults
276 if (empty($this->formatters)) {
277 $this->addDefaultFormatters();
278 $this->addDefaultSimplifiers();
280 if (!$this->hasFormatter($format)) {
281 throw new UnknownFormatException($format);
283 $formatter = $this->formatters[$format];
288 * Test to see if the stipulated format exists
290 public function hasFormatter($format)
292 return array_key_exists($format, $this->formatters);
296 * Render the data as necessary (e.g. to select or reorder fields).
298 * @param FormatterInterface $formatter
299 * @param mixed $originalData
300 * @param mixed $restructuredData
301 * @param FormatterOptions $options Formatting options
304 public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
306 if ($formatter instanceof RenderDataInterface) {
307 return $formatter->renderData($originalData, $restructuredData, $options);
309 return $restructuredData;
313 * Determine if the provided data is compatible with the formatter being used.
315 * @param FormatterInterface $formatter Formatter being used
316 * @param mixed $structuredOutput Data to validate
319 public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
321 // If the formatter implements ValidationInterface, then let it
322 // test the data and throw or return an error
323 if ($formatter instanceof ValidationInterface) {
324 return $formatter->validate($structuredOutput);
326 // If the formatter does not implement ValidationInterface, then
327 // it will never be passed an ArrayObject; we will always give
328 // it a simple array.
329 $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
330 // If we could not simplify to an array, then throw an exception.
331 // We will never give a formatter anything other than an array
332 // unless it validates that it can accept the data type.
333 if (!is_array($structuredOutput)) {
334 throw new IncompatibleDataException(
340 return $structuredOutput;
343 protected function simplifyToArray($structuredOutput, FormatterOptions $options)
345 // We can do nothing unless the provided data is an object.
346 if (!is_object($structuredOutput)) {
347 return $structuredOutput;
349 // Check to see if any of the simplifiers can convert the given data
351 $outputDataType = new \ReflectionClass($structuredOutput);
352 foreach ($this->arraySimplifiers as $simplifier) {
353 if ($simplifier->canSimplify($outputDataType)) {
354 $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
357 // Convert data structure back into its original form, if necessary.
358 if ($structuredOutput instanceof OriginalDataInterface) {
359 return $structuredOutput->getOriginalData();
361 // Convert \ArrayObjects to a simple array.
362 if ($structuredOutput instanceof \ArrayObject) {
363 return $structuredOutput->getArrayCopy();
365 return $structuredOutput;
368 protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
370 foreach ($this->arraySimplifiers as $simplifier) {
371 if ($simplifier->canSimplify($structuredOutput)) {
379 * Convert from one format to another if necessary prior to restructuring.
381 public function convertData($structuredOutput, FormatterOptions $options)
383 if ($structuredOutput instanceof ConversionInterface) {
384 return $structuredOutput->convert($options);
386 return $structuredOutput;
390 * Restructure the data as necessary (e.g. to select or reorder fields).
392 * @param mixed $structuredOutput
393 * @param FormatterOptions $options
396 public function restructureData($structuredOutput, FormatterOptions $options)
398 if ($structuredOutput instanceof RestructureInterface) {
399 return $structuredOutput->restructure($options);
401 return $structuredOutput;
405 * Allow the formatter access to the raw structured data prior
406 * to restructuring. For example, the 'list' formatter may wish
407 * to display the row keys when provided table output. If this
408 * function returns a result that does not evaluate to 'false',
409 * then that result will be used as-is, and restructuring and
410 * validation will not occur.
412 * @param mixed $structuredOutput
413 * @param FormatterOptions $options
416 public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
418 if ($formatter instanceof OverrideRestructureInterface) {
419 return $formatter->overrideRestructure($structuredOutput, $options);
424 * Allow the formatter to mess with the configuration options before any
425 * transformations et. al. get underway.
426 * @param FormatterInterface $formatter
427 * @param mixed $structuredOutput
428 * @param FormatterOptions $options
429 * @return FormatterOptions
431 public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
433 if ($formatter instanceof OverrideOptionsInterface) {
434 return $formatter->overrideOptions($structuredOutput, $options);