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;
24 * Manage a collection of formatters; return one on request.
26 class FormatterManager
28 /** var FormatterInterface[] */
29 protected $formatters = [];
30 /** var SimplifyToArrayInterface[] */
31 protected $arraySimplifiers = [];
33 public function __construct()
37 public function addDefaultFormatters()
39 $defaultFormatters = [
40 'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
41 'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
42 'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
43 'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
44 'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
45 'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
46 'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
47 'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
48 'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
49 'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
50 'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
51 'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
53 if (class_exists('Symfony\Component\VarDumper\Dumper\CliDumper')) {
54 $defaultFormatters['var_dump'] = '\Consolidation\OutputFormatters\Formatters\VarDumpFormatter';
56 foreach ($defaultFormatters as $id => $formatterClassname) {
57 $formatter = new $formatterClassname;
58 $this->addFormatter($id, $formatter);
60 $this->addFormatter('', $this->formatters['string']);
63 public function addDefaultSimplifiers()
65 // Add our default array simplifier (DOMDocument to array)
66 $this->addSimplifier(new DomToArraySimplifier());
72 * @param string $key the identifier of the formatter to add
73 * @param string $formatter the class name of the formatter to add
74 * @return FormatterManager
76 public function addFormatter($key, FormatterInterface $formatter)
78 $this->formatters[$key] = $formatter;
85 * @param SimplifyToArrayInterface $simplifier the array simplifier to add
86 * @return FormatterManager
88 public function addSimplifier(SimplifyToArrayInterface $simplifier)
90 $this->arraySimplifiers[] = $simplifier;
95 * Return a set of InputOption based on the annotations of a command.
96 * @param FormatterOptions $options
97 * @return InputOption[]
99 public function automaticOptions(FormatterOptions $options, $dataType)
101 $automaticOptions = [];
103 // At the moment, we only support automatic options for --format
104 // and --fields, so exit if the command returns no data.
105 if (!isset($dataType)) {
109 $validFormats = $this->validFormats($dataType);
110 if (empty($validFormats)) {
114 $availableFields = $options->get(FormatterOptions::FIELD_LABELS);
115 $hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD);
116 $defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml');
118 if (count($validFormats) > 1) {
119 // Make an input option for --format
120 $description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
121 $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_REQUIRED, $description, $defaultFormat);
124 if ($availableFields) {
125 $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
126 $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
127 $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, $description, $defaultFields);
128 $automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_REQUIRED, "Select just one field, and force format to 'string'.", '');
131 return $automaticOptions;
135 * Given a list of available fields, return a list of field descriptions.
138 protected function availableFieldsList($availableFields)
141 function ($key) use ($availableFields) {
142 return $availableFields[$key] . " ($key)";
144 array_keys($availableFields)
149 * Return the identifiers for all valid data types that have been registered.
151 * @param mixed $dataType \ReflectionObject or other description of the produced data type
154 public function validFormats($dataType)
157 foreach ($this->formatters as $formatId => $formatterName) {
158 $formatter = $this->getFormatter($formatId);
159 if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
160 $validFormats[] = $formatId;
164 return $validFormats;
167 public function isValidFormat(FormatterInterface $formatter, $dataType)
169 if (is_array($dataType)) {
170 $dataType = new \ReflectionClass('\ArrayObject');
172 if (!is_object($dataType) && !class_exists($dataType)) {
175 if (!$dataType instanceof \ReflectionClass) {
176 $dataType = new \ReflectionClass($dataType);
178 return $this->isValidDataType($formatter, $dataType);
181 public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType)
183 if ($this->canSimplifyToArray($dataType)) {
184 if ($this->isValidFormat($formatter, [])) {
188 // If the formatter does not implement ValidationInterface, then
189 // it is presumed that the formatter only accepts arrays.
190 if (!$formatter instanceof ValidationInterface) {
191 return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
193 return $formatter->isValidDataType($dataType);
197 * Format and write output
199 * @param OutputInterface $output Output stream to write to
200 * @param string $format Data format to output in
201 * @param mixed $structuredOutput Data to output
202 * @param FormatterOptions $options Formatting options
204 public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
206 $formatter = $this->getFormatter((string)$format);
207 if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
208 $validFormats = $this->validFormats($structuredOutput);
209 throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
211 if ($structuredOutput instanceof FormatterAwareInterface) {
212 $structuredOutput->setFormatter($formatter);
214 // Give the formatter a chance to override the options
215 $options = $this->overrideOptions($formatter, $structuredOutput, $options);
216 $restructuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
217 if ($formatter instanceof MetadataFormatterInterface) {
218 $formatter->writeMetadata($output, $structuredOutput, $options);
220 $formatter->write($output, $restructuredOutput, $options);
223 protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
225 // Give the formatter a chance to do something with the
226 // raw data before it is restructured.
227 $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
228 if ($overrideRestructure) {
229 return $overrideRestructure;
232 // Restructure the output data (e.g. select fields to display, etc.).
233 $restructuredOutput = $this->restructureData($structuredOutput, $options);
235 // Make sure that the provided data is in the correct format for the selected formatter.
236 $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
238 // Give the original data a chance to re-render the structured
239 // output after it has been restructured and validated.
240 $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
242 return $restructuredOutput;
246 * Fetch the requested formatter.
248 * @param string $format Identifier for requested formatter
249 * @return FormatterInterface
251 public function getFormatter($format)
253 // The client must inject at least one formatter before asking for
254 // any formatters; if not, we will provide all of the usual defaults
256 if (empty($this->formatters)) {
257 $this->addDefaultFormatters();
258 $this->addDefaultSimplifiers();
260 if (!$this->hasFormatter($format)) {
261 throw new UnknownFormatException($format);
263 $formatter = $this->formatters[$format];
268 * Test to see if the stipulated format exists
270 public function hasFormatter($format)
272 return array_key_exists($format, $this->formatters);
276 * Render the data as necessary (e.g. to select or reorder fields).
278 * @param FormatterInterface $formatter
279 * @param mixed $originalData
280 * @param mixed $restructuredData
281 * @param FormatterOptions $options Formatting options
284 public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
286 if ($formatter instanceof RenderDataInterface) {
287 return $formatter->renderData($originalData, $restructuredData, $options);
289 return $restructuredData;
293 * Determine if the provided data is compatible with the formatter being used.
295 * @param FormatterInterface $formatter Formatter being used
296 * @param mixed $structuredOutput Data to validate
299 public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
301 // If the formatter implements ValidationInterface, then let it
302 // test the data and throw or return an error
303 if ($formatter instanceof ValidationInterface) {
304 return $formatter->validate($structuredOutput);
306 // If the formatter does not implement ValidationInterface, then
307 // it will never be passed an ArrayObject; we will always give
308 // it a simple array.
309 $structuredOutput = $this->simplifyToArray($structuredOutput, $options);
310 // If we could not simplify to an array, then throw an exception.
311 // We will never give a formatter anything other than an array
312 // unless it validates that it can accept the data type.
313 if (!is_array($structuredOutput)) {
314 throw new IncompatibleDataException(
320 return $structuredOutput;
323 protected function simplifyToArray($structuredOutput, FormatterOptions $options)
325 // We can do nothing unless the provided data is an object.
326 if (!is_object($structuredOutput)) {
327 return $structuredOutput;
329 // Check to see if any of the simplifiers can convert the given data
331 $outputDataType = new \ReflectionClass($structuredOutput);
332 foreach ($this->arraySimplifiers as $simplifier) {
333 if ($simplifier->canSimplify($outputDataType)) {
334 $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
337 // Convert data structure back into its original form, if necessary.
338 if ($structuredOutput instanceof OriginalDataInterface) {
339 return $structuredOutput->getOriginalData();
341 // Convert \ArrayObjects to a simple array.
342 if ($structuredOutput instanceof \ArrayObject) {
343 return $structuredOutput->getArrayCopy();
345 return $structuredOutput;
348 protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
350 foreach ($this->arraySimplifiers as $simplifier) {
351 if ($simplifier->canSimplify($structuredOutput)) {
359 * Restructure the data as necessary (e.g. to select or reorder fields).
361 * @param mixed $structuredOutput
362 * @param FormatterOptions $options
365 public function restructureData($structuredOutput, FormatterOptions $options)
367 if ($structuredOutput instanceof RestructureInterface) {
368 return $structuredOutput->restructure($options);
370 return $structuredOutput;
374 * Allow the formatter access to the raw structured data prior
375 * to restructuring. For example, the 'list' formatter may wish
376 * to display the row keys when provided table output. If this
377 * function returns a result that does not evaluate to 'false',
378 * then that result will be used as-is, and restructuring and
379 * validation will not occur.
381 * @param mixed $structuredOutput
382 * @param FormatterOptions $options
385 public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
387 if ($formatter instanceof OverrideRestructureInterface) {
388 return $formatter->overrideRestructure($structuredOutput, $options);
393 * Allow the formatter to mess with the configuration options before any
394 * transformations et. al. get underway.
395 * @param FormatterInterface $formatter
396 * @param mixed $structuredOutput
397 * @param FormatterOptions $options
398 * @return FormatterOptions
400 public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
402 if ($formatter instanceof OverrideOptionsInterface) {
403 return $formatter->overrideOptions($structuredOutput, $options);