diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index ef59f22499264..9a6e20d52f96d 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index cdbe0eb44e659..b4e501d7efab7 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -17,6 +17,7 @@ * Encodes CSV data. * * @author Kévin Dunglas + * @author Oliver Hoff */ 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 @@ public function encode($data, $format, array $context = array()) } } - 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, $headers, $delimiter, $enclosure, $escapeChar); - fputcsv($handle, $result, $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 @@ private function getCsvOptions(array $context) $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(); + + 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 array($delimiter, $enclosure, $escapeChar, $keySeparator); + return $headers; } } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 61cbc03ee6d26..a5e5c256f34ad 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -135,12 +135,42 @@ public function testEncodeEmptyArray() $this->assertEquals("\n\n", $this->encoder->encode(array(array()), 'csv')); } - /** - * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException - */ - public function testEncodeNonFlattenableStructure() + public function testEncodeVariableStructure() + { + $value = array( + array('a' => array('foo', 'bar')), + array('a' => array(), 'b' => 'baz'), + array('a' => array('bar', 'foo'), 'c' => 'pong'), + ); + $csv = <<assertEquals($csv, $this->encoder->encode($value, 'csv')); + } + + public function testEncodeCustomHeaders() { - $this->encoder->encode(array(array('a' => array('foo', 'bar')), array('a' => array())), 'csv'); + $context = array( + CsvEncoder::HEADERS_KEY => array( + 'b', + 'c', + ), + ); + $value = array( + array('a' => 'foo', 'b' => 'bar'), + ); + $csv = <<assertEquals($csv, $this->encoder->encode($value, 'csv', $context)); } public function testSupportsDecoding() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy