Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f522265

Browse filesBrowse files
committed
[Serializer] Add a CSV encoder
1 parent 473263a commit f522265
Copy full SHA for f522265

File tree

Expand file treeCollapse file tree

2 files changed

+380
-0
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+380
-0
lines changed
+169Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Encoder;
13+
14+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Encodes CSV data.
18+
*
19+
* @author Kévin Dunglas <dunglas@gmail.com>
20+
*/
21+
class CsvEncoder implements EncoderInterface, DecoderInterface
22+
{
23+
const FORMAT = 'csv';
24+
25+
private $delimiter;
26+
private $enclosure;
27+
private $escapeChar;
28+
private $keySeparator;
29+
30+
/**
31+
* @param string $delimiter
32+
* @param string $enclosure
33+
* @param string $escapeChar
34+
* @param string $keySeparator
35+
*/
36+
public function __construct($delimiter = ',', $enclosure = '"', $escapeChar = '\\', $keySeparator = '.')
37+
{
38+
$this->delimiter = $delimiter;
39+
$this->enclosure = $enclosure;
40+
$this->escapeChar = $escapeChar;
41+
$this->keySeparator = $keySeparator;
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
public function encode($data, $format, array $context = array())
48+
{
49+
$handle = fopen('php://temp,', 'w+');
50+
51+
// Sequential arrays are considered as collections
52+
if (array_keys($data) !== range(0, count($data) - 1)) {
53+
$data = array($data);
54+
}
55+
56+
$headers = null;
57+
foreach ($data as $value) {
58+
$result = array();
59+
$this->flatten($value, $result);
60+
61+
if (null === $headers) {
62+
$headers = array_keys($result);
63+
fputcsv($handle, $headers, $this->delimiter, $this->enclosure, $this->escapeChar);
64+
} elseif (array_keys($result) !== $headers) {
65+
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.');
66+
}
67+
68+
fputcsv($handle, $result, $this->delimiter, $this->enclosure, $this->escapeChar);
69+
}
70+
71+
rewind($handle);
72+
$value = stream_get_contents($handle);
73+
fclose($handle);
74+
75+
return $value;
76+
}
77+
78+
/**
79+
* Flattens an array and generates keys including the path.
80+
*
81+
* @param array $array
82+
* @param array $result
83+
* @param string $parentKey
84+
*/
85+
private function flatten(array $array, array &$result, $parentKey = '')
86+
{
87+
foreach ($array as $key => $value) {
88+
if (is_array($value)) {
89+
$this->flatten($value, $result, $parentKey.$key.$this->keySeparator);
90+
} else {
91+
$result[$parentKey.$key] = $value;
92+
}
93+
}
94+
}
95+
96+
/**
97+
* {@inheritdoc}
98+
*/
99+
public function supportsEncoding($format)
100+
{
101+
return self::FORMAT === $format;
102+
}
103+
104+
/**
105+
* {@inheritdoc}
106+
*/
107+
public function decode($data, $format, array $context = array())
108+
{
109+
$handle = fopen('php://temp', 'r+');
110+
fwrite($handle, $data);
111+
rewind($handle);
112+
113+
$headers = null;
114+
$nbHeaders = 0;
115+
$result = array();
116+
117+
while (false !== ($cols = fgetcsv($handle, 0, $this->delimiter, $this->enclosure, $this->escapeChar))) {
118+
$nbCols = count($cols);
119+
120+
if (null === $headers) {
121+
$nbHeaders = $nbCols;
122+
123+
foreach ($cols as $col) {
124+
$headers[] = explode($this->keySeparator, $col);
125+
}
126+
127+
continue;
128+
}
129+
130+
$item = array();
131+
for ($i = 0; ($i < $nbCols) && ($i < $nbHeaders); ++$i) {
132+
$depth = count($headers[$i]);
133+
$arr = &$item;
134+
for ($j = 0; $j < $depth; ++$j) {
135+
// Handle nested arrays
136+
if ($j === ($depth - 1)) {
137+
$arr[$headers[$i][$j]] = $cols[$i];
138+
139+
continue;
140+
}
141+
142+
if (!isset($arr[$headers[$i][$j]])) {
143+
$arr[$headers[$i][$j]] = array();
144+
}
145+
146+
$arr = &$arr[$headers[$i][$j]];
147+
}
148+
}
149+
150+
$result[] = $item;
151+
}
152+
fclose($handle);
153+
154+
if (empty($result) || isset($result[1])) {
155+
return $result;
156+
}
157+
158+
// If there is only one data line in the document, return it (the line), the result is not considered as a collection
159+
return $result[0];
160+
}
161+
162+
/**
163+
* {@inheritdoc}
164+
*/
165+
public function supportsDecoding($format)
166+
{
167+
return self::FORMAT === $format;
168+
}
169+
}
+211Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Encoder;
13+
14+
use Symfony\Component\Serializer\Encoder\CsvEncoder;
15+
16+
/**
17+
* @author Kévin Dunglas <dunglas@gmail.com>
18+
*/
19+
class CsvEncoderTest extends \PHPUnit_Framework_TestCase
20+
{
21+
/**
22+
* @var CsvEncoder
23+
*/
24+
private $encoder;
25+
26+
protected function setUp()
27+
{
28+
$this->encoder = new CsvEncoder();
29+
}
30+
31+
public function testSupportEncoding()
32+
{
33+
$this->assertTrue($this->encoder->supportsEncoding('csv'));
34+
$this->assertFalse($this->encoder->supportsEncoding('foo'));
35+
}
36+
37+
public function testEncode()
38+
{
39+
$value = array('foo' => 'hello', 'bar' => 'hey ho');
40+
41+
$this->assertEquals(<<<'CSV'
42+
foo,bar
43+
hello,"hey ho"
44+
45+
CSV
46+
, $this->encoder->encode($value, 'csv'));
47+
}
48+
49+
public function testEncodeCollection()
50+
{
51+
$value = array(
52+
array('foo' => 'hello', 'bar' => 'hey ho'),
53+
array('foo' => 'hi', 'bar' => 'let\'s go'),
54+
);
55+
56+
$this->assertEquals(<<<'CSV'
57+
foo,bar
58+
hello,"hey ho"
59+
hi,"let's go"
60+
61+
CSV
62+
, $this->encoder->encode($value, 'csv'));
63+
}
64+
65+
public function testEncodeNestedArrays()
66+
{
67+
$value = array('foo' => 'hello', 'bar' => array(
68+
array('id' => 'yo', 1 => 'wesh'),
69+
array('baz' => 'Halo', 'foo' => 'olá'),
70+
));
71+
72+
$this->assertEquals(<<<'CSV'
73+
foo,bar.0.id,bar.0.1,bar.1.baz,bar.1.foo
74+
hello,yo,wesh,Halo,olá
75+
76+
CSV
77+
, $this->encoder->encode($value, 'csv'));
78+
}
79+
80+
public function testEncodeCustomSettings()
81+
{
82+
$this->encoder = new CsvEncoder(';', "'", '|', '-');
83+
84+
$value = array('a' => 'he\'llo', 'c' => array('d' => 'foo'));
85+
86+
$this->assertEquals(<<<'CSV'
87+
a;c-d
88+
'he''llo';foo
89+
90+
CSV
91+
, $this->encoder->encode($value, 'csv'));
92+
}
93+
94+
public function testEncodeEmptyArray()
95+
{
96+
$this->assertEquals("\n\n", $this->encoder->encode(array(), 'csv'));
97+
$this->assertEquals("\n\n", $this->encoder->encode(array(array()), 'csv'));
98+
}
99+
100+
/**
101+
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
102+
*/
103+
public function testEncodeNonFlattenableStructure()
104+
{
105+
$this->encoder->encode(array(array('a' => array('foo', 'bar')), array('a' => array())), 'csv');
106+
}
107+
108+
public function testSupportsDecoding()
109+
{
110+
$this->assertTrue($this->encoder->supportsDecoding('csv'));
111+
$this->assertFalse($this->encoder->supportsDecoding('foo'));
112+
}
113+
114+
public function testDecode()
115+
{
116+
$expected = array('foo' => 'a', 'bar' => 'b');
117+
118+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
119+
foo,bar
120+
a,b
121+
CSV
122+
, 'csv'));
123+
}
124+
125+
public function testDecodeCollection()
126+
{
127+
$expected = array(
128+
array('foo' => 'a', 'bar' => 'b'),
129+
array('foo' => 'c', 'bar' => 'd'),
130+
array('foo' => 'f'),
131+
);
132+
133+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
134+
foo,bar
135+
a,b
136+
c,d
137+
f
138+
139+
CSV
140+
, 'csv'));
141+
}
142+
143+
public function testDecodeToManyRelation()
144+
{
145+
$expected = array(
146+
array('foo' => 'bar', 'relations' => array(array('a' => 'b'), array('a' => 'b'))),
147+
array('foo' => 'bat', 'relations' => array(array('a' => 'b'), array('a' => ''))),
148+
array('foo' => 'bat', 'relations' => array(array('a' => 'b'))),
149+
array('foo' => 'baz', 'relations' => array(array('a' => 'c'), array('a' => 'c'))),
150+
);
151+
152+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
153+
foo,relations.0.a,relations.1.a
154+
bar,b,b
155+
bat,b,
156+
bat,b
157+
baz,c,c
158+
CSV
159+
, 'csv'));
160+
}
161+
162+
public function testDecodeNestedArrays()
163+
{
164+
$expected = array(
165+
array('foo' => 'a', 'bar' => array('baz' => array('bat' => 'b'))),
166+
array('foo' => 'c', 'bar' => array('baz' => array('bat' => 'd'))),
167+
);
168+
169+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
170+
foo,bar.baz.bat
171+
a,b
172+
c,d
173+
CSV
174+
, 'csv'));
175+
}
176+
177+
public function testDecodeCustomSettings()
178+
{
179+
$this->encoder = new CsvEncoder(';', "'", '|', '-');
180+
181+
$expected = array('a' => 'hell\'o', 'bar' => array('baz' => 'b'));
182+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
183+
a;bar-baz
184+
'hell''o';b;c
185+
CSV
186+
, 'csv'));
187+
}
188+
189+
public function testDecodeMalformedCollection()
190+
{
191+
$expected = array(
192+
array('foo' => 'a', 'bar' => 'b'),
193+
array('foo' => 'c', 'bar' => 'd'),
194+
array('foo' => 'f'),
195+
);
196+
197+
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
198+
foo,bar
199+
a,b,e
200+
c,d,g,h
201+
f
202+
203+
CSV
204+
, 'csv'));
205+
}
206+
207+
public function testDecodeEmptyArray()
208+
{
209+
$this->assertEquals(array(), $this->encoder->decode('', 'csv'));
210+
}
211+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.