Refactoring

This commit is contained in:
gamonoid
2017-09-03 20:39:22 +02:00
parent af40881847
commit a7274d3cfd
5075 changed files with 238202 additions and 16291 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,395 @@
<?php
namespace Consolidation\OutputFormatters;
use Consolidation\OutputFormatters\Exception\IncompatibleDataException;
use Consolidation\OutputFormatters\Exception\InvalidFormatException;
use Consolidation\OutputFormatters\Exception\UnknownFormatException;
use Consolidation\OutputFormatters\Formatters\FormatterInterface;
use Consolidation\OutputFormatters\Formatters\RenderDataInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
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;
/**
* 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 = [
'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',
];
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_OPTIONAL, $description, $defaultFormat);
}
if ($availableFields) {
$defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], '');
$description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields));
$automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields);
$automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_OPTIONAL, "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)
{
$formatter = $this->getFormatter((string)$format);
if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) {
$validFormats = $this->validFormats($structuredOutput);
throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats);
}
// Give the formatter a chance to override the options
$options = $this->overrideOptions($formatter, $structuredOutput, $options);
$structuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options);
$formatter->write($output, $structuredOutput, $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;
}
/**
* 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)
{
if ($formatter instanceof OverrideOptionsInterface) {
return $formatter->overrideOptions($structuredOutput, $options);
}
return $options;
}
}

View File

@@ -0,0 +1,107 @@
<?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 => ',',
];
}
/**
* @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);
}
}
protected function writeOneLine(OutputInterface $output, $data, $options)
{
$defaults = $this->getDefaultFormatterOptions();
$delimiter = $options->get(FormatterOptions::DELIMITER, $defaults);
$output->write($this->csvEscape($data, $delimiter));
}
protected function csvEscape($data, $delimiter = ',')
{
$buffer = fopen('php://temp', 'r+');
fputcsv($buffer, $data, $delimiter);
rewind($buffer);
$csv = fgets($buffer);
fclose($buffer);
return $csv;
}
}

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 mixed $data Structured data to format
*
* @return string
*/
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;
/**
* 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,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,81 @@
<?php
namespace Consolidation\OutputFormatters\Formatters;
use Consolidation\OutputFormatters\Validate\ValidationInterface;
use Consolidation\OutputFormatters\Options\OverrideOptionsInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\Validate\ValidDataTypesTrait;
use Symfony\Component\Console\Output\OutputInterface;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
/**
* 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
{
/**
* All data types are acceptable.
*/
public function isValidDataType(\ReflectionClass $dataType)
{
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 as a TSV value.
*
* @param OutputInterface $output
* @param mixed $data
* @param FormatterOptions $options
*/
protected function reduceToSigleFieldAndWrite(OutputInterface $output, $data, FormatterOptions $options)
{
$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,128 @@
<?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;
/**
* 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
{
use ValidDataTypesTrait;
use RenderTableDataTrait;
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);
if ($includeHeaders && !$isList) {
$headers = $tableTransformer->getHeaders();
$table->setHeaders($headers);
}
$data = $tableTransformer->getTableData($includeHeaders && $isList);
$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();
$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,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,76 @@
<?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 (!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,372 @@
<?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_STRING_FIELD = 'default-string-field';
const DELIMITER = 'delimiter';
const TERMINAL_WIDTH = 'width';
/**
* 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 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);
}
/**
* 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,90 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\StructuredData\RestructureInterface;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\ListDataInterface;
use Consolidation\OutputFormatters\Transformations\ReorderFields;
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 \ArrayObject implements RestructureInterface, ListDataInterface, RenderCellCollectionInterface
{
use RenderCellCollectionTrait;
protected $data;
public function __construct($data)
{
parent::__construct($data);
}
abstract public function restructure(FormatterOptions $options);
abstract public function getListData(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 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, $defaults);
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 => [],
FormatterOptions::ROW_LABELS => [],
FormatterOptions::DEFAULT_FIELDS => [],
];
}
}

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,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,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,58 @@
<?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
{
/**
* 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,16 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
interface RenderCellCollectionInterface extends RenderCellInterface
{
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,59 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
use Consolidation\OutputFormatters\Options\FormatterOptions;
trait RenderCellCollectionTrait
{
/** @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) {
$cellData = $renderer->renderCell($key, $cellData, $options, $rowData);
if (is_string($cellData)) {
return $cellData;
}
}
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,39 @@
<?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
{
/**
* 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,22 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData;
interface TableDataInterface
{
/**
* Return the original data for this table. Used by any
* formatter that is -not- a table.
*/
public function getOriginalData();
/**
* 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,12 @@
<?php
namespace Consolidation\OutputFormatters\StructuredData\Xml;
interface DomDataInterface
{
/**
* Convert data into a \DomDocument.
*
* @return \DomDocument
*/
public function getDomData();
}

View File

@@ -0,0 +1,96 @@
<?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 = is_numeric($key) ? $childElementName : $key;
if (($elementName != $childElementName) && $this->isAttribute($elementName, $key, $value)) {
$xmlParent->setAttribute($key, $value);
return;
}
$this->addXmlData($dom, $xmlParent, $elementName, $value);
}
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,212 @@
<?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);
if ("{$uniformChildrenName}s" == $element->nodeName) {
$result = $this->getUniformChildren($element->nodeName, $element);
} else {
$result = $this->getUniqueChildren($element->nodeName, $element);
}
return $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);
}
$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);
}
/**
* 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,36 @@
<?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) + ['', ''];
$result[$key] = trim($value);
}
return $result;
}
}

View File

@@ -0,0 +1,123 @@
<?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);
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 (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,123 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
use Consolidation\OutputFormatters\StructuredData\TableDataInterface;
use Consolidation\OutputFormatters\StructuredData\OriginalDataInterface;
class TableTransformation extends \ArrayObject implements TableDataInterface, OriginalDataInterface
{
protected $headers;
protected $rowLabels;
protected $layout;
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;
}
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()
{
return $this->getArrayCopy();
}
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,227 @@
<?php
namespace Consolidation\OutputFormatters\Transformations;
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;
}
/**
* Calculate our padding widths from the specified table style.
* @param TableStyle $style
*/
public function setPaddingFromStyle(TableStyle $style)
{
$verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar()));
$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 = $minimumWidths;
}
/**
* Wrap the cells in each part of the provided data table
* @param array $rows
* @return array
*/
public function wrap($rows, $widths = [])
{
// If the width was not set, then disable wordwrap.
if (!$this->width) {
return $rows;
}
// Calculate the column widths to use based on the content.
$auto_widths = $this->columnAutowidth($rows, $widths);
// 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[$colkey]);
}
}
return $newrows;
}
/**
* 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);
}
/**
* Determine the best fit for column widths. Ported from Drush.
*
* @param array $rows The rows to use for calculations.
* @param array $widths Manually specified widths of each column
* (in characters) - these will be left as is.
*/
protected function columnAutowidth($rows, $widths)
{
$auto_widths = $widths;
// First we determine the distribution of row lengths in each column.
// This is an array of descending character length keys (i.e. starting at
// the rightmost character column), with the value indicating the number
// of rows where that character column is present.
$col_dist = [];
// We will also calculate the longest word in each column
$max_word_lens = [];
foreach ($rows as $rowkey => $row) {
foreach ($row as $col_id => $cell) {
$longest_word_len = static::longestWordLength($cell);
if ((!isset($max_word_lens[$col_id]) || ($max_word_lens[$col_id] < $longest_word_len))) {
$max_word_lens[$col_id] = $longest_word_len;
}
if (empty($widths[$col_id])) {
$length = strlen($cell);
if ($length == 0) {
$col_dist[$col_id][0] = 0;
}
while ($length > 0) {
if (!isset($col_dist[$col_id][$length])) {
$col_dist[$col_id][$length] = 0;
}
$col_dist[$col_id][$length]++;
$length--;
}
}
}
}
foreach ($col_dist as $col_id => $count) {
// Sort the distribution in decending key order.
krsort($col_dist[$col_id]);
// Initially we set all columns to their "ideal" longest width
// - i.e. the width of their longest column.
$auto_widths[$col_id] = max(array_keys($col_dist[$col_id]));
}
// We determine what width we have available to use, and what width the
// above "ideal" columns take up.
$available_width = $this->width - ($this->extraPaddingAtBeginningOfLine + $this->extraPaddingAtEndOfLine + (count($auto_widths) * $this->paddingInEachCell));
$auto_width_current = array_sum($auto_widths);
// If we cannot fit into the minimum width anyway, then just return
// the max word length of each column as the 'ideal'
$minimumIdealLength = array_sum($this->minimumWidths);
if ($minimumIdealLength && ($available_width < $minimumIdealLength)) {
return $max_word_lens;
}
// If we need to reduce a column so that we can fit the space we use this
// loop to figure out which column will cause the "least wrapping",
// (relative to the other columns) and reduce the width of that column.
while ($auto_width_current > $available_width) {
list($column, $width) = $this->selectColumnToReduce($col_dist, $auto_widths, $max_word_lens);
if (!$column || $width <= 1) {
// If we have reached a width of 1 then give up, so wordwrap can still progress.
break;
}
// Reduce the width of the selected column.
$auto_widths[$column]--;
// Reduce our overall table width counter.
$auto_width_current--;
// Remove the corresponding data from the disctribution, so next time
// around we use the data for the row to the left.
unset($col_dist[$column][$width]);
}
return $auto_widths;
}
protected function selectColumnToReduce($col_dist, $auto_widths, $max_word_lens)
{
$column = false;
$count = 0;
$width = 0;
foreach ($col_dist as $col_id => $counts) {
// Of the columns whose length is still > than the the lenght
// of their maximum word length
if ($auto_widths[$col_id] > $max_word_lens[$col_id]) {
if ($this->shouldSelectThisColumn($count, $counts, $width)) {
$column = $col_id;
$count = current($counts);
$width = key($counts);
}
}
}
if ($column !== false) {
return [$column, $width];
}
foreach ($col_dist as $col_id => $counts) {
if (empty($this->minimumWidths) || ($auto_widths[$col_id] > $this->minimumWidths[$col_id])) {
if ($this->shouldSelectThisColumn($count, $counts, $width)) {
$column = $col_id;
$count = current($counts);
$width = key($counts);
}
}
}
return [$column, $width];
}
protected function shouldSelectThisColumn($count, $counts, $width)
{
return
// If we are just starting out, select the first column.
($count == 0) ||
// OR: if this column would cause less wrapping than the currently
// selected column, then select it.
(current($counts) < $count) ||
// OR: if this column would cause the same amount of wrapping, but is
// longer, then we choose to wrap the longer column (proportionally
// less wrapping, and helps avoid triple line wraps).
(current($counts) == $count && key($counts) > $width);
}
/**
* 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,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[]
*/
public abstract 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);
}