11
11
12
12
namespace Symfony \Component \Serializer \Encoder ;
13
13
14
+ use Symfony \Component \Serializer \ByteOrderMark ;
14
15
use Symfony \Component \Serializer \Exception \InvalidArgumentException ;
15
16
16
17
/**
@@ -30,6 +31,8 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
30
31
const ESCAPE_FORMULAS_KEY = 'csv_escape_formulas ' ;
31
32
const AS_COLLECTION_KEY = 'as_collection ' ;
32
33
const NO_HEADERS_KEY = 'no_headers ' ;
34
+ const OUTPUT_BOM = 'output_bom ' ;
35
+ const SKIP_INPUT_BOM = 'skip_input_bom ' ;
33
36
34
37
private $ formulasStartCharacters = ['= ' , '- ' , '+ ' , '@ ' ];
35
38
private $ defaultContext = [
@@ -40,6 +43,8 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
40
43
self ::HEADERS_KEY => [],
41
44
self ::KEY_SEPARATOR_KEY => '. ' ,
42
45
self ::NO_HEADERS_KEY => false ,
46
+ self ::OUTPUT_BOM => '' ,
47
+ self ::SKIP_INPUT_BOM => true ,
43
48
];
44
49
45
50
/**
@@ -90,7 +95,7 @@ public function encode($data, $format, array $context = [])
90
95
}
91
96
}
92
97
93
- list ($ delimiter , $ enclosure , $ escapeChar , $ keySeparator , $ headers , $ escapeFormulas ) = $ this ->getCsvOptions ($ context );
98
+ list ($ delimiter , $ enclosure , $ escapeChar , $ keySeparator , $ headers , $ escapeFormulas, $ outputBOM ) = $ this ->getCsvOptions ($ context );
94
99
95
100
foreach ($ data as &$ value ) {
96
101
$ flattened = [];
@@ -101,6 +106,8 @@ public function encode($data, $format, array $context = [])
101
106
102
107
$ headers = array_merge (array_values ($ headers ), array_diff ($ this ->extractHeaders ($ data ), $ headers ));
103
108
109
+ fwrite ($ handle , $ outputBOM );
110
+
104
111
if (!($ context [self ::NO_HEADERS_KEY ] ?? false )) {
105
112
fputcsv ($ handle , $ headers , $ delimiter , $ enclosure , $ escapeChar );
106
113
}
@@ -134,6 +141,11 @@ public function decode($data, $format, array $context = [])
134
141
fwrite ($ handle , $ data );
135
142
rewind ($ handle );
136
143
144
+ if (($ context [self ::SKIP_INPUT_BOM ] ?? true ) === false ) {
145
+ $ inputBom = $ this ->determineBom (substr ($ data , 0 , 4 ));
146
+ fseek ($ handle , strlen ($ inputBom ));
147
+ }
148
+
137
149
$ headers = null ;
138
150
$ nbHeaders = 0 ;
139
151
$ headerCount = [];
@@ -238,12 +250,13 @@ private function getCsvOptions(array $context): array
238
250
$ keySeparator = $ context [self ::KEY_SEPARATOR_KEY ] ?? $ this ->defaultContext [self ::KEY_SEPARATOR_KEY ];
239
251
$ headers = $ context [self ::HEADERS_KEY ] ?? $ this ->defaultContext [self ::HEADERS_KEY ];
240
252
$ escapeFormulas = $ context [self ::ESCAPE_FORMULAS_KEY ] ?? $ this ->defaultContext [self ::ESCAPE_FORMULAS_KEY ];
253
+ $ outputBOM = $ context [self ::OUTPUT_BOM ] ?? $ this ->defaultContext [self ::OUTPUT_BOM ];
241
254
242
255
if (!\is_array ($ headers )) {
243
256
throw new InvalidArgumentException (sprintf ('The "%s" context variable must be an array or null, given "%s". ' , self ::HEADERS_KEY , \gettype ($ headers )));
244
257
}
245
258
246
- return [$ delimiter , $ enclosure , $ escapeChar , $ keySeparator , $ headers , $ escapeFormulas ];
259
+ return [$ delimiter , $ enclosure , $ escapeChar , $ keySeparator , $ headers , $ escapeFormulas, $ outputBOM ];
247
260
}
248
261
249
262
/**
@@ -281,4 +294,18 @@ private function extractHeaders(iterable $data): array
281
294
282
295
return $ headers ;
283
296
}
297
+
298
+ private function determineBom (string $ text ): string
299
+ {
300
+ static $ list ;
301
+
302
+ $ list = $ list ?? (new \ReflectionClass (ByteOrderMark::class))->getConstants ();
303
+ foreach ($ list as $ sequence ) {
304
+ if (0 === strpos ($ text , $ sequence )) {
305
+ return $ sequence ;
306
+ }
307
+ }
308
+
309
+ return '' ;
310
+ }
284
311
}
0 commit comments