CsvEncoder handling variable structures and custom header order

This commit is contained in:
Oliver Hoff 2017-09-19 11:55:20 +02:00 committed by Fabien Potencier
parent aad62c427c
commit d173494e48
3 changed files with 96 additions and 18 deletions

View File

@ -7,6 +7,8 @@ CHANGELOG
* added `AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT` context option
to disable throwing an `UnexpectedValueException` on a type mismatch
* added support for serializing `DateInterval` objects
* improved `CsvEncoder` to handle variable nested structures
* CSV headers can be passed to the `CsvEncoder` via the `csv_headers` serialization context variable
3.3.0
-----

View File

@ -17,6 +17,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
* Encodes CSV data.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Oliver Hoff <oliver@hofff.com>
*/
class CsvEncoder implements EncoderInterface, DecoderInterface
{
@ -25,6 +26,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
const ENCLOSURE_KEY = 'csv_enclosure';
const ESCAPE_CHAR_KEY = 'csv_escape_char';
const KEY_SEPARATOR_KEY = 'csv_key_separator';
const HEADERS_KEY = 'csv_headers';
private $delimiter;
private $enclosure;
@ -69,21 +71,22 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
}
}
list($delimiter, $enclosure, $escapeChar, $keySeparator) = $this->getCsvOptions($context);
list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers) = $this->getCsvOptions($context);
$headers = null;
foreach ($data as $value) {
$result = array();
$this->flatten($value, $result, $keySeparator);
foreach ($data as &$value) {
$flattened = array();
$this->flatten($value, $flattened, $keySeparator);
$value = $flattened;
}
unset($value);
if (null === $headers) {
$headers = array_keys($result);
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
} elseif (array_keys($result) !== $headers) {
throw new InvalidArgumentException('To use the CSV encoder, each line in the data array must have the same structure. You may want to use a custom normalizer class to normalize the data format before passing it to the CSV encoder.');
}
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
fputcsv($handle, $result, $delimiter, $enclosure, $escapeChar);
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
$headers = array_fill_keys($headers, '');
foreach ($data as $row) {
fputcsv($handle, array_replace($headers, $row), $delimiter, $enclosure, $escapeChar);
}
rewind($handle);
@ -194,7 +197,50 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
$enclosure = isset($context[self::ENCLOSURE_KEY]) ? $context[self::ENCLOSURE_KEY] : $this->enclosure;
$escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar;
$keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator;
$headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : array();
return array($delimiter, $enclosure, $escapeChar, $keySeparator);
if (!is_array($headers)) {
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, gettype($headers)));
}
return array($delimiter, $enclosure, $escapeChar, $keySeparator, $headers);
}
/**
* @param array $data
*
* @return string[]
*/
private function extractHeaders(array $data)
{
$headers = array();
$flippedHeaders = array();
foreach ($data as $row) {
$previousHeader = null;
foreach ($row as $header => $_) {
if (isset($flippedHeaders[$header])) {
$previousHeader = $header;
continue;
}
if (null === $previousHeader) {
$n = count($headers);
} else {
$n = $flippedHeaders[$previousHeader] + 1;
for ($j = count($headers); $j > $n; --$j) {
++$flippedHeaders[$headers[$j] = $headers[$j - 1]];
}
}
$headers[$n] = $header;
$flippedHeaders[$header] = $n;
$previousHeader = $header;
}
}
return $headers;
}
}

View File

@ -135,12 +135,42 @@ CSV
$this->assertEquals("\n\n", $this->encoder->encode(array(array()), 'csv'));
}
/**
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
*/
public function testEncodeNonFlattenableStructure()
public function testEncodeVariableStructure()
{
$this->encoder->encode(array(array('a' => array('foo', 'bar')), array('a' => array())), 'csv');
$value = array(
array('a' => array('foo', 'bar')),
array('a' => array(), 'b' => 'baz'),
array('a' => array('bar', 'foo'), 'c' => 'pong'),
);
$csv = <<<CSV
a.0,a.1,c,b
foo,bar,,
,,,baz
bar,foo,pong,
CSV;
$this->assertEquals($csv, $this->encoder->encode($value, 'csv'));
}
public function testEncodeCustomHeaders()
{
$context = array(
CsvEncoder::HEADERS_KEY => array(
'b',
'c',
),
);
$value = array(
array('a' => 'foo', 'b' => 'bar'),
);
$csv = <<<CSV
b,c,a
bar,,foo
CSV;
$this->assertEquals($csv, $this->encoder->encode($value, 'csv', $context));
}
public function testSupportsDecoding()