Skip to content

Commit 115b988

Browse files
committed
feature #24256 CsvEncoder handling variable structures and custom header order (Oliver Hoff)
This PR was squashed before being merged into the 3.4 branch (closes #24256). Discussion ---------- CsvEncoder handling variable structures and custom header order | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #23278 | License | MIT | Doc PR | TBD This PR improves the CsvEncoder to handle variable nesting structures and adds a context option that allows custom csv header order. Commits ------- d173494e48 CsvEncoder handling variable structures and custom header order
2 parents f853467 + 4c626e6 commit 115b988

File tree

3 files changed

+96
-18
lines changed

3 files changed

+96
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ CHANGELOG
88
to disable throwing an `UnexpectedValueException` on a type mismatch
99
* added support for serializing `DateInterval` objects
1010
* added getter for extra attributes in `ExtraAttributesException`
11+
* improved `CsvEncoder` to handle variable nested structures
12+
* CSV headers can be passed to the `CsvEncoder` via the `csv_headers` serialization context variable
1113

1214
3.3.0
1315
-----

Encoder/CsvEncoder.php

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* Encodes CSV data.
1818
*
1919
* @author Kévin Dunglas <dunglas@gmail.com>
20+
* @author Oliver Hoff <oliver@hofff.com>
2021
*/
2122
class CsvEncoder implements EncoderInterface, DecoderInterface
2223
{
@@ -25,6 +26,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
2526
const ENCLOSURE_KEY = 'csv_enclosure';
2627
const ESCAPE_CHAR_KEY = 'csv_escape_char';
2728
const KEY_SEPARATOR_KEY = 'csv_key_separator';
29+
const HEADERS_KEY = 'csv_headers';
2830

2931
private $delimiter;
3032
private $enclosure;
@@ -69,21 +71,22 @@ public function encode($data, $format, array $context = array())
6971
}
7072
}
7173

72-
list($delimiter, $enclosure, $escapeChar, $keySeparator) = $this->getCsvOptions($context);
74+
list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers) = $this->getCsvOptions($context);
7375

74-
$headers = null;
75-
foreach ($data as $value) {
76-
$result = array();
77-
$this->flatten($value, $result, $keySeparator);
76+
foreach ($data as &$value) {
77+
$flattened = array();
78+
$this->flatten($value, $flattened, $keySeparator);
79+
$value = $flattened;
80+
}
81+
unset($value);
7882

79-
if (null === $headers) {
80-
$headers = array_keys($result);
81-
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
82-
} elseif (array_keys($result) !== $headers) {
83-
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.');
84-
}
83+
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
84+
85+
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
8586

86-
fputcsv($handle, $result, $delimiter, $enclosure, $escapeChar);
87+
$headers = array_fill_keys($headers, '');
88+
foreach ($data as $row) {
89+
fputcsv($handle, array_replace($headers, $row), $delimiter, $enclosure, $escapeChar);
8790
}
8891

8992
rewind($handle);
@@ -194,7 +197,50 @@ private function getCsvOptions(array $context)
194197
$enclosure = isset($context[self::ENCLOSURE_KEY]) ? $context[self::ENCLOSURE_KEY] : $this->enclosure;
195198
$escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar;
196199
$keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator;
200+
$headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : array();
201+
202+
if (!is_array($headers)) {
203+
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, gettype($headers)));
204+
}
205+
206+
return array($delimiter, $enclosure, $escapeChar, $keySeparator, $headers);
207+
}
208+
209+
/**
210+
* @param array $data
211+
*
212+
* @return string[]
213+
*/
214+
private function extractHeaders(array $data)
215+
{
216+
$headers = array();
217+
$flippedHeaders = array();
218+
219+
foreach ($data as $row) {
220+
$previousHeader = null;
221+
222+
foreach ($row as $header => $_) {
223+
if (isset($flippedHeaders[$header])) {
224+
$previousHeader = $header;
225+
continue;
226+
}
227+
228+
if (null === $previousHeader) {
229+
$n = count($headers);
230+
} else {
231+
$n = $flippedHeaders[$previousHeader] + 1;
232+
233+
for ($j = count($headers); $j > $n; --$j) {
234+
++$flippedHeaders[$headers[$j] = $headers[$j - 1]];
235+
}
236+
}
237+
238+
$headers[$n] = $header;
239+
$flippedHeaders[$header] = $n;
240+
$previousHeader = $header;
241+
}
242+
}
197243

198-
return array($delimiter, $enclosure, $escapeChar, $keySeparator);
244+
return $headers;
199245
}
200246
}

Tests/Encoder/CsvEncoderTest.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,42 @@ public function testEncodeEmptyArray()
135135
$this->assertEquals("\n\n", $this->encoder->encode(array(array()), 'csv'));
136136
}
137137

138-
/**
139-
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
140-
*/
141-
public function testEncodeNonFlattenableStructure()
138+
public function testEncodeVariableStructure()
139+
{
140+
$value = array(
141+
array('a' => array('foo', 'bar')),
142+
array('a' => array(), 'b' => 'baz'),
143+
array('a' => array('bar', 'foo'), 'c' => 'pong'),
144+
);
145+
$csv = <<<CSV
146+
a.0,a.1,c,b
147+
foo,bar,,
148+
,,,baz
149+
bar,foo,pong,
150+
151+
CSV;
152+
153+
$this->assertEquals($csv, $this->encoder->encode($value, 'csv'));
154+
}
155+
156+
public function testEncodeCustomHeaders()
142157
{
143-
$this->encoder->encode(array(array('a' => array('foo', 'bar')), array('a' => array())), 'csv');
158+
$context = array(
159+
CsvEncoder::HEADERS_KEY => array(
160+
'b',
161+
'c',
162+
),
163+
);
164+
$value = array(
165+
array('a' => 'foo', 'b' => 'bar'),
166+
);
167+
$csv = <<<CSV
168+
b,c,a
169+
bar,,foo
170+
171+
CSV;
172+
173+
$this->assertEquals($csv, $this->encoder->encode($value, 'csv', $context));
144174
}
145175

146176
public function testSupportsDecoding()

0 commit comments

Comments
 (0)