default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions

View File

@@ -0,0 +1,57 @@
<?php
namespace Consolidation\OutputFormatters\Exception;
/**
* Contains some helper functions used by exceptions in this project.
*/
abstract class AbstractDataFormatException extends \Exception
{
/**
* Return a description of the data type represented by the provided parameter.
*
* @param \ReflectionClass $data The data type to describe. Note that
* \ArrayObject is used as a proxy to mean an array primitive (or an ArrayObject).
* @return string
*/
protected static function describeDataType($data)
{
if (is_array($data) || ($data instanceof \ReflectionClass)) {
if (is_array($data) || ($data->getName() == 'ArrayObject')) {
return 'an array';
}
return 'an instance of ' . $data->getName();
}
if (is_string($data)) {
return 'a string';
}
if (is_object($data)) {
return 'an instance of ' . get_class($data);
}
throw new \Exception("Undescribable data error: " . var_export($data, true));
}
protected static function describeAllowedTypes($allowedTypes)
{
if (is_array($allowedTypes) && !empty($allowedTypes)) {
if (count($allowedTypes) > 1) {
return static::describeListOfAllowedTypes($allowedTypes);
}
$allowedTypes = $allowedTypes[0];
}
return static::describeDataType($allowedTypes);
}
protected static function describeListOfAllowedTypes($allowedTypes)
{
$descriptions = [];
foreach ($allowedTypes as $oneAllowedType) {
$descriptions[] = static::describeDataType($oneAllowedType);
}
if (count($descriptions) == 2) {
return "either {$descriptions[0]} or {$descriptions[1]}";
}
$lastDescription = array_pop($descriptions);
$otherDescriptions = implode(', ', $descriptions);
return "one of $otherDescriptions or $lastDescription";
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Consolidation\OutputFormatters\Exception;
use Consolidation\OutputFormatters\Formatters\FormatterInterface;
/**
* Represents an incompatibility between the output data and selected formatter.
*/
class IncompatibleDataException extends AbstractDataFormatException
{
public function __construct(FormatterInterface $formatter, $data, $allowedTypes)
{
$formatterDescription = get_class($formatter);
$dataDescription = static::describeDataType($data);
$allowedTypesDescription = static::describeAllowedTypes($allowedTypes);
$message = "Data provided to $formatterDescription must be $allowedTypesDescription. Instead, $dataDescription was provided.";
parent::__construct($message, 1);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Consolidation\OutputFormatters\Exception;
/**
* Represents an incompatibility between the output data and selected formatter.
*/
class InvalidFormatException extends AbstractDataFormatException
{
public function __construct($format, $data, $validFormats)
{
$dataDescription = static::describeDataType($data);
$message = "The format $format cannot be used with the data produced by this command, which was $dataDescription. Valid formats are: " . implode(',', $validFormats);
parent::__construct($message, 1);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Consolidation\OutputFormatters\Exception;
/**
* Indicates that the requested format does not exist.
*/
class UnknownFieldException extends \Exception
{
public function __construct($field)
{
$message = "The requested field, '$field', is not defined.";
parent::__construct($message, 1);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Consolidation\OutputFormatters\Exception;
/**
* Indicates that the requested format does not exist.
*/
class UnknownFormatException extends \Exception
{
public function __construct($format)
{
$message = "The requested format, '$format', is not available.";
parent::__construct($message, 1);
}
}

View File

@@ -0,0 +1,444 @@
<?php
namespace Consolidation\OutputFormatters;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Consolidation\OutputFormatters\Exception\InvalidFormatException;
use Consolidation\OutputFormatters\Exception\UnknownFormatException;
use Consolidation\OutputFormatters\Formatters\FormatterAwareInterface;
use Consolidation\OutputFormatters\Formatters\FormatterInterface;
use Consolidation\OutputFormatters\Formatters\MetadataFormatterInterface;
use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
use Consolidation\OutputFormatters\StructuredData\MetadataInterface;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Transformations\DomToArraySimplifier;
use Consolidation\OutputFormatters\Transformations\OverrideRestructureInterface;
use Consolidation\OutputFormatters\Transformations\SimplifyToArrayInterface;
use Consolidation\OutputFormatters\Validate\ValidationInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Consolidation\OutputFormatters\StructuredData\OriginalDataInterface;
use Consolidation\OutputFormatters\StructuredData\ListDataFromKeys;
use Consolidation\OutputFormatters\StructuredData\ConversionInterface;
use Consolidation\OutputFormatters\Formatters\HumanReadableFormat;
/**
* Manage a collection of formatters; return one on request.
*/
class FormatterManager
{
/** var FormatterInterface[] */
protected $formatters = [];
/** var SimplifyToArrayInterface[] */
protected $arraySimplifiers = [];
public function __construct()
{
}
public function addDefaultFormatters()
{
$defaultFormatters = [
'null' => '\Consolidation\OutputFormatters\Formatters\NoOutputFormatter',
'string' => '\Consolidation\OutputFormatters\Formatters\StringFormatter',
'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter',
'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter',
'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter',
'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter',
'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter',
'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter',
'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter',
'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter',
'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter',
'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter',
'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter',
];
if (class_exists('Symfony\Component\VarDumper\Dumper\CliDumper')) {
$defaultFormatters['var_dump'] = '\Consolidation\OutputFormatters\Formatters\VarDumpFormatter';
}
foreach ($defaultFormatters as $id => $formatterClassname) {
$formatter = new $formatterClassname;
$this->addFormatter($id, $formatter);
}
$this->addFormatter('', $this->formatters['string']);
}
public function addDefaultSimplifiers()
{
// Add our default array simplifier (DOMDocument to array)
$this->addSimplifier(new DomToArraySimplifier());
}
/**
* Add a formatter
*
* @param string $key the identifier of the formatter to add
* @param string $formatter the class name of the formatter to add
* @return FormatterManager
*/
public function addFormatter($key, FormatterInterface $formatter)
{
$this->formatters[$key] = $formatter;
return $this;
}
/**
* Add a simplifier
*
* @param SimplifyToArrayInterface $simplifier the array simplifier to add
* @return FormatterManager
*/
public function addSimplifier(SimplifyToArrayInterface $simplifier)
{
$this->arraySimplifiers[] = $simplifier;
return $this;
}
/**
* Return a set of InputOption based on the annotations of a command.
* @param FormatterOptions $options
* @return InputOption[]
*/
public function automaticOptions(FormatterOptions $options, $dataType)
{
$automaticOptions = [];
// At the moment, we only support automatic options for --format
// and --fields, so exit if the command returns no data.
if (!isset($dataType)) {
return [];
}
$validFormats = $this->validFormats($dataType);
if (empty($validFormats)) {
return [];
}
$availableFields = $options->get(FormatterOptions::FIELD_LABELS);
$hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD);
$defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml');
if (count($validFormats) > 1) {
// Make an input option for --format
$description = 'Format the result data. Available formats: ' . implode(',', $validFormats);
$automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_REQUIRED, $description, $defaultFormat);
}
$dataTypeClass = ($dataType instanceof \ReflectionClass) ? $dataType : new \ReflectionClass($dataType);
if ($availableFields) {
$defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
$description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
$automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_REQUIRED, $description, $defaultFields);
} elseif ($dataTypeClass->implementsInterface('Consolidation\OutputFormatters\StructuredData\RestructureInterface')) {
$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".', []);
}
if (isset($automaticOptions[FormatterOptions::FIELDS])) {
$automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_REQUIRED, "Select just one field, and force format to *string*.", '');
}
return $automaticOptions;
}
/**
* Given a list of available fields, return a list of field descriptions.
* @return string[]
*/
protected function availableFieldsList($availableFields)
{
return array_map(
function ($key) use ($availableFields) {
return $availableFields[$key] . " ($key)";
},
array_keys($availableFields)
);
}
/**
* Return the identifiers for all valid data types that have been registered.
*
* @param mixed $dataType \ReflectionObject or other description of the produced data type
* @return array
*/
public function validFormats($dataType)
{
$validFormats = [];
foreach ($this->formatters as $formatId => $formatterName) {
$formatter = $this->getFormatter($formatId);
if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) {
$validFormats[] = $formatId;
}
}
sort($validFormats);
return $validFormats;
}
public function isValidFormat(FormatterInterface $formatter, $dataType)
{
if (is_array($dataType)) {
$dataType = new \ReflectionClass('\ArrayObject');
}
if (!is_object($dataType) && !class_exists($dataType)) {
return false;
}
if (!$dataType instanceof \ReflectionClass) {
$dataType = new \ReflectionClass($dataType);
}
return $this->isValidDataType($formatter, $dataType);
}
public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType)
{
if ($this->canSimplifyToArray($dataType)) {
if ($this->isValidFormat($formatter, [])) {
return true;
}
}
// If the formatter does not implement ValidationInterface, then
// it is presumed that the formatter only accepts arrays.
if (!$formatter instanceof ValidationInterface) {
return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject');
}
return $formatter->isValidDataType($dataType);
}
/**
* Format and write output
*
* @param OutputInterface $output Output stream to write to
* @param string $format Data format to output in
* @param mixed $structuredOutput Data to output
* @param FormatterOptions $options Formatting options
*/
public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options)
{
// Convert the data to another format (e.g. converting from RowsOfFields to
// UnstructuredListData when the fields indicate an unstructured transformation
// is requested).
$structuredOutput = $this->convertData($structuredOutput, $options);
// TODO: If the $format is the default format (not selected by the user), and
// if `convertData` switched us to unstructured data, then select a new default
// format (e.g. yaml) if the selected format cannot render the converted data.
$formatter = $this->getFormatter((string)$format);
// If the data format is not applicable for the selected formatter, throw an error.
if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
$validFormats = $this->validFormats($structuredOutput);
throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
}
if ($structuredOutput instanceof FormatterAwareInterface) {
$structuredOutput->setFormatter($formatter);
}
// Give the formatter a chance to override the options
$options = $this->overrideOptions($formatter, $structuredOutput, $options);
$restructuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
if ($formatter instanceof MetadataFormatterInterface) {
$formatter->writeMetadata($output, $structuredOutput, $options);
}
$formatter->write($output, $restructuredOutput, $options);
}
protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
{
// Give the formatter a chance to do something with the
// raw data before it is restructured.
$overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options);
if ($overrideRestructure) {
return $overrideRestructure;
}
// Restructure the output data (e.g. select fields to display, etc.).
$restructuredOutput = $this->restructureData($structuredOutput, $options);
// Make sure that the provided data is in the correct format for the selected formatter.
$restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options);
// Give the original data a chance to re-render the structured
// output after it has been restructured and validated.
$restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options);
return $restructuredOutput;
}
/**
* Fetch the requested formatter.
*
* @param string $format Identifier for requested formatter
* @return FormatterInterface
*/
public function getFormatter($format)
{
// The client must inject at least one formatter before asking for
// any formatters; if not, we will provide all of the usual defaults
// as a convenience.
if (empty($this->formatters)) {
$this->addDefaultFormatters();
$this->addDefaultSimplifiers();
}
if (!$this->hasFormatter($format)) {
throw new UnknownFormatException($format);
}
$formatter = $this->formatters[$format];
return $formatter;
}
/**
* Test to see if the stipulated format exists
*/
public function hasFormatter($format)
{
return array_key_exists($format, $this->formatters);
}
/**
* Render the data as necessary (e.g. to select or reorder fields).
*
* @param FormatterInterface $formatter
* @param mixed $originalData
* @param mixed $restructuredData
* @param FormatterOptions $options Formatting options
* @return mixed
*/
public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options)
{
if ($formatter instanceof RenderDataInterface) {
return $formatter->renderData($originalData, $restructuredData, $options);
}
return $restructuredData;
}
/**
* Determine if the provided data is compatible with the formatter being used.
*
* @param FormatterInterface $formatter Formatter being used
* @param mixed $structuredOutput Data to validate
* @return mixed
*/
public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
{
// If the formatter implements ValidationInterface, then let it
// test the data and throw or return an error
if ($formatter instanceof ValidationInterface) {
return $formatter->validate($structuredOutput);
}
// If the formatter does not implement ValidationInterface, then
// it will never be passed an ArrayObject; we will always give
// it a simple array.
$structuredOutput = $this->simplifyToArray($structuredOutput, $options);
// If we could not simplify to an array, then throw an exception.
// We will never give a formatter anything other than an array
// unless it validates that it can accept the data type.
if (!is_array($structuredOutput)) {
throw new IncompatibleDataException(
$formatter,
$structuredOutput,
[]
);
}
return $structuredOutput;
}
protected function simplifyToArray($structuredOutput, FormatterOptions $options)
{
// We can do nothing unless the provided data is an object.
if (!is_object($structuredOutput)) {
return $structuredOutput;
}
// Check to see if any of the simplifiers can convert the given data
// set to an array.
$outputDataType = new \ReflectionClass($structuredOutput);
foreach ($this->arraySimplifiers as $simplifier) {
if ($simplifier->canSimplify($outputDataType)) {
$structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options);
}
}
// Convert data structure back into its original form, if necessary.
if ($structuredOutput instanceof OriginalDataInterface) {
return $structuredOutput->getOriginalData();
}
// Convert \ArrayObjects to a simple array.
if ($structuredOutput instanceof \ArrayObject) {
return $structuredOutput->getArrayCopy();
}
return $structuredOutput;
}
protected function canSimplifyToArray(\ReflectionClass $structuredOutput)
{
foreach ($this->arraySimplifiers as $simplifier) {
if ($simplifier->canSimplify($structuredOutput)) {
return true;
}
}
return false;
}
/**
* Convert from one format to another if necessary prior to restructuring.
*/
public function convertData($structuredOutput, FormatterOptions $options)
{
if ($structuredOutput instanceof ConversionInterface) {
return $structuredOutput->convert($options);
}
return $structuredOutput;
}
/**
* Restructure the data as necessary (e.g. to select or reorder fields).
*
* @param mixed $structuredOutput
* @param FormatterOptions $options
* @return mixed
*/
public function restructureData($structuredOutput, FormatterOptions $options)
{
if ($structuredOutput instanceof RestructureInterface) {
return $structuredOutput->restructure($options);
}
return $structuredOutput;
}
/**
* Allow the formatter access to the raw structured data prior
* to restructuring. For example, the 'list' formatter may wish
* to display the row keys when provided table output. If this
* function returns a result that does not evaluate to 'false',
* then that result will be used as-is, and restructuring and
* validation will not occur.
*
* @param mixed $structuredOutput
* @param FormatterOptions $options
* @return mixed
*/
public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
{
if ($formatter instanceof OverrideRestructureInterface) {
return $formatter->overrideRestructure($structuredOutput, $options);
}
}
/**
* Allow the formatter to mess with the configuration options before any
* transformations et. al. get underway.
* @param FormatterInterface $formatter
* @param mixed $structuredOutput
* @param FormatterOptions $options
* @return FormatterOptions
*/
public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options)
{
// Set the "Human Readable" option if the formatter has the HumanReadable marker interface
if ($formatter instanceof HumanReadableFormat) {
$options->setHumanReadable();
}
// The formatter may also make dynamic adjustment to the options.
if ($formatter instanceof OverrideOptionsInterface) {
return $formatter->overrideOptions($structuredOutput, $options);
}
return $options;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Validate\ValidDataTypesInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Consolidation\OutputFormatters\Transformations\TableTransformation;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Comma-separated value formatters
*
* Display the provided structured data in a comma-separated list. If
* there are multiple records provided, then they will be printed
* one per line. The primary data types accepted are RowsOfFields and
* PropertyList. The later behaves exactly like the former, save for
* the fact that it contains but a single row. This formmatter can also
* accept a PHP array; this is also interpreted as a single-row of data
* with no header.
*/
class CsvFormatter implements FormatterInterface, ValidDataTypesInterface, RenderDataInterface
{
use ValidDataTypesTrait;
use RenderTableDataTrait;
public function validDataTypes()
{
return
[
new \ReflectionClass('\Consolidation\OutputFormatters\StructuredData\RowsOfFields'),
new \ReflectionClass('\Consolidation\OutputFormatters\StructuredData\PropertyList'),
new \ReflectionClass('\ArrayObject'),
];
}
public function validate($structuredData)
{
// If the provided data was of class RowsOfFields
// or PropertyList, it will be converted into
// a TableTransformation object.
if (!is_array($structuredData) && (!$structuredData instanceof TableTransformation)) {
throw new IncompatibleDataException(
$this,
$structuredData,
$this->validDataTypes()
);
}
// If the data was provided to us as a single array, then
// convert it to a single row.
if (is_array($structuredData) && !empty($structuredData)) {
$firstRow = reset($structuredData);
if (!is_array($firstRow)) {
return [$structuredData];
}
}
return $structuredData;
}
/**
* Return default values for formatter options
* @return array
*/
protected function getDefaultFormatterOptions()
{
return [
FormatterOptions::INCLUDE_FIELD_LABELS => true,
FormatterOptions::DELIMITER => ',',
FormatterOptions::CSV_ENCLOSURE => '"',
FormatterOptions::CSV_ESCAPE_CHAR => "\\",
];
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$defaults = $this->getDefaultFormatterOptions();
$includeFieldLabels = $options->get(FormatterOptions::INCLUDE_FIELD_LABELS, $defaults);
if ($includeFieldLabels && ($data instanceof TableTransformation)) {
$headers = $data->getHeaders();
$this->writeOneLine($output, $headers, $options);
}
foreach ($data as $line) {
$this->writeOneLine($output, $line, $options);
}
}
/**
* Writes a single a single line of formatted CSV data to the output stream.
*
* @param OutputInterface $output the output stream to write to.
* @param array $data an array of field data to convert to a CSV string.
* @param FormatterOptions $options the specified options for this formatter.
*/
protected function writeOneLine(OutputInterface $output, $data, $options)
{
$defaults = $this->getDefaultFormatterOptions();
$delimiter = $options->get(FormatterOptions::DELIMITER, $defaults);
$enclosure = $options->get(FormatterOptions::CSV_ENCLOSURE, $defaults);
$escapeChar = $options->get(FormatterOptions::CSV_ESCAPE_CHAR, $defaults);
$output->write($this->csvEscape($data, $delimiter, $enclosure, $escapeChar));
}
/**
* Generates a CSV-escaped string from an array of field data.
*
* @param array $data an array of field data to format as a CSV.
* @param string $delimiter the delimiter to use between fields.
* @param string $enclosure character to use when enclosing complex fields.
* @param string $escapeChar character to use when escaping special characters.
*
* @return string|bool the formatted CSV string, or FALSE if the formatting failed.
*/
protected function csvEscape($data, $delimiter = ',', $enclosure = '"', $escapeChar = "\\")
{
$buffer = fopen('php://temp', 'r+');
if (version_compare(PHP_VERSION, '5.5.4', '>=')) {
fputcsv($buffer, $data, $delimiter, $enclosure, $escapeChar);
} else {
fputcsv($buffer, $data, $delimiter, $enclosure);
}
rewind($buffer);
$csv = fgets($buffer);
fclose($buffer);
return $csv;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
interface FormatterAwareInterface
{
public function setFormatter(FormatterInterface $formatter);
public function getFormatter();
public function isHumanReadable();
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
trait FormatterAwareTrait
{
protected $formatter;
public function setFormatter(FormatterInterface $formatter)
{
$this->formatter = $formatter;
}
public function getFormatter()
{
return $this->formatter;
}
public function isHumanReadable()
{
return $this->formatter && $this->formatter instanceof \Consolidation\OutputFormatters\Formatters\HumanReadableFormat;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
interface FormatterInterface
{
/**
* Given structured data, apply appropriate
* formatting, and return a printable string.
*
* @param OutputInterface output stream to write to
* @param mixed $data Structured data to format
* @param FormatterOptions formating options
*/
public function write(OutputInterface $output, $data, FormatterOptions $options);
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
/**
* Marker interface that indicates that a cell data renderer
* (@see Consolidation\OutputFormatters\SturcturedData\RenderCellInterface)
* may test for to determine whether it is allowable to add
* human-readable formatting into the cell data
* (@see Consolidation\OutputFormatters\SturcturedData\NumericCallRenderer).
*/
interface HumanReadableFormat
{
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Json formatter
*
* Convert an array or ArrayObject into Json.
*/
class JsonFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\ListDataInterface;
use Consolidation\OutputFormatters\StructuredData\RenderCellInterface;
use Consolidation\OutputFormatters\Transformations\OverrideRestructureInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Display the data in a simple list.
*
* This formatter prints a plain, unadorned list of data,
* with each data item appearing on a separate line. If you
* wish your list to contain headers, then use the table
* formatter, and wrap your data in an PropertyList.
*/
class ListFormatter implements FormatterInterface, OverrideRestructureInterface, RenderDataInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$output->writeln(implode("\n", $data));
}
/**
* @inheritdoc
*/
public function overrideRestructure($structuredOutput, FormatterOptions $options)
{
// If the structured data implements ListDataInterface,
// then we will render whatever data its 'getListData'
// method provides.
if ($structuredOutput instanceof ListDataInterface) {
return $this->renderData($structuredOutput, $structuredOutput->getListData($options), $options);
}
}
/**
* @inheritdoc
*/
public function renderData($originalData, $restructuredData, FormatterOptions $options)
{
if ($originalData instanceof RenderCellInterface) {
return $this->renderEachCell($originalData, $restructuredData, $options);
}
return $restructuredData;
}
protected function renderEachCell($originalData, $restructuredData, FormatterOptions $options)
{
foreach ($restructuredData as $key => $cellData) {
$restructuredData[$key] = $originalData->renderCell($key, $cellData, $options, $restructuredData);
}
return $restructuredData;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
interface MetadataFormatterInterface
{
/**
* Given some metadata, decide how to display it.
*
* @param OutputInterface output stream to write to
* @param array $metadata associative array containing metadata
* @param FormatterOptions formating options
*/
public function writeMetadata(OutputInterface $output, $metadata, FormatterOptions $options);
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
use Consolidation\OutputFormatters\StructuredData\MetadataInterface;
trait MetadataFormatterTrait
{
/**
* @inheritdoc
*/
public function writeMetadata(OutputInterface $output, $structuredOutput, FormatterOptions $options)
{
$template = $options->get(FormatterOptions::METADATA_TEMPLATE);
if (!$template) {
return;
}
if (!$structuredOutput instanceof MetadataInterface) {
return;
}
$metadata = $structuredOutput->getMetadata();
if (empty($metadata)) {
return;
}
$message = $this->interpolate($template, $metadata);
return $output->writeln($message);
}
/**
* Interpolates context values into the message placeholders.
*
* @author PHP Framework Interoperability Group
*
* @param string $message
* @param array $context
*
* @return string
*/
private function interpolate($message, array $context)
{
// build a replacement array with braces around the context keys
$replace = array();
foreach ($context as $key => $val) {
if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
$replace[sprintf('{%s}', $key)] = $val;
}
}
// interpolate replacement values into the message and return
return strtr($message, $replace);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Validate\ValidationInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Symfony\Component\Console\Output\OutputInterface;
/**
* No output formatter
*
* This formatter never produces any output. It is useful in cases where
* a command should not produce any output by default, but may do so if
* the user explicitly includes a --format option.
*/
class NoOutputFormatter implements FormatterInterface, ValidationInterface
{
/**
* All data types are acceptable.
*/
public function isValidDataType(\ReflectionClass $dataType)
{
return true;
}
/**
* @inheritdoc
*/
public function validate($structuredData)
{
return $structuredData;
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Print_r formatter
*
* Run provided date thruogh print_r.
*/
class PrintRFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$output->writeln(print_r($data, true));
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface RenderDataInterface
{
/**
* Convert the contents of the output data just before it
* is to be printed, prior to output but after restructuring
* and validation.
*
* @param mixed $originalData
* @param mixed $restructuredData
* @param FormatterOptions $options Formatting options
* @return mixed
*/
public function renderData($originalData, $restructuredData, FormatterOptions $options);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RenderCellInterface;
trait RenderTableDataTrait
{
/**
* @inheritdoc
*/
public function renderData($originalData, $restructuredData, FormatterOptions $options)
{
if ($originalData instanceof RenderCellInterface) {
return $this->renderEachCell($originalData, $restructuredData, $options);
}
return $restructuredData;
}
protected function renderEachCell($originalData, $restructuredData, FormatterOptions $options)
{
foreach ($restructuredData as $id => $row) {
foreach ($row as $key => $cellData) {
$restructuredData[$id][$key] = $originalData->renderCell($key, $cellData, $options, $row);
}
}
return $restructuredData;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Consolidation\OutputFormatters\Validate\ValidDataTypesInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Consolidation\OutputFormatters\StructuredData\TableDataInterface;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Consolidation\OutputFormatters\StructuredData\PropertyList;
/**
* Display sections of data.
*
* This formatter takes data in the RowsOfFields data type.
* Each row represents one section; the data in each section
* is rendered in two columns, with the key in the first column
* and the value in the second column.
*/
class SectionsFormatter implements FormatterInterface, ValidDataTypesInterface, RenderDataInterface
{
use ValidDataTypesTrait;
use RenderTableDataTrait;
public function validDataTypes()
{
return
[
new \ReflectionClass('\Consolidation\OutputFormatters\StructuredData\RowsOfFields')
];
}
/**
* @inheritdoc
*/
public function validate($structuredData)
{
// If the provided data was of class RowsOfFields
// or PropertyList, it will be converted into
// a TableTransformation object by the restructure call.
if (!$structuredData instanceof TableDataInterface) {
throw new IncompatibleDataException(
$this,
$structuredData,
$this->validDataTypes()
);
}
return $structuredData;
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $tableTransformer, FormatterOptions $options)
{
$table = new Table($output);
$table->setStyle('compact');
foreach ($tableTransformer as $rowid => $row) {
$rowLabel = $tableTransformer->getRowLabel($rowid);
$output->writeln('');
$output->writeln($rowLabel);
$sectionData = new PropertyList($row);
$sectionOptions = new FormatterOptions([], $options->getOptions());
$sectionTableTransformer = $sectionData->restructure($sectionOptions);
$table->setRows($sectionTableTransformer->getTableData(true));
$table->render();
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Serialize formatter
*
* Run provided date thruogh serialize.
*/
class SerializeFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$output->writeln(serialize($data));
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\StructuredData\UnstructuredInterface;
use Consolidation\OutputFormatters\Transformations\SimplifiedFormatterInterface;
use Consolidation\OutputFormatters\Transformations\StringTransformationInterface;
use Consolidation\OutputFormatters\Validate\ValidationInterface;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Symfony\Component\Console\Output\OutputInterface;
/**
* String formatter
*
* This formatter is used as the default action when no
* particular formatter is requested. It will print the
* provided data only if it is a string; if any other
* type is given, then nothing is printed.
*/
class StringFormatter implements FormatterInterface, ValidationInterface, OverrideOptionsInterface
{
/**
* By default, we assume that we can convert any data type to `string`,
* unless it implements UnstructuredInterface, in which case we won't
* allow the `string` format unless the data type also implements
* StringTransformationInterface.
*/
public function isValidDataType(\ReflectionClass $dataType)
{
if ($dataType->implementsInterface('\Consolidation\OutputFormatters\StructuredData\UnstructuredInterface') && !$dataType->implementsInterface('\Consolidation\OutputFormatters\Transformations\StringTransformationInterface')) {
return false;
}
return true;
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
if (is_string($data)) {
return $output->writeln($data);
}
return $this->reduceToSigleFieldAndWrite($output, $data, $options);
}
/**
* @inheritdoc
*/
public function overrideOptions($structuredOutput, FormatterOptions $options)
{
$defaultField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD, [], '');
$userFields = $options->get(FormatterOptions::FIELDS, [FormatterOptions::FIELDS => $options->get(FormatterOptions::FIELD)]);
$optionsOverride = $options->override([]);
if (empty($userFields) && !empty($defaultField)) {
$optionsOverride->setOption(FormatterOptions::FIELDS, $defaultField);
}
return $optionsOverride;
}
/**
* If the data provided to a 'string' formatter is a table, then try
* to emit it in a simplified form (by default, TSV).
*
* @param OutputInterface $output
* @param mixed $data
* @param FormatterOptions $options
*/
protected function reduceToSigleFieldAndWrite(OutputInterface $output, $data, FormatterOptions $options)
{
if ($data instanceof StringTransformationInterface) {
$simplified = $data->simplifyToString($options);
return $output->write($simplified);
}
$alternateFormatter = new TsvFormatter();
try {
$data = $alternateFormatter->validate($data);
$alternateFormatter->write($output, $data, $options);
} catch (\Exception $e) {
}
}
/**
* Always validate any data, though. This format will never
* cause an error if it is selected for an incompatible data type; at
* worse, it simply does not print any data.
*/
public function validate($structuredData)
{
return $structuredData;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
use Consolidation\OutputFormatters\Validate\ValidDataTypesInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Consolidation\OutputFormatters\StructuredData\TableDataInterface;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Consolidation\OutputFormatters\Transformations\WordWrapper;
use Consolidation\OutputFormatters\Formatters\HumanReadableFormat;
/**
* Display a table of data with the Symfony Table class.
*
* This formatter takes data of either the RowsOfFields or
* PropertyList data type. Tables can be rendered with the
* rows running either vertically (the normal orientation) or
* horizontally. By default, associative lists will be displayed
* as two columns, with the key in the first column and the
* value in the second column.
*/
class TableFormatter implements FormatterInterface, ValidDataTypesInterface, RenderDataInterface, MetadataFormatterInterface, HumanReadableFormat
{
use ValidDataTypesTrait;
use RenderTableDataTrait;
use MetadataFormatterTrait;
protected $fieldLabels;
protected $defaultFields;
public function __construct()
{
}
public function validDataTypes()
{
return
[
new \ReflectionClass('\Consolidation\OutputFormatters\StructuredData\RowsOfFields'),
new \ReflectionClass('\Consolidation\OutputFormatters\StructuredData\PropertyList')
];
}
/**
* @inheritdoc
*/
public function validate($structuredData)
{
// If the provided data was of class RowsOfFields
// or PropertyList, it will be converted into
// a TableTransformation object by the restructure call.
if (!$structuredData instanceof TableDataInterface) {
throw new IncompatibleDataException(
$this,
$structuredData,
$this->validDataTypes()
);
}
return $structuredData;
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $tableTransformer, FormatterOptions $options)
{
$headers = [];
$defaults = [
FormatterOptions::TABLE_STYLE => 'consolidation',
FormatterOptions::INCLUDE_FIELD_LABELS => true,
];
$table = new Table($output);
static::addCustomTableStyles($table);
$table->setStyle($options->get(FormatterOptions::TABLE_STYLE, $defaults));
$isList = $tableTransformer->isList();
$includeHeaders = $options->get(FormatterOptions::INCLUDE_FIELD_LABELS, $defaults);
$listDelimiter = $options->get(FormatterOptions::LIST_DELIMITER, $defaults);
$headers = $tableTransformer->getHeaders();
$data = $tableTransformer->getTableData($includeHeaders && $isList);
if ($listDelimiter) {
if (!empty($headers)) {
array_splice($headers, 1, 0, ':');
}
$data = array_map(function ($item) {
array_splice($item, 1, 0, ':');
return $item;
}, $data);
}
if ($includeHeaders && !$isList) {
$table->setHeaders($headers);
}
// todo: $output->getFormatter();
$data = $this->wrap($headers, $data, $table->getStyle(), $options);
$table->setRows($data);
$table->render();
}
/**
* Wrap the table data
* @param array $data
* @param TableStyle $tableStyle
* @param FormatterOptions $options
* @return array
*/
protected function wrap($headers, $data, TableStyle $tableStyle, FormatterOptions $options)
{
$wrapper = new WordWrapper($options->get(FormatterOptions::TERMINAL_WIDTH));
$wrapper->setPaddingFromStyle($tableStyle);
if (!empty($headers)) {
$headerLengths = array_map(function ($item) {
return strlen($item);
}, $headers);
$wrapper->setMinimumWidths($headerLengths);
}
return $wrapper->wrap($data);
}
/**
* Add our custom table style(s) to the table.
*/
protected static function addCustomTableStyles($table)
{
// The 'consolidation' style is the same as the 'symfony-style-guide'
// style, except it maintains the colored headers used in 'default'.
$consolidationStyle = new TableStyle();
if (method_exists($consolidationStyle, 'setHorizontalBorderChars')) {
$consolidationStyle
->setHorizontalBorderChars('-')
->setVerticalBorderChars(' ')
->setDefaultCrossingChar(' ')
;
} else {
$consolidationStyle
->setHorizontalBorderChar('-')
->setVerticalBorderChar(' ')
->setCrossingChar(' ')
;
}
$table->setStyleDefinition('consolidation', $consolidationStyle);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Validate\ValidDataTypesInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Transformations\TableTransformation;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Tab-separated value formatters
*
* Display the provided structured data in a tab-separated list. Output
* escaping is much lighter, since there is no allowance for altering
* the delimiter.
*/
class TsvFormatter extends CsvFormatter
{
protected function getDefaultFormatterOptions()
{
return [
FormatterOptions::INCLUDE_FIELD_LABELS => false,
];
}
protected function writeOneLine(OutputInterface $output, $data, $options)
{
$output->writeln($this->tsvEscape($data));
}
protected function tsvEscape($data)
{
return implode("\t", array_map(
function ($item) {
return str_replace(["\t", "\n"], ['\t', '\n'], $item);
},
$data
));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
/**
* Var_dump formatter
*
* Run provided data through Symfony VarDumper component.
*/
class VarDumpFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$dumper = new CliDumper();
$cloned_data = (new VarCloner())->cloneVar($data);
if ($output instanceof StreamOutput) {
// When stream output is used the dumper is smart enough to
// determine whether or not to apply colors to the dump.
// @see Symfony\Component\VarDumper\Dumper\CliDumper::supportsColors
$dumper->dump($cloned_data, $output->getStream());
} else {
// @todo Use dumper return value to get output once we stop support
// VarDumper v2.
$stream = fopen('php://memory', 'r+b');
$dumper->dump($cloned_data, $stream);
$output->writeln(stream_get_contents($stream, -1, 0));
fclose($stream);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Var_export formatter
*
* Run provided date thruogh var_export.
*/
class VarExportFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
$output->writeln(var_export($data, true));
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
use Consolidation\OutputFormatters\Validate\ValidDataTypesInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Consolidation\OutputFormatters\StructuredData\TableDataInterface;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface;
/**
* Display a table of data with the Symfony Table class.
*
* This formatter takes data of either the RowsOfFields or
* PropertyList data type. Tables can be rendered with the
* rows running either vertically (the normal orientation) or
* horizontally. By default, associative lists will be displayed
* as two columns, with the key in the first column and the
* value in the second column.
*/
class XmlFormatter implements FormatterInterface, ValidDataTypesInterface
{
use ValidDataTypesTrait;
public function __construct()
{
}
public function validDataTypes()
{
return
[
new \ReflectionClass('\DOMDocument'),
new \ReflectionClass('\ArrayObject'),
];
}
/**
* @inheritdoc
*/
public function validate($structuredData)
{
if ($structuredData instanceof \DOMDocument) {
return $structuredData;
}
if ($structuredData instanceof DomDataInterface) {
return $structuredData->getDomData();
}
if ($structuredData instanceof \ArrayObject) {
return $structuredData->getArrayCopy();
}
if (!is_array($structuredData)) {
throw new IncompatibleDataException(
$this,
$structuredData,
$this->validDataTypes()
);
}
return $structuredData;
}
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $dom, FormatterOptions $options)
{
if (is_array($dom)) {
$schema = $options->getXmlSchema();
$dom = $schema->arrayToXML($dom);
}
$dom->formatOutput = true;
$output->writeln($dom->saveXML());
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Symfony\Component\Yaml\Yaml;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Yaml formatter
*
* Convert an array or ArrayObject into Yaml.
*/
class YamlFormatter implements FormatterInterface
{
/**
* @inheritdoc
*/
public function write(OutputInterface $output, $data, FormatterOptions $options)
{
// Set Yaml\Dumper's default indentation for nested nodes/collections to
// 2 spaces for consistency with Drupal coding standards.
$indent = 2;
// The level where you switch to inline YAML is set to PHP_INT_MAX to
// ensure this does not occur.
$output->writeln(Yaml::dump($data, PHP_INT_MAX, $indent, false, true));
}
}

View File

@@ -0,0 +1,405 @@
<?php
namespace Consolidation\OutputFormatters\Options;
use Symfony\Component\Console\Input\InputInterface;
use Consolidation\OutputFormatters\Transformations\PropertyParser;
use Consolidation\OutputFormatters\StructuredData\Xml\XmlSchema;
use Consolidation\OutputFormatters\StructuredData\Xml\XmlSchemaInterface;
/**
* FormetterOptions holds information that affects the way a formatter
* renders its output.
*
* There are three places where a formatter might get options from:
*
* 1. Configuration associated with the command that produced the output.
* This is passed in to FormatterManager::write() along with the data
* to format. It might originally come from annotations on the command,
* or it might come from another source. Examples include the field labels
* for a table, or the default list of fields to display.
*
* 2. Options specified by the user, e.g. by commandline options.
*
* 3. Default values associated with the formatter itself.
*
* This class caches configuration from sources (1) and (2), and expects
* to be provided the defaults, (3), whenever a value is requested.
*/
class FormatterOptions
{
/** var array */
protected $configurationData = [];
/** var array */
protected $options = [];
/** var InputInterface */
protected $input;
const FORMAT = 'format';
const DEFAULT_FORMAT = 'default-format';
const TABLE_STYLE = 'table-style';
const LIST_ORIENTATION = 'list-orientation';
const FIELDS = 'fields';
const FIELD = 'field';
const INCLUDE_FIELD_LABELS = 'include-field-labels';
const ROW_LABELS = 'row-labels';
const FIELD_LABELS = 'field-labels';
const DEFAULT_FIELDS = 'default-fields';
const DEFAULT_TABLE_FIELDS = 'default-table-fields';
const DEFAULT_STRING_FIELD = 'default-string-field';
const DELIMITER = 'delimiter';
const CSV_ENCLOSURE = 'csv-enclosure';
const CSV_ESCAPE_CHAR = 'csv-escape-char';
const LIST_DELIMITER = 'list-delimiter';
const TERMINAL_WIDTH = 'width';
const METADATA_TEMPLATE = 'metadata-template';
const HUMAN_READABLE = 'human-readable';
/**
* Create a new FormatterOptions with the configuration data and the
* user-specified options for this request.
*
* @see FormatterOptions::setInput()
* @param array $configurationData
* @param array $options
*/
public function __construct($configurationData = [], $options = [])
{
$this->configurationData = $configurationData;
$this->options = $options;
}
/**
* Create a new FormatterOptions object with new configuration data (provided),
* and the same options data as this instance.
*
* @param array $configurationData
* @return FormatterOptions
*/
public function override($configurationData)
{
$override = new self();
$override
->setConfigurationData($configurationData + $this->getConfigurationData())
->setOptions($this->getOptions());
return $override;
}
public function setTableStyle($style)
{
return $this->setConfigurationValue(self::TABLE_STYLE, $style);
}
public function setDelimiter($delimiter)
{
return $this->setConfigurationValue(self::DELIMITER, $delimiter);
}
public function setCsvEnclosure($enclosure)
{
return $this->setConfigurationValue(self::CSV_ENCLOSURE, $enclosure);
}
public function setCsvEscapeChar($escapeChar)
{
return $this->setConfigurationValue(self::CSV_ESCAPE_CHAR, $escapeChar);
}
public function setListDelimiter($listDelimiter)
{
return $this->setConfigurationValue(self::LIST_DELIMITER, $listDelimiter);
}
public function setIncludeFieldLables($includFieldLables)
{
return $this->setConfigurationValue(self::INCLUDE_FIELD_LABELS, $includFieldLables);
}
public function setListOrientation($listOrientation)
{
return $this->setConfigurationValue(self::LIST_ORIENTATION, $listOrientation);
}
public function setRowLabels($rowLabels)
{
return $this->setConfigurationValue(self::ROW_LABELS, $rowLabels);
}
public function setDefaultFields($fields)
{
return $this->setConfigurationValue(self::DEFAULT_FIELDS, $fields);
}
public function setFieldLabels($fieldLabels)
{
return $this->setConfigurationValue(self::FIELD_LABELS, $fieldLabels);
}
public function setDefaultStringField($defaultStringField)
{
return $this->setConfigurationValue(self::DEFAULT_STRING_FIELD, $defaultStringField);
}
public function setWidth($width)
{
return $this->setConfigurationValue(self::TERMINAL_WIDTH, $width);
}
public function setHumanReadable($isHumanReadable = true)
{
return $this->setConfigurationValue(self::HUMAN_READABLE, $isHumanReadable);
}
/**
* Get a formatter option
*
* @param string $key
* @param array $defaults
* @param mixed $default
* @return mixed
*/
public function get($key, $defaults = [], $default = false)
{
$value = $this->fetch($key, $defaults, $default);
return $this->parse($key, $value);
}
/**
* Return the XmlSchema to use with --format=xml for data types that support
* that. This is used when an array needs to be converted into xml.
*
* @return XmlSchema
*/
public function getXmlSchema()
{
return new XmlSchema();
}
/**
* Determine the format that was requested by the caller.
*
* @param array $defaults
* @return string
*/
public function getFormat($defaults = [])
{
return $this->get(self::FORMAT, [], $this->get(self::DEFAULT_FORMAT, $defaults, ''));
}
/**
* Look up a key, and return its raw value.
*
* @param string $key
* @param array $defaults
* @param mixed $default
* @return mixed
*/
protected function fetch($key, $defaults = [], $default = false)
{
$defaults = $this->defaultsForKey($key, $defaults, $default);
$values = $this->fetchRawValues($defaults);
return $values[$key];
}
/**
* Reduce provided defaults to the single item identified by '$key',
* if it exists, or an empty array otherwise.
*
* @param string $key
* @param array $defaults
* @return array
*/
protected function defaultsForKey($key, $defaults, $default = false)
{
if (array_key_exists($key, $defaults)) {
return [$key => $defaults[$key]];
}
return [$key => $default];
}
/**
* Look up all of the items associated with the provided defaults.
*
* @param array $defaults
* @return array
*/
protected function fetchRawValues($defaults = [])
{
return array_merge(
$defaults,
$this->getConfigurationData(),
$this->getOptions(),
$this->getInputOptions($defaults)
);
}
/**
* Given the raw value for a specific key, do any type conversion
* (e.g. from a textual list to an array) needed for the data.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function parse($key, $value)
{
$optionFormat = $this->getOptionFormat($key);
if (!empty($optionFormat) && is_string($value)) {
return $this->$optionFormat($value);
}
return $value;
}
/**
* Convert from a textual list to an array
*
* @param string $value
* @return array
*/
public function parsePropertyList($value)
{
return PropertyParser::parse($value);
}
/**
* Given a specific key, return the class method name of the
* parsing method for data stored under this key.
*
* @param string $key
* @return string
*/
protected function getOptionFormat($key)
{
$propertyFormats = [
self::ROW_LABELS => 'PropertyList',
self::FIELD_LABELS => 'PropertyList',
];
if (array_key_exists($key, $propertyFormats)) {
return "parse{$propertyFormats[$key]}";
}
return '';
}
/**
* Change the configuration data for this formatter options object.
*
* @param array $configurationData
* @return FormatterOptions
*/
public function setConfigurationData($configurationData)
{
$this->configurationData = $configurationData;
return $this;
}
/**
* Change one configuration value for this formatter option.
*
* @param string $key
* @param mixed $value
* @return FormetterOptions
*/
protected function setConfigurationValue($key, $value)
{
$this->configurationData[$key] = $value;
return $this;
}
/**
* Change one configuration value for this formatter option, but only
* if it does not already have a value set.
*
* @param string $key
* @param mixed $value
* @return FormetterOptions
*/
public function setConfigurationDefault($key, $value)
{
if (!array_key_exists($key, $this->configurationData)) {
return $this->setConfigurationValue($key, $value);
}
return $this;
}
/**
* Return a reference to the configuration data for this object.
*
* @return array
*/
public function getConfigurationData()
{
return $this->configurationData;
}
/**
* Set all of the options that were specified by the user for this request.
*
* @param array $options
* @return FormatterOptions
*/
public function setOptions($options)
{
$this->options = $options;
return $this;
}
/**
* Change one option value specified by the user for this request.
*
* @param string $key
* @param mixed $value
* @return FormatterOptions
*/
public function setOption($key, $value)
{
$this->options[$key] = $value;
return $this;
}
/**
* Return a reference to the user-specified options for this request.
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* Provide a Symfony Console InputInterface containing the user-specified
* options for this request.
*
* @param InputInterface $input
* @return type
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
/**
* Return all of the options from the provided $defaults array that
* exist in our InputInterface object.
*
* @param array $defaults
* @return array
*/
public function getInputOptions($defaults)
{
if (!isset($this->input)) {
return [];
}
$options = [];
foreach ($defaults as $key => $value) {
if ($this->input->hasOption($key)) {
$result = $this->input->getOption($key);
if (isset($result)) {
$options[$key] = $this->input->getOption($key);
}
}
}
return $options;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Consolidation\OutputFormatters\Options;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface OverrideOptionsInterface
{
/**
* Allow the formatter to mess with the configuration options before any
* transformations et. al. get underway.
*
* @param mixed $structuredOutput Data to restructure
* @param FormatterOptions $options Formatting options
* @return FormatterOptions
*/
public function overrideOptions($structuredOutput, FormatterOptions $options);
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
/**
* Base class for all list data types.
*/
class AbstractListData extends \ArrayObject implements ListDataInterface
{
public function __construct($data)
{
parent::__construct($data);
}
public function getListData(FormatterOptions $options)
{
return array_keys($this->getArrayCopy());
}
protected function getReorderedFieldLabels($data, $options, $defaults)
{
$reorderer = new ReorderFields();
$fieldLabels = $reorderer->reorder(
$this->getFields($options, $defaults),
$options->get(FormatterOptions::FIELD_LABELS, $defaults),
$data
);
return $fieldLabels;
}
protected function getFields($options, $defaults)
{
$fieldShortcut = $options->get(FormatterOptions::FIELD);
if (!empty($fieldShortcut)) {
return [$fieldShortcut];
}
$result = $options->get(FormatterOptions::FIELDS);
if (!empty($result)) {
return $result;
}
$isHumanReadable = $options->get(FormatterOptions::HUMAN_READABLE);
if ($isHumanReadable) {
$result = $options->get(FormatterOptions::DEFAULT_TABLE_FIELDS);
if (!empty($result)) {
return $result;
}
}
return $options->get(FormatterOptions::DEFAULT_FIELDS, $defaults);
}
/**
* A structured list may provide its own set of default options. These
* will be used in place of the command's default options (from the
* annotations) in instances where the user does not provide the options
* explicitly (on the commandline) or implicitly (via a configuration file).
*
* @return array
*/
protected function defaultOptions()
{
return [
FormatterOptions::FIELDS => [],
FormatterOptions::FIELD_LABELS => [],
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\ListDataInterface;
use Consolidation\OutputFormatters\Transformations\TableTransformation;
/**
* Holds an array where each element of the array is one row,
* and each row contains an associative array where the keys
* are the field names, and the values are the field data.
*
* It is presumed that every row contains the same keys.
*/
abstract class AbstractStructuredList extends AbstractListData implements RestructureInterface, RenderCellCollectionInterface
{
use RenderCellCollectionTrait;
public function __construct($data)
{
parent::__construct($data);
}
abstract public function restructure(FormatterOptions $options);
protected function createTableTransformation($data, $options)
{
$defaults = $this->defaultOptions();
$fieldLabels = $this->getReorderedFieldLabels($data, $options, $defaults);
$tableTransformer = $this->instantiateTableTransformation($data, $fieldLabels, $options->get(FormatterOptions::ROW_LABELS, $defaults));
if ($options->get(FormatterOptions::LIST_ORIENTATION, $defaults)) {
$tableTransformer->setLayout(TableTransformation::LIST_LAYOUT);
}
return $tableTransformer;
}
protected function instantiateTableTransformation($data, $fieldLabels, $rowLabels)
{
return new TableTransformation($data, $fieldLabels, $rowLabels);
}
protected function defaultOptions()
{
return [
FormatterOptions::ROW_LABELS => [],
FormatterOptions::DEFAULT_FIELDS => [],
] + parent::defaultOptions();
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
/**
* Old name for PropertyList class.
*
* @deprecated
*/
class AssociativeList extends PropertyList
{
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
class CallableRenderer implements RenderCellInterface
{
/** @var callable */
protected $renderFunction;
public function __construct(callable $renderFunction)
{
$this->renderFunction = $renderFunction;
}
/**
* {@inheritdoc}
*/
public function renderCell($key, $cellData, FormatterOptions $options, $rowData)
{
return call_user_func($this->renderFunction, $key, $cellData, $options, $rowData);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface ConversionInterface
{
/**
* Allow structured data to be converted -- i.e. from
* RowsOfFields to UnstructuredListData.
*
* @param FormatterOptions $options Formatting options
*/
public function convert(FormatterOptions $options);
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Transformations\UnstructuredDataListTransformation;
/**
* FieldProcessor will do various alterations on field sets.
*/
class FieldProcessor
{
public static function processFieldAliases($fields)
{
if (!is_array($fields)) {
$fields = array_filter(explode(',', $fields));
}
$transformed_fields = [];
foreach ($fields as $field) {
list($machine_name,$label) = explode(' as ', $field) + [$field, preg_replace('#.*\.#', '', $field)];
$transformed_fields[$machine_name] = $label;
}
return $transformed_fields;
}
/**
* Determine whether the data structure has unstructured field access,
* e.g. `a.b.c` or `foo as bar`.
* @param type $fields
* @return type
*/
public static function hasUnstructuredFieldAccess($fields)
{
if (is_array($fields)) {
$fields = implode(',', $fields);
}
return (strpos($fields, ' as ') !== false) || (strpos($fields, '.') !== false);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface;
class HelpDocument implements DomDataInterface
{
/**
* Convert data into a \DomDocument.
*
* @return \DomDocument
*/
public function getDomData()
{
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
/**
* @deprecated Use UnstructuredListData
*/
class ListDataFromKeys extends AbstractListData
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface ListDataInterface
{
/**
* Convert data to a format suitable for use in a list.
* By default, the array values will be used. Implement
* ListDataInterface to use some other criteria (e.g. array keys).
*
* @return array
*/
public function getListData(FormatterOptions $options);
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface MetadataHolderInterface
{
public function getDataKey();
public function setDataKey($key);
public function getMetadataKey();
public function setMetadataKey($key);
public function extractData($data);
public function extractMetadata($data);
public function reconstruct($data, $metadata);
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
/**
* A structured data object may contains some elements that
* are actually metadata. Metadata is not included in the
* output of tabular data formatters (e.g. table, csv), although
* some of these (e.g. table) may render the metadata alongside
* the data. Raw data formatters (e.g. yaml, json) will render
* both the data and the metadata.
*
* There are two possible options for the data format; either the
* data is nested inside some element, and ever other item is
* metadata, or the metadata may be nested inside some element,
* and every other item is the data rows.
*
* Example 1: nested data
*
* [
* 'data' => [ ... rows of field data ... ],
* 'metadata1' => '...',
* 'metadata2' => '...',
* ]
*
* Example 2: nested metadata
*
* [
* 'metadata' => [ ... metadata items ... ],
* 'rowid1' => [ ... ],
* 'rowid2' => [ ... ],
* ]
*
* It is, of course, also possible that both the data and
* the metadata may be nested inside subelements.
*/
trait MetadataHolderTrait
{
protected $dataKey = false;
protected $metadataKey = false;
public function getDataKey()
{
return $this->dataKey;
}
public function setDataKey($key)
{
$this->dataKey = $key;
return $this;
}
public function getMetadataKey()
{
return $this->metadataKey;
}
public function setMetadataKey($key)
{
$this->metadataKey = $key;
return $this;
}
public function extractData($data)
{
if ($this->metadataKey) {
unset($data[$this->metadataKey]);
}
if ($this->dataKey) {
if (!isset($data[$this->dataKey])) {
return [];
}
return $data[$this->dataKey];
}
return $data;
}
public function extractMetadata($data)
{
if (!$this->dataKey && !$this->metadataKey) {
return [];
}
if ($this->dataKey) {
unset($data[$this->dataKey]);
}
if ($this->metadataKey) {
if (!isset($data[$this->metadataKey])) {
return [];
}
return $data[$this->metadataKey];
}
return $data;
}
public function reconstruct($data, $metadata)
{
$reconstructedData = ($this->dataKey) ? [$this->dataKey => $data] : $data;
$reconstructedMetadata = ($this->metadataKey) ? [$this->metadataKey => $metadata] : $metadata;
return $reconstructedData + $reconstructedMetadata;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface MetadataInterface
{
/**
* Return the metadata associated with the structured data (if any)
*/
public function getMetadata();
}

View File

@@ -0,0 +1,137 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Formatters\FormatterAwareInterface;
use Consolidation\OutputFormatters\Formatters\FormatterAwareTrait;
/**
* Create a formatter to add commas to numeric data.
*
* Example:
*
* -------
* Value
* -------
* 2,384
* 143,894
* 23
* 98,538
*
* This formatter may also be re-used for other purposes where right-justified
* data is desired by simply making a subclass. See method comments below.
*
* Usage:
*
* return (new RowsOfFields($data))->addRenderer(
* new NumericCellRenderer($data, ['value'])
* );
*
*/
class NumericCellRenderer implements RenderCellInterface, FormatterAwareInterface
{
use FormatterAwareTrait;
protected $data;
protected $renderedColumns;
protected $widths = [];
/**
* NumericCellRenderer constructor
*/
public function __construct($data, $renderedColumns)
{
$this->data = $data;
$this->renderedColumns = $renderedColumns;
}
/**
* @inheritdoc
*/
public function renderCell($key, $cellData, FormatterOptions $options, $rowData)
{
if (!$this->isRenderedFormat($options) || !$this->isRenderedColumn($key)) {
return $cellData;
}
if ($this->isRenderedData($cellData)) {
$cellData = $this->formatCellData($cellData);
}
return $this->justifyCellData($key, $cellData);
}
/**
* Right-justify the cell data.
*/
protected function justifyCellData($key, $cellData)
{
return str_pad($cellData, $this->columnWidth($key), " ", STR_PAD_LEFT);
}
/**
* Determine if this format is to be formatted.
*/
protected function isRenderedFormat(FormatterOptions $options)
{
return $this->isHumanReadable();
}
/**
* Determine if this is a column that should be formatted.
*/
protected function isRenderedColumn($key)
{
return array_key_exists($key, $this->renderedColumns);
}
/**
* Ignore cell data that should not be formatted.
*/
protected function isRenderedData($cellData)
{
return is_numeric($cellData);
}
/**
* Format the cell data.
*/
protected function formatCellData($cellData)
{
return number_format($this->convertCellDataToString($cellData));
}
/**
* This formatter only works with columns whose columns are strings.
* To use this formatter for another purpose, override this method
* to ensure that the cell data is a string before it is formatted.
*/
protected function convertCellDataToString($cellData)
{
return $cellData;
}
/**
* Get the cached column width for the provided key.
*/
protected function columnWidth($key)
{
if (!isset($this->widths[$key])) {
$this->widths[$key] = $this->calculateColumnWidth($key);
}
return $this->widths[$key];
}
/**
* Using the cached table data, calculate the largest width
* for the data in the table for use when right-justifying.
*/
protected function calculateColumnWidth($key)
{
$width = isset($this->renderedColumns[$key]) ? $this->renderedColumns[$key] : 0;
foreach ($this->data as $row) {
$data = $this->formatCellData($row[$key]);
$width = max(strlen($data), $width);
}
return $width;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
interface OriginalDataInterface
{
/**
* Return the original data for this table. Used by any
* formatter that expects an array.
*/
public function getOriginalData();
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\ListDataInterface;
use Consolidation\OutputFormatters\Transformations\PropertyParser;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
use Consolidation\OutputFormatters\Transformations\TableTransformation;
use Consolidation\OutputFormatters\Transformations\PropertyListTableTransformation;
/**
* Holds an array where each element of the array is one
* key : value pair. The keys must be unique, as is typically
* the case for associative arrays.
*/
class PropertyList extends AbstractStructuredList implements ConversionInterface
{
/**
* @inheritdoc
*/
public function convert(FormatterOptions $options)
{
$defaults = $this->defaultOptions();
$fields = $this->getFields($options, $defaults);
if (FieldProcessor::hasUnstructuredFieldAccess($fields)) {
return new UnstructuredData($this->getArrayCopy());
}
return $this;
}
/**
* Restructure this data for output by converting it into a table
* transformation object.
*
* @param FormatterOptions $options Options that affect output formatting.
* @return Consolidation\OutputFormatters\Transformations\TableTransformation
*/
public function restructure(FormatterOptions $options)
{
$data = [$this->getArrayCopy()];
$options->setConfigurationDefault('list-orientation', true);
$tableTransformer = $this->createTableTransformation($data, $options);
return $tableTransformer;
}
public function getListData(FormatterOptions $options)
{
$data = $this->getArrayCopy();
$defaults = $this->defaultOptions();
$fieldLabels = $this->getReorderedFieldLabels([$data], $options, $defaults);
$result = [];
foreach ($fieldLabels as $id => $label) {
$result[$id] = $data[$id];
}
return $result;
}
protected function defaultOptions()
{
return [
FormatterOptions::LIST_ORIENTATION => true,
] + parent::defaultOptions();
}
protected function instantiateTableTransformation($data, $fieldLabels, $rowLabels)
{
return new PropertyListTableTransformation($data, $fieldLabels, $rowLabels);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Formatters\FormatterAwareInterface;
interface RenderCellCollectionInterface extends RenderCellInterface, FormatterAwareInterface
{
const PRIORITY_FIRST = 'first';
const PRIORITY_NORMAL = 'normal';
const PRIORITY_FALLBACK = 'fallback';
/**
* Add a renderer
*
* @return $this
*/
public function addRenderer(RenderCellInterface $renderer);
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Formatters\FormatterAwareInterface;
use Consolidation\OutputFormatters\Formatters\FormatterAwareTrait;
trait RenderCellCollectionTrait
{
use FormatterAwareTrait;
/** @var RenderCellInterface[] */
protected $rendererList = [
RenderCellCollectionInterface::PRIORITY_FIRST => [],
RenderCellCollectionInterface::PRIORITY_NORMAL => [],
RenderCellCollectionInterface::PRIORITY_FALLBACK => [],
];
/**
* Add a renderer
*
* @return $this
*/
public function addRenderer(RenderCellInterface $renderer, $priority = RenderCellCollectionInterface::PRIORITY_NORMAL)
{
$this->rendererList[$priority][] = $renderer;
return $this;
}
/**
* Add a callable as a renderer
*
* @return $this
*/
public function addRendererFunction(callable $rendererFn, $priority = RenderCellCollectionInterface::PRIORITY_NORMAL)
{
$renderer = new CallableRenderer($rendererFn);
return $this->addRenderer($renderer, $priority);
}
/**
* {@inheritdoc}
*/
public function renderCell($key, $cellData, FormatterOptions $options, $rowData)
{
$flattenedRendererList = array_reduce(
$this->rendererList,
function ($carry, $item) {
return array_merge($carry, $item);
},
[]
);
foreach ($flattenedRendererList as $renderer) {
if ($renderer instanceof FormatterAwareInterface) {
$renderer->setFormatter($this->getFormatter());
}
$cellData = $renderer->renderCell($key, $cellData, $options, $rowData);
}
return $cellData;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface RenderCellInterface
{
/**
* Convert the contents of one table cell into a string,
* so that it may be placed in the table. Renderer should
* return the $cellData passed to it if it does not wish to
* process it.
*
* @param string $key Identifier of the cell being rendered
* @param mixed $cellData The data to render
* @param FormatterOptions $options The formatting options
* @param array $rowData The rest of the row data
*
* @return mixed
*/
public function renderCell($key, $cellData, FormatterOptions $options, $rowData);
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface RestructureInterface
{
/**
* Allow structured data to be restructured -- i.e. to select fields
* to show, reorder fields, etc.
*
* @param FormatterOptions $options Formatting options
*/
public function restructure(FormatterOptions $options);
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
/**
* Holds an array where each element of the array is one row,
* and each row contains an associative array where the keys
* are the field names, and the values are the field data.
*
* It is presumed that every row contains the same keys.
*/
class RowsOfFields extends AbstractStructuredList implements ConversionInterface
{
/**
* @inheritdoc
*/
public function convert(FormatterOptions $options)
{
$defaults = $this->defaultOptions();
$fields = $this->getFields($options, $defaults);
if (FieldProcessor::hasUnstructuredFieldAccess($fields)) {
return new UnstructuredListData($this->getArrayCopy());
}
return $this;
}
/**
* Restructure this data for output by converting it into a table
* transformation object.
*
* @param FormatterOptions $options Options that affect output formatting.
* @return Consolidation\OutputFormatters\Transformations\TableTransformation
*/
public function restructure(FormatterOptions $options)
{
$data = $this->getArrayCopy();
return $this->createTableTransformation($data, $options);
}
public function getListData(FormatterOptions $options)
{
return array_keys($this->getArrayCopy());
}
protected function defaultOptions()
{
return [
FormatterOptions::LIST_ORIENTATION => false,
] + parent::defaultOptions();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
/**
* A RowsOfFields data structure that also contains metadata.
* @see MetadataHolderTrait
*/
class RowsOfFieldsWithMetadata extends RowsOfFields implements MetadataInterface, MetadataHolderInterface
{
use MetadataHolderTrait;
/**
* Restructure this data for output by converting it into a table
* transformation object. First, though, remove any metadata items.
*
* @param FormatterOptions $options Options that affect output formatting.
* @return Consolidation\OutputFormatters\Transformations\TableTransformation
*/
public function restructure(FormatterOptions $options)
{
$originalData = $this->getArrayCopy();
$data = $this->extractData($originalData);
$tableTranformer = $this->createTableTransformation($data, $options);
$tableTranformer->setOriginalData($this);
return $tableTranformer;
}
public function getMetadata()
{
return $this->extractMetadata($this->getArrayCopy());
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
interface TableDataInterface
{
/**
* Convert structured data into a form suitable for use
* by the table formatter.
*
* @param boolean $includeRowKey Add a field containing the
* key from each row.
*
* @return array
*/
public function getTableData($includeRowKey = false);
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Transformations\UnstructuredDataTransformation;
/**
* Represents aribtrary unstructured array data where the
* data to display in --list format comes from the array keys.
*
* Unstructured list data can have variable keys in every rown (unlike
* RowsOfFields, which expects uniform rows), and the data elements may
* themselves be deep arrays.
*/
class UnstructuredData extends AbstractListData implements UnstructuredInterface, RestructureInterface
{
public function __construct($data)
{
parent::__construct($data);
}
public function restructure(FormatterOptions $options)
{
$defaults = $this->defaultOptions();
$fields = $this->getFields($options, $defaults);
return new UnstructuredDataTransformation($this->getArrayCopy(), FieldProcessor::processFieldAliases($fields));
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
/**
* UnstructuredInterface is a marker interface that indicates that the
* data type is unstructured, and has no default conversion to a string.
* Unstructured data supports the `string` format only if it also implements
* StringTransformationInterface.
*/
interface UnstructuredInterface
{
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Transformations\UnstructuredDataListTransformation;
/**
* Represents aribtrary unstructured array data where the
* data to display in --list format comes from the array keys.
*
* Unstructured list data can have variable keys in every rown (unlike
* RowsOfFields, which expects uniform rows), and the data elements may
* themselves be deep arrays.
*/
class UnstructuredListData extends AbstractListData implements UnstructuredInterface, RestructureInterface
{
public function __construct($data)
{
parent::__construct($data);
}
public function restructure(FormatterOptions $options)
{
$defaults = $this->defaultOptions();
$fields = $this->getFields($options, $defaults);
return new UnstructuredDataListTransformation($this->getArrayCopy(), FieldProcessor::processFieldAliases($fields));
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData\Xml;
interface DomDataInterface
{
/**
* Convert data into a \DomDocument.
*
* @return \DomDocument
*/
public function getDomData();
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData\Xml;
class XmlSchema implements XmlSchemaInterface
{
protected $elementList;
public function __construct($elementList = [])
{
$defaultElementList =
[
'*' => ['description'],
];
$this->elementList = array_merge_recursive($elementList, $defaultElementList);
}
public function arrayToXML($structuredData)
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$topLevelElement = $this->getTopLevelElementName($structuredData);
$this->addXmlData($dom, $dom, $topLevelElement, $structuredData);
return $dom;
}
protected function addXmlData(\DOMDocument $dom, $xmlParent, $elementName, $structuredData)
{
$element = $dom->createElement($elementName);
$xmlParent->appendChild($element);
if (is_string($structuredData)) {
$element->appendChild($dom->createTextNode($structuredData));
return;
}
$this->addXmlChildren($dom, $element, $elementName, $structuredData);
}
protected function addXmlChildren(\DOMDocument $dom, $xmlParent, $elementName, $structuredData)
{
foreach ($structuredData as $key => $value) {
$this->addXmlDataOrAttribute($dom, $xmlParent, $elementName, $key, $value);
}
}
protected function addXmlDataOrAttribute(\DOMDocument $dom, $xmlParent, $elementName, $key, $value)
{
$childElementName = $this->getDefaultElementName($elementName);
$elementName = $this->determineElementName($key, $childElementName, $value);
if (($elementName != $childElementName) && $this->isAttribute($elementName, $key, $value)) {
$xmlParent->setAttribute($key, $value);
return;
}
$this->addXmlData($dom, $xmlParent, $elementName, $value);
}
protected function determineElementName($key, $childElementName, $value)
{
if (is_numeric($key)) {
return $childElementName;
}
if (is_object($value)) {
$value = (array)$value;
}
if (!is_array($value)) {
return $key;
}
if (array_key_exists('id', $value) && ($value['id'] == $key)) {
return $childElementName;
}
if (array_key_exists('name', $value) && ($value['name'] == $key)) {
return $childElementName;
}
return $key;
}
protected function getTopLevelElementName($structuredData)
{
return 'document';
}
protected function getDefaultElementName($parentElementName)
{
$singularName = $this->singularForm($parentElementName);
if (isset($singularName)) {
return $singularName;
}
return 'item';
}
protected function isAttribute($parentElementName, $elementName, $value)
{
if (!is_string($value)) {
return false;
}
return !$this->inElementList($parentElementName, $elementName) && !$this->inElementList('*', $elementName);
}
protected function inElementList($parentElementName, $elementName)
{
if (!array_key_exists($parentElementName, $this->elementList)) {
return false;
}
return in_array($elementName, $this->elementList[$parentElementName]);
}
protected function singularForm($name)
{
if (substr($name, strlen($name) - 1) == "s") {
return substr($name, 0, strlen($name) - 1);
}
}
protected function isAssoc($data)
{
return array_keys($data) == range(0, count($data));
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData\Xml;
/**
* When using arrays, we could represent XML data in a number of
* different ways.
*
* For example, given the following XML data strucutre:
*
* <document id="1" name="doc">
* <foobars>
* <foobar id="123">
* <name>blah</name>
* <widgets>
* <widget>
* <foo>a</foo>
* <bar>b</bar>
* <baz>c</baz>
* </widget>
* </widgets>
* </foobar>
* </foobars>
* </document>
*
* This could be:
*
* [
* 'id' => 1,
* 'name' => 'doc',
* 'foobars' =>
* [
* [
* 'id' => '123',
* 'name' => 'blah',
* 'widgets' =>
* [
* [
* 'foo' => 'a',
* 'bar' => 'b',
* 'baz' => 'c',
* ]
* ],
* ],
* ]
* ]
*
* The challenge is more in going from an array back to the more
* structured xml format. Note that any given key => string mapping
* could represent either an attribute, or a simple XML element
* containing only a string value. In general, we do *not* want to add
* extra layers of nesting in the data structure to disambiguate between
* these kinds of data, as we want the source data to render cleanly
* into other formats, e.g. yaml, json, et. al., and we do not want to
* force every data provider to have to consider the optimal xml schema
* for their data.
*
* Our strategy, therefore, is to expect clients that wish to provide
* a very specific xml representation to return a DOMDocument, and,
* for other data structures where xml is a secondary concern, then we
* will use some default heuristics to convert from arrays to xml.
*/
interface XmlSchemaInterface
{
/**
* Convert data to a format suitable for use in a list.
* By default, the array values will be used. Implement
* ListDataInterface to use some other criteria (e.g. array keys).
*
* @return \DOMDocument
*/
public function arrayToXml($structuredData);
}

View File

@@ -0,0 +1,237 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface;
use Consolidation\OutputFormatters\StructuredData\Xml\XmlSchema;
/**
* Simplify a DOMDocument to an array.
*/
class DomToArraySimplifier implements SimplifyToArrayInterface
{
public function __construct()
{
}
/**
* @param ReflectionClass $dataType
*/
public function canSimplify(\ReflectionClass $dataType)
{
return
$dataType->isSubclassOf('\Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface') ||
$dataType->isSubclassOf('DOMDocument') ||
($dataType->getName() == 'DOMDocument');
}
public function simplifyToArray($structuredData, FormatterOptions $options)
{
if ($structuredData instanceof DomDataInterface) {
$structuredData = $structuredData->getDomData();
}
if ($structuredData instanceof \DOMDocument) {
// $schema = $options->getXmlSchema();
$simplified = $this->elementToArray($structuredData);
$structuredData = array_shift($simplified);
}
return $structuredData;
}
/**
* Recursively convert the provided DOM element into a php array.
*
* @param \DOMNode $element
* @return array
*/
protected function elementToArray(\DOMNode $element)
{
if ($element->nodeType == XML_TEXT_NODE) {
return $element->nodeValue;
}
$attributes = $this->getNodeAttributes($element);
$children = $this->getNodeChildren($element);
return array_merge($attributes, $children);
}
/**
* Get all of the attributes of the provided element.
*
* @param \DOMNode $element
* @return array
*/
protected function getNodeAttributes($element)
{
if (empty($element->attributes)) {
return [];
}
$attributes = [];
foreach ($element->attributes as $key => $attribute) {
$attributes[$key] = $attribute->nodeValue;
}
return $attributes;
}
/**
* Get all of the children of the provided element, with simplification.
*
* @param \DOMNode $element
* @return array
*/
protected function getNodeChildren($element)
{
if (empty($element->childNodes)) {
return [];
}
$uniformChildrenName = $this->hasUniformChildren($element);
// Check for plurals.
if (in_array($element->nodeName, ["{$uniformChildrenName}s", "{$uniformChildrenName}es"])) {
$result = $this->getUniformChildren($element->nodeName, $element);
} else {
$result = $this->getUniqueChildren($element->nodeName, $element);
}
return array_filter($result);
}
/**
* Get the data from the children of the provided node in preliminary
* form.
*
* @param \DOMNode $element
* @return array
*/
protected function getNodeChildrenData($element)
{
$children = [];
foreach ($element->childNodes as $key => $value) {
$children[$key] = $this->elementToArray($value);
}
return $children;
}
/**
* Determine whether the children of the provided element are uniform.
* @see getUniformChildren(), below.
*
* @param \DOMNode $element
* @return boolean
*/
protected function hasUniformChildren($element)
{
$last = false;
foreach ($element->childNodes as $key => $value) {
$name = $value->nodeName;
if (!$name) {
return false;
}
if ($last && ($name != $last)) {
return false;
}
$last = $name;
}
return $last;
}
/**
* Convert the children of the provided DOM element into an array.
* Here, 'uniform' means that all of the element names of the children
* are identical, and further, the element name of the parent is the
* plural form of the child names. When the children are uniform in
* this way, then the parent element name will be used as the key to
* store the children in, and the child list will be returned as a
* simple list with their (duplicate) element names omitted.
*
* @param string $parentKey
* @param \DOMNode $element
* @return array
*/
protected function getUniformChildren($parentKey, $element)
{
$children = $this->getNodeChildrenData($element);
$simplifiedChildren = [];
foreach ($children as $key => $value) {
if ($this->valueCanBeSimplified($value)) {
$value = array_shift($value);
}
$id = $this->getIdOfValue($value);
if ($id) {
$simplifiedChildren[$parentKey][$id] = $value;
} else {
$simplifiedChildren[$parentKey][] = $value;
}
}
return $simplifiedChildren;
}
/**
* Determine whether the provided value has additional unnecessary
* nesting. {"color": "red"} is converted to "red". No other
* simplification is done.
*
* @param \DOMNode $value
* @return boolean
*/
protected function valueCanBeSimplified($value)
{
if (!is_array($value)) {
return false;
}
if (count($value) != 1) {
return false;
}
$data = array_shift($value);
return is_string($data);
}
/**
* If the object has an 'id' or 'name' element, then use that
* as the array key when storing this value in its parent.
* @param mixed $value
* @return string
*/
protected function getIdOfValue($value)
{
if (!is_array($value)) {
return false;
}
if (array_key_exists('id', $value)) {
return trim($value['id'], '-');
}
if (array_key_exists('name', $value)) {
return trim($value['name'], '-');
}
}
/**
* Convert the children of the provided DOM element into an array.
* Here, 'unique' means that all of the element names of the children are
* different. Since the element names will become the key of the
* associative array that is returned, so duplicates are not supported.
* If there are any duplicates, then an exception will be thrown.
*
* @param string $parentKey
* @param \DOMNode $element
* @return array
*/
protected function getUniqueChildren($parentKey, $element)
{
$children = $this->getNodeChildrenData($element);
if ((count($children) == 1) && (is_string($children[0]))) {
return [$element->nodeName => $children[0]];
}
$simplifiedChildren = [];
foreach ($children as $key => $value) {
if (is_numeric($key) && is_array($value) && (count($value) == 1)) {
$valueKeys = array_keys($value);
$key = $valueKeys[0];
$value = array_shift($value);
}
if (array_key_exists($key, $simplifiedChildren)) {
throw new \Exception("Cannot convert data from a DOM document to an array, because <$key> appears more than once, and is not wrapped in a <{$key}s> element.");
}
$simplifiedChildren[$key] = $value;
}
return $simplifiedChildren;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface OverrideRestructureInterface
{
/**
* Select data to use directly from the structured output,
* before the restructure operation has been executed.
*
* @param mixed $structuredOutput Data to restructure
* @param FormatterOptions $options Formatting options
* @return mixed
*/
public function overrideRestructure($structuredOutput, FormatterOptions $options);
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
class PropertyListTableTransformation extends TableTransformation
{
public function getOriginalData()
{
$data = $this->getArrayCopy();
return $data[0];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
/**
* Transform a string of properties into a PHP associative array.
*
* Input:
*
* one: red
* two: white
* three: blue
*
* Output:
*
* [
* 'one' => 'red',
* 'two' => 'white',
* 'three' => 'blue',
* ]
*/
class PropertyParser
{
public static function parse($data)
{
if (!is_string($data)) {
return $data;
}
$result = [];
$lines = explode("\n", $data);
foreach ($lines as $line) {
list($key, $value) = explode(':', trim($line), 2) + ['', ''];
if (!empty($key) && !empty($value)) {
$result[$key] = trim($value);
}
}
return $result;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Symfony\Component\Finder\Glob;
use Consolidation\OutputFormatters\Exception\UnknownFieldException;
/**
* Reorder the field labels based on the user-selected fields
* to display.
*/
class ReorderFields
{
/**
* Given a simple list of user-supplied field keys or field labels,
* return a reordered version of the field labels matching the
* user selection.
*
* @param string|array $fields The user-selected fields
* @param array $fieldLabels An associative array mapping the field
* key to the field label
* @param array $data The data that will be rendered.
*
* @return array
*/
public function reorder($fields, $fieldLabels, $data)
{
$firstRow = reset($data);
if (!$firstRow) {
$firstRow = $fieldLabels;
}
if (empty($fieldLabels) && !empty($data)) {
$fieldLabels = array_combine(array_keys($firstRow), array_map('ucfirst', array_keys($firstRow)));
}
$fields = $this->getSelectedFieldKeys($fields, $fieldLabels);
if (empty($fields)) {
return array_intersect_key($fieldLabels, $firstRow);
}
return $this->reorderFieldLabels($fields, $fieldLabels, $data);
}
protected function reorderFieldLabels($fields, $fieldLabels, $data)
{
$result = [];
$firstRow = reset($data);
if (!$firstRow) {
$firstRow = $fieldLabels;
}
foreach ($fields as $field) {
if (array_key_exists($field, $firstRow)) {
if (array_key_exists($field, $fieldLabels)) {
$result[$field] = $fieldLabels[$field];
}
}
}
return $result;
}
protected function getSelectedFieldKeys($fields, $fieldLabels)
{
if (empty($fieldLabels)) {
return [];
}
if (is_string($fields)) {
$fields = explode(',', $fields);
}
$selectedFields = [];
foreach ($fields as $field) {
$matchedFields = $this->matchFieldInLabelMap($field, $fieldLabels);
if (empty($matchedFields)) {
throw new UnknownFieldException($field);
}
$selectedFields = array_merge($selectedFields, $matchedFields);
}
return $selectedFields;
}
protected function matchFieldInLabelMap($field, $fieldLabels)
{
$fieldRegex = $this->convertToRegex($field);
return
array_filter(
array_keys($fieldLabels),
function ($key) use ($fieldRegex, $fieldLabels) {
$value = $fieldLabels[$key];
return preg_match($fieldRegex, $value) || preg_match($fieldRegex, $key);
}
);
}
/**
* Convert the provided string into a regex suitable for use in
* preg_match.
*
* Matching occurs in the same way as the Symfony Finder component:
* http://symfony.com/doc/current/components/finder.html#file-name
*/
protected function convertToRegex($str)
{
return $this->isRegex($str) ? $str : Glob::toRegex($str);
}
/**
* Checks whether the string is a regex. This function is copied from
* MultiplePcreFilterIterator in the Symfony Finder component.
*
* @param string $str
*
* @return bool Whether the given string is a regex
*/
protected function isRegex($str)
{
if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) {
$start = substr($m[1], 0, 1);
$end = substr($m[1], -1);
if ($start === $end) {
return !preg_match('/[*?[:alnum:] \\\\]/', $start);
}
foreach (array(array('{', '}'), array('(', ')'), array('[', ']'), array('<', '>')) as $delimiters) {
if ($start === $delimiters[0] && $end === $delimiters[1]) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface SimplifyToArrayInterface
{
/**
* Convert structured data into a generic array, usable by generic
* array-based formatters. Objects that implement this interface may
* be attached to the FormatterManager, and will be used on any data
* structure that needs to be simplified into an array. An array
* simplifier should take no action other than to return its input data
* if it cannot simplify the provided data into an array.
*
* @param mixed $structuredOutput The data to simplify to an array.
*
* @return array
*/
public function simplifyToArray($structuredOutput, FormatterOptions $options);
/**
* Indicate whether or not the given data type can be simplified to an array
*/
public function canSimplify(\ReflectionClass $structuredOutput);
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
interface StringTransformationInterface
{
/**
* simplifyToString is called by the string formatter to convert
* structured data to a simple string.
*/
public function simplifyToString(FormatterOptions $options);
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\StructuredData\TableDataInterface;
use Consolidation\OutputFormatters\StructuredData\OriginalDataInterface;
use Consolidation\OutputFormatters\StructuredData\MetadataHolderInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Formatters\TsvFormatter;
use Symfony\Component\Console\Output\BufferedOutput;
class TableTransformation extends \ArrayObject implements TableDataInterface, StringTransformationInterface, OriginalDataInterface
{
protected $headers;
protected $rowLabels;
protected $layout;
/** @var MetadataHolderInterface */
protected $originalData;
const TABLE_LAYOUT = 'table';
const LIST_LAYOUT = 'list';
public function __construct($data, $fieldLabels, $rowLabels = [])
{
$this->headers = $fieldLabels;
$this->rowLabels = $rowLabels;
$rows = static::transformRows($data, $fieldLabels);
$this->layout = self::TABLE_LAYOUT;
parent::__construct($rows);
}
public function setLayout($layout)
{
$this->layout = $layout;
}
public function getLayout()
{
return $this->layout;
}
public function isList()
{
return $this->layout == self::LIST_LAYOUT;
}
/**
* @inheritdoc
*/
public function simplifyToString(FormatterOptions $options)
{
$alternateFormatter = new TsvFormatter();
$output = new BufferedOutput();
try {
$data = $alternateFormatter->validate($this->getArrayCopy());
$alternateFormatter->write($output, $this->getArrayCopy(), $options);
} catch (\Exception $e) {
}
return $output->fetch();
}
protected static function transformRows($data, $fieldLabels)
{
$rows = [];
foreach ($data as $rowid => $row) {
$rows[$rowid] = static::transformRow($row, $fieldLabels);
}
return $rows;
}
protected static function transformRow($row, $fieldLabels)
{
$result = [];
foreach ($fieldLabels as $key => $label) {
$result[$key] = array_key_exists($key, $row) ? $row[$key] : '';
}
return $result;
}
public function getHeaders()
{
return $this->headers;
}
public function getHeader($key)
{
if (array_key_exists($key, $this->headers)) {
return $this->headers[$key];
}
return $key;
}
public function getRowLabels()
{
return $this->rowLabels;
}
public function getRowLabel($rowid)
{
if (array_key_exists($rowid, $this->rowLabels)) {
return $this->rowLabels[$rowid];
}
return $rowid;
}
public function getOriginalData()
{
if (isset($this->originalData)) {
return $this->originalData->reconstruct($this->getArrayCopy(), $this->originalData->getMetadata());
}
return $this->getArrayCopy();
}
public function setOriginalData(MetadataHolderInterface $data)
{
$this->originalData = $data;
}
public function getTableData($includeRowKey = false)
{
$data = $this->getArrayCopy();
if ($this->isList()) {
$data = $this->convertTableToList();
}
if ($includeRowKey) {
$data = $this->getRowDataWithKey($data);
}
return $data;
}
protected function convertTableToList()
{
$result = [];
foreach ($this as $row) {
foreach ($row as $key => $value) {
$result[$key][] = $value;
}
}
return $result;
}
protected function getRowDataWithKey($data)
{
$result = [];
$i = 0;
foreach ($data as $key => $row) {
array_unshift($row, $this->getHeader($key));
$i++;
$result[$key] = $row;
}
return $result;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Dflydev\DotAccessData\Data;
use Dflydev\DotAccessData\Exception\MissingPathException;
class UnstructuredDataFieldAccessor
{
protected $data;
public function __construct($data)
{
$this->data = $data;
}
public function get($fields)
{
$data = new Data($this->data);
$result = new Data();
foreach ($fields as $key => $label) {
$item = null;
try {
$item = $data->get($key);
} catch (MissingPathException $e) {
}
if (isset($item)) {
if ($label == '.') {
if (!is_array($item)) {
return $item;
}
foreach ($item as $key => $value) {
$result->set($key, $value);
}
} else {
$result->set($label, $data->get($key));
}
}
}
return $result->export();
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
class UnstructuredDataListTransformation extends \ArrayObject implements StringTransformationInterface
{
public function __construct($data, $fields)
{
$this->originalData = $data;
$rows = static::transformRows($data, $fields);
parent::__construct($rows);
}
protected static function transformRows($data, $fields)
{
$rows = [];
foreach ($data as $rowid => $row) {
$rows[$rowid] = UnstructuredDataTransformation::transformRow($row, $fields);
}
return $rows;
}
public function simplifyToString(FormatterOptions $options)
{
$result = '';
$iterator = $this->getIterator();
while ($iterator->valid()) {
$simplifiedRow = UnstructuredDataTransformation::simplifyRow($iterator->current());
if (isset($simplifiedRow)) {
$result .= "$simplifiedRow\n";
}
$iterator->next();
}
return $result;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Options\FormatterOptions;
class UnstructuredDataTransformation extends \ArrayObject implements StringTransformationInterface
{
protected $originalData;
public function __construct($data, $fields)
{
$this->originalData = $data;
$rows = static::transformRow($data, $fields);
parent::__construct($rows);
}
public function simplifyToString(FormatterOptions $options)
{
return static::simplifyRow($this->getArrayCopy());
}
public static function transformRow($row, $fields)
{
if (empty($fields)) {
return $row;
}
$fieldAccessor = new UnstructuredDataFieldAccessor($row);
return $fieldAccessor->get($fields);
}
public static function simplifyRow($row)
{
if (is_string($row)) {
return $row;
}
if (static::isSimpleArray($row)) {
return implode("\n", $row);
}
// No good way to simplify - just dump a json fragment
return json_encode($row);
}
protected static function isSimpleArray($row)
{
foreach ($row as $item) {
if (!is_string($item)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\Transformations\Wrap\CalculateWidths;
use Consolidation\OutputFormatters\Transformations\Wrap\ColumnWidths;
use Symfony\Component\Console\Helper\TableStyle;
class WordWrapper
{
protected $width;
protected $minimumWidths;
// For now, hardcode these to match what the Symfony Table helper does.
// Note that these might actually need to be adjusted depending on the
// table style.
protected $extraPaddingAtBeginningOfLine = 0;
protected $extraPaddingAtEndOfLine = 0;
protected $paddingInEachCell = 3;
public function __construct($width)
{
$this->width = $width;
$this->minimumWidths = new ColumnWidths();
}
/**
* Calculate our padding widths from the specified table style.
* @param TableStyle $style
*/
public function setPaddingFromStyle(TableStyle $style)
{
if (method_exists($style, 'getBorderChars')) {
return $this->setPaddingFromSymfony5Style($style);
}
$verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar()));
$paddingLen = strlen($style->getPaddingChar());
$this->extraPaddingAtBeginningOfLine = 0;
$this->extraPaddingAtEndOfLine = $verticalBorderLen;
$this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1;
}
/**
* Calculate our padding widths from the specified table style.
* @param TableStyle $style
*/
public function setPaddingFromSymfony5Style(TableStyle $style)
{
$borderChars = $style->getBorderChars();
$verticalBorderChar = $borderChars[1];
$verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $verticalBorderChar));
$paddingLen = strlen($style->getPaddingChar());
$this->extraPaddingAtBeginningOfLine = 0;
$this->extraPaddingAtEndOfLine = $verticalBorderLen;
$this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1;
}
/**
* If columns have minimum widths, then set them here.
* @param array $minimumWidths
*/
public function setMinimumWidths($minimumWidths)
{
$this->minimumWidths = new ColumnWidths($minimumWidths);
}
/**
* Set the minimum width of just one column
*/
public function minimumWidth($colkey, $width)
{
$this->minimumWidths->setWidth($colkey, $width);
}
/**
* Wrap the cells in each part of the provided data table
* @param array $rows
* @return array
*/
public function wrap($rows, $widths = [])
{
$auto_widths = $this->calculateWidths($rows, $widths);
// If no widths were provided, then disable wrapping
if ($auto_widths->isEmpty()) {
return $rows;
}
// Do wordwrap on all cells.
$newrows = array();
foreach ($rows as $rowkey => $row) {
foreach ($row as $colkey => $cell) {
$newrows[$rowkey][$colkey] = $this->wrapCell($cell, $auto_widths->width($colkey));
}
}
return $newrows;
}
/**
* Determine what widths we'll use for wrapping.
*/
protected function calculateWidths($rows, $widths = [])
{
// Widths must be provided in some form or another, or we won't wrap.
if (empty($widths) && !$this->width) {
return new ColumnWidths();
}
// Technically, `$widths`, if provided here, should be used
// as the exact widths to wrap to. For now we'll just treat
// these as minimum widths
$minimumWidths = $this->minimumWidths->combine(new ColumnWidths($widths));
$calculator = new CalculateWidths();
$dataCellWidths = $calculator->calculateLongestCell($rows);
$availableWidth = $this->width - $dataCellWidths->paddingSpace($this->paddingInEachCell, $this->extraPaddingAtEndOfLine, $this->extraPaddingAtBeginningOfLine);
$this->minimumWidths->adjustMinimumWidths($availableWidth, $dataCellWidths);
return $calculator->calculate($availableWidth, $dataCellWidths, $minimumWidths);
}
/**
* Wrap one cell. Guard against modifying non-strings and
* then call through to wordwrap().
*
* @param mixed $cell
* @param string $cellWidth
* @return mixed
*/
protected function wrapCell($cell, $cellWidth)
{
if (!is_string($cell)) {
return $cell;
}
return wordwrap($cell, $cellWidth, "\n", true);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Consolidation\OutputFormatters\Transformations\Wrap;
use Symfony\Component\Console\Helper\TableStyle;
/**
* Calculate column widths for table cells.
*
* Influenced by Drush and webmozart/console.
*/
class CalculateWidths
{
public function __construct()
{
}
/**
* Given the total amount of available space, and the width of
* the columns to place, calculate the optimum column widths to use.
*/
public function calculate($availableWidth, ColumnWidths $dataWidths, ColumnWidths $minimumWidths)
{
// First, check to see if all columns will fit at their full widths.
// If so, do no further calculations. (This may be redundant with
// the short column width calculation.)
if ($dataWidths->totalWidth() <= $availableWidth) {
return $dataWidths->enforceMinimums($minimumWidths);
}
// Get the short columns first. If there are none, then distribute all
// of the available width among the remaining columns.
$shortColWidths = $this->getShortColumns($availableWidth, $dataWidths, $minimumWidths);
if ($shortColWidths->isEmpty()) {
return $this->distributeLongColumns($availableWidth, $dataWidths, $minimumWidths);
}
// If some short columns were removed, then account for the length
// of the removed columns and make a recursive call (since the average
// width may be higher now, if the removed columns were shorter in
// length than the previous average).
$availableWidth -= $shortColWidths->totalWidth();
$remainingWidths = $dataWidths->removeColumns($shortColWidths->keys());
$remainingColWidths = $this->calculate($availableWidth, $remainingWidths, $minimumWidths);
return $shortColWidths->combine($remainingColWidths);
}
/**
* Calculate the longest cell data from any row of each of the cells.
*/
public function calculateLongestCell($rows)
{
return $this->calculateColumnWidths(
$rows,
function ($cell) {
return strlen($cell);
}
);
}
/**
* Calculate the longest word and longest line in the provided data.
*/
public function calculateLongestWord($rows)
{
return $this->calculateColumnWidths(
$rows,
function ($cell) {
return static::longestWordLength($cell);
}
);
}
protected function calculateColumnWidths($rows, callable $fn)
{
$widths = [];
// Examine each row and find the longest line length and longest
// word in each column.
foreach ($rows as $rowkey => $row) {
foreach ($row as $colkey => $cell) {
$value = $fn($cell);
if ((!isset($widths[$colkey]) || ($widths[$colkey] < $value))) {
$widths[$colkey] = $value;
}
}
}
return new ColumnWidths($widths);
}
/**
* Return all of the columns whose longest line length is less than or
* equal to the average width.
*/
public function getShortColumns($availableWidth, ColumnWidths $dataWidths, ColumnWidths $minimumWidths)
{
$averageWidth = $dataWidths->averageWidth($availableWidth);
$shortColWidths = $dataWidths->findShortColumns($averageWidth);
return $shortColWidths->enforceMinimums($minimumWidths);
}
/**
* Distribute the remainig space among the columns that were not
* included in the list of "short" columns.
*/
public function distributeLongColumns($availableWidth, ColumnWidths $dataWidths, ColumnWidths $minimumWidths)
{
// First distribute the remainder without regard to the minimum widths.
$result = $dataWidths->distribute($availableWidth);
// Find columns that are shorter than their minimum width.
$undersized = $result->findUndersizedColumns($minimumWidths);
// Nothing too small? Great, we're done!
if ($undersized->isEmpty()) {
return $result;
}
// Take out the columns that are too small and redistribute the rest.
$availableWidth -= $undersized->totalWidth();
$remaining = $dataWidths->removeColumns($undersized->keys());
$distributeRemaining = $this->distributeLongColumns($availableWidth, $remaining, $minimumWidths);
return $undersized->combine($distributeRemaining);
}
/**
* Return the length of the longest word in the string.
* @param string $str
* @return int
*/
protected static function longestWordLength($str)
{
$words = preg_split('#[ /-]#', $str);
$lengths = array_map(function ($s) {
return strlen($s);
}, $words);
return max($lengths);
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace Consolidation\OutputFormatters\Transformations\Wrap;
use Symfony\Component\Console\Helper\TableStyle;
/**
* Calculate the width of data in table cells in preparation for word wrapping.
*/
class ColumnWidths
{
protected $widths;
public function __construct($widths = [])
{
$this->widths = $widths;
}
public function paddingSpace(
$paddingInEachCell,
$extraPaddingAtEndOfLine = 0,
$extraPaddingAtBeginningOfLine = 0
) {
return ($extraPaddingAtBeginningOfLine + $extraPaddingAtEndOfLine + (count($this->widths) * $paddingInEachCell));
}
/**
* Find all of the columns that are shorter than the specified threshold.
*/
public function findShortColumns($thresholdWidth)
{
$thresholdWidths = array_fill_keys(array_keys($this->widths), $thresholdWidth);
return $this->findColumnsUnderThreshold($thresholdWidths);
}
/**
* Find all of the columns that are shorter than the corresponding minimum widths.
*/
public function findUndersizedColumns($minimumWidths)
{
return $this->findColumnsUnderThreshold($minimumWidths->widths());
}
protected function findColumnsUnderThreshold(array $thresholdWidths)
{
$shortColWidths = [];
foreach ($this->widths as $key => $maxLength) {
if (isset($thresholdWidths[$key]) && ($maxLength <= $thresholdWidths[$key])) {
$shortColWidths[$key] = $maxLength;
}
}
return new ColumnWidths($shortColWidths);
}
/**
* If the widths specified by this object do not fit within the
* provided avaiable width, then reduce them all proportionally.
*/
public function adjustMinimumWidths($availableWidth, $dataCellWidths)
{
$result = $this->selectColumns($dataCellWidths->keys());
if ($result->isEmpty()) {
return $result;
}
$numberOfColumns = $dataCellWidths->count();
// How many unspecified columns are there?
$unspecifiedColumns = $numberOfColumns - $result->count();
$averageWidth = $this->averageWidth($availableWidth);
// Reserve some space for the columns that have no minimum.
// Make sure they collectively get at least half of the average
// width for each column. Or should it be a quarter?
$reservedSpacePerColumn = ($averageWidth / 2);
$reservedSpace = $reservedSpacePerColumn * $unspecifiedColumns;
// Calculate how much of the available space is remaining for use by
// the minimum column widths after the reserved space is accounted for.
$remainingAvailable = $availableWidth - $reservedSpace;
// Don't do anything if our widths fit inside the available widths.
if ($result->totalWidth() <= $remainingAvailable) {
return $result;
}
// Shrink the minimum widths if the table is too compressed.
return $result->distribute($remainingAvailable);
}
/**
* Return proportional weights
*/
public function distribute($availableWidth)
{
$result = [];
$totalWidth = $this->totalWidth();
$lastColumn = $this->lastColumn();
$widths = $this->widths();
// Take off the last column, and calculate proportional weights
// for the first N-1 columns.
array_pop($widths);
foreach ($widths as $key => $width) {
$result[$key] = round(($width / $totalWidth) * $availableWidth);
}
// Give the last column the rest of the available width
$usedWidth = $this->sumWidth($result);
$result[$lastColumn] = $availableWidth - $usedWidth;
return new ColumnWidths($result);
}
public function lastColumn()
{
$keys = $this->keys();
return array_pop($keys);
}
/**
* Return the number of columns.
*/
public function count()
{
return count($this->widths);
}
/**
* Calculate how much space is available on average for all columns.
*/
public function averageWidth($availableWidth)
{
if ($this->isEmpty()) {
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
}
return $availableWidth / $this->count();
}
/**
* Return the available keys (column identifiers) from the calculated
* data set.
*/
public function keys()
{
return array_keys($this->widths);
}
/**
* Set the length of the specified column.
*/
public function setWidth($key, $width)
{
$this->widths[$key] = $width;
}
/**
* Return the length of the specified column.
*/
public function width($key)
{
return isset($this->widths[$key]) ? $this->widths[$key] : 0;
}
/**
* Return all of the lengths
*/
public function widths()
{
return $this->widths;
}
/**
* Return true if there is no data in this object
*/
public function isEmpty()
{
return empty($this->widths);
}
/**
* Return the sum of the lengths of the provided widths.
*/
public function totalWidth()
{
return static::sumWidth($this->widths());
}
/**
* Return the sum of the lengths of the provided widths.
*/
public static function sumWidth($widths)
{
return array_reduce(
$widths,
function ($carry, $item) {
return $carry + $item;
}
);
}
/**
* Ensure that every item in $widths that has a corresponding entry
* in $minimumWidths is as least as large as the minimum value held there.
*/
public function enforceMinimums($minimumWidths)
{
$result = [];
if ($minimumWidths instanceof ColumnWidths) {
$minimumWidths = $minimumWidths->widths();
}
$minimumWidths += $this->widths;
foreach ($this->widths as $key => $value) {
$result[$key] = max($value, $minimumWidths[$key]);
}
return new ColumnWidths($result);
}
/**
* Remove all of the specified columns from this data structure.
*/
public function removeColumns($columnKeys)
{
$widths = $this->widths();
foreach ($columnKeys as $key) {
unset($widths[$key]);
}
return new ColumnWidths($widths);
}
/**
* Select all columns that exist in the provided list of keys.
*/
public function selectColumns($columnKeys)
{
$widths = [];
foreach ($columnKeys as $key) {
if (isset($this->widths[$key])) {
$widths[$key] = $this->width($key);
}
}
return new ColumnWidths($widths);
}
/**
* Combine this set of widths with another set, and return
* a new set that contains the entries from both.
*/
public function combine(ColumnWidths $combineWith)
{
// Danger: array_merge renumbers numeric keys; that must not happen here.
$combined = $combineWith->widths();
foreach ($this->widths() as $key => $value) {
$combined[$key] = $value;
}
return new ColumnWidths($combined);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Consolidation\OutputFormatters\Validate;
/**
* Formatters may implement ValidDataTypesInterface in order to indicate
* exactly which formats they support. The validDataTypes method can be
* called to retrieve a list of data types useful in providing hints in
* exception messages about which data types can be used with the formatter.
*
* Note that it is OPTIONAL for formatters to implement this interface.
* If a formatter implements only ValidationInterface, then clients that
* request the formatter via FormatterManager::write() will still get a list
* (via an InvalidFormatException) of all of the formats that are usable
* with the provided data type. Implementing ValidDataTypesInterface is
* benefitial to clients who instantiate a formatter directly (via `new`).
*
* Formatters that implement ValidDataTypesInterface may wish to use
* ValidDataTypesTrait.
*/
interface ValidDataTypesInterface extends ValidationInterface
{
/**
* Return the list of data types acceptable to this formatter
*
* @return \ReflectionClass[]
*/
public function validDataTypes();
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Consolidation\OutputFormatters\Validate;
/**
* Provides a default implementation of isValidDataType.
*
* Users of this trait are expected to implement ValidDataTypesInterface.
*/
trait ValidDataTypesTrait
{
/**
* Return the list of data types acceptable to this formatter
*
* @return \ReflectionClass[]
*/
abstract public function validDataTypes();
/**
* Return the list of data types acceptable to this formatter
*/
public function isValidDataType(\ReflectionClass $dataType)
{
return array_reduce(
$this->validDataTypes(),
function ($carry, $supportedType) use ($dataType) {
return
$carry ||
($dataType->getName() == $supportedType->getName()) ||
($dataType->isSubclassOf($supportedType->getName()));
},
false
);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Consolidation\OutputFormatters\Validate;
/**
* Formatters may implement ValidationInterface in order to indicate
* whether a particular data structure is supported. Any formatter that does
* not implement ValidationInterface is assumed to only operate on arrays,
* or data types that implement SimplifyToArrayInterface.
*/
interface ValidationInterface
{
/**
* Return true if the specified format is valid for use with
* this formatter.
*/
public function isValidDataType(\ReflectionClass $dataType);
/**
* Throw an IncompatibleDataException if the provided data cannot
* be processed by this formatter. Return the source data if it
* is valid. The data may be encapsulated or converted if necessary.
*
* @param mixed $structuredData Data to validate
*
* @return mixed
*/
public function validate($structuredData);
}