Skip to content

Commit 709b66d

Browse files
committed
WIP
1 parent e05c25c commit 709b66d

19 files changed

+791
-271
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Context;
13+
14+
/**
15+
* Holds contexts needed by serialalization indexed by their FQCN.
16+
*
17+
* @author Mathias Arlaud <[email protected]>
18+
*/
19+
final class Context
20+
{
21+
/**
22+
* @template T of object
23+
*
24+
* @var array<class-string<T>, T> $optionsMap
25+
*/
26+
private array $optionsMap = [];
27+
28+
public function __construct(...$options)
29+
{
30+
$this->addOptions(...$options);
31+
}
32+
33+
public function addOptions(object ...$optionsList): self
34+
{
35+
foreach ($optionsList as $options) {
36+
$this->optionsMap[get_class($options)] = $options;
37+
}
38+
39+
return $this;
40+
}
41+
42+
/**
43+
* @param class-string $optionsClass
44+
*/
45+
public function removeOptions(string ...$optionsClassList): self
46+
{
47+
foreach ($optionsClassList as $optionsClass) {
48+
unset($this->optionsMap[$optionsClass]);
49+
}
50+
51+
return $this;
52+
}
53+
54+
/**
55+
* @param class-string $optionsClass
56+
*/
57+
public function hasOptions(string $optionsClass): bool
58+
{
59+
return isset($this->optionsMap[$optionsClass]);
60+
}
61+
62+
/**
63+
* @template T of object
64+
*
65+
* @param class-string<T>
66+
*
67+
* @return T|null
68+
*/
69+
public function getOptions(string $optionsClass): ?object
70+
{
71+
return $this->optionsMap[$optionsClass] ?? null;
72+
}
73+
}

src/Symfony/Component/Serializer/Encoder/ChainDecoder.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Encoder;
1313

14+
use Symfony\Component\Serializer\Context\Context;
1415
use Symfony\Component\Serializer\Exception\RuntimeException;
1516

1617
/**
@@ -35,15 +36,15 @@ public function __construct(array $decoders = [])
3536
/**
3637
* {@inheritdoc}
3738
*/
38-
final public function decode(string $data, string $format, array $context = []): mixed
39+
final public function decode(string $data, string $format, Context $context = null): mixed
3940
{
4041
return $this->getDecoder($format, $context)->decode($data, $format, $context);
4142
}
4243

4344
/**
4445
* {@inheritdoc}
4546
*/
46-
public function supportsDecoding(string $format, array $context = []): bool
47+
public function supportsDecoding(string $format, Context $context = null): bool
4748
{
4849
try {
4950
$this->getDecoder($format, $context);
@@ -59,7 +60,7 @@ public function supportsDecoding(string $format, array $context = []): bool
5960
*
6061
* @throws RuntimeException if no decoder is found
6162
*/
62-
private function getDecoder(string $format, array $context): DecoderInterface
63+
private function getDecoder(string $format, Context $context): DecoderInterface
6364
{
6465
if (isset($this->decoderByFormat[$format])
6566
&& isset($this->decoders[$this->decoderByFormat[$format]])

src/Symfony/Component/Serializer/Encoder/ChainEncoder.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Encoder;
1313

14+
use Symfony\Component\Serializer\Context\Context;
1415
use Symfony\Component\Serializer\Exception\RuntimeException;
1516

1617
/**
@@ -35,15 +36,15 @@ public function __construct(array $encoders = [])
3536
/**
3637
* {@inheritdoc}
3738
*/
38-
final public function encode(mixed $data, string $format, array $context = []): string
39+
final public function encode(mixed $data, string $format, Context $context = null): string
3940
{
4041
return $this->getEncoder($format, $context)->encode($data, $format, $context);
4142
}
4243

4344
/**
4445
* {@inheritdoc}
4546
*/
46-
public function supportsEncoding(string $format, array $context = []): bool
47+
public function supportsEncoding(string $format, Context $context = null): bool
4748
{
4849
try {
4950
$this->getEncoder($format, $context);
@@ -77,7 +78,7 @@ public function needsNormalization(string $format, array $context = []): bool
7778
*
7879
* @throws RuntimeException if no encoder is found
7980
*/
80-
private function getEncoder(string $format, array $context): EncoderInterface
81+
private function getEncoder(string $format, Context $context): EncoderInterface
8182
{
8283
if (isset($this->encoderByFormat[$format])
8384
&& isset($this->encoders[$this->encoderByFormat[$format]])

src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Serializer\Encoder;
1313

14+
use Symfony\Component\Serializer\Context\Context;
15+
1416
/**
1517
* Adds the support of an extra $context parameter for the supportsDecoding method.
1618
*
@@ -20,8 +22,6 @@ interface ContextAwareDecoderInterface extends DecoderInterface
2022
{
2123
/**
2224
* {@inheritdoc}
23-
*
24-
* @param array $context options that decoders have access to
2525
*/
26-
public function supportsDecoding(string $format, array $context = []): bool;
26+
public function supportsDecoding(string $format, Context $context = null): bool;
2727
}

src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Serializer\Encoder;
1313

14+
use Symfony\Component\Serializer\Context\Context;
15+
1416
/**
1517
* Adds the support of an extra $context parameter for the supportsEncoding method.
1618
*
@@ -20,8 +22,6 @@ interface ContextAwareEncoderInterface extends EncoderInterface
2022
{
2123
/**
2224
* {@inheritdoc}
23-
*
24-
* @param array $context options that encoders have access to
2525
*/
26-
public function supportsEncoding(string $format, array $context = []): bool;
26+
public function supportsEncoding(string $format, Context $context = null): bool;
2727
}

src/Symfony/Component/Serializer/Encoder/CsvEncoder.php

Lines changed: 34 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Encoder;
1313

14-
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
14+
use Symfony\Component\Serializer\Context\Context;
1515
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1616

1717
/**
@@ -23,43 +23,25 @@
2323
class CsvEncoder implements EncoderInterface, DecoderInterface
2424
{
2525
public const FORMAT = 'csv';
26-
public const DELIMITER_KEY = 'csv_delimiter';
27-
public const ENCLOSURE_KEY = 'csv_enclosure';
28-
public const ESCAPE_CHAR_KEY = 'csv_escape_char';
29-
public const KEY_SEPARATOR_KEY = 'csv_key_separator';
30-
public const HEADERS_KEY = 'csv_headers';
31-
public const ESCAPE_FORMULAS_KEY = 'csv_escape_formulas';
32-
public const AS_COLLECTION_KEY = 'as_collection';
33-
public const NO_HEADERS_KEY = 'no_headers';
34-
public const END_OF_LINE = 'csv_end_of_line';
35-
public const OUTPUT_UTF8_BOM_KEY = 'output_utf8_bom';
3626

3727
private const UTF8_BOM = "\xEF\xBB\xBF";
3828

39-
private $formulasStartCharacters = ['=', '-', '+', '@'];
40-
private $defaultContext = [
41-
self::DELIMITER_KEY => ',',
42-
self::ENCLOSURE_KEY => '"',
43-
self::ESCAPE_CHAR_KEY => '',
44-
self::END_OF_LINE => "\n",
45-
self::ESCAPE_FORMULAS_KEY => false,
46-
self::HEADERS_KEY => [],
47-
self::KEY_SEPARATOR_KEY => '.',
48-
self::NO_HEADERS_KEY => false,
49-
self::AS_COLLECTION_KEY => true,
50-
self::OUTPUT_UTF8_BOM_KEY => false,
51-
];
52-
53-
public function __construct(array $defaultContext = [])
29+
private array $formulasStartCharacters = ['=', '-', '+', '@'];
30+
31+
private CsvEncoderOptions $defaultOptions;
32+
33+
public function __construct(Context $defaultContext = null)
5434
{
55-
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
35+
$this->defaultOptions = $defaultContext?->getOptions(CsvEncoderOptions::class) ?? new CsvEncoderOptions();
5636
}
5737

5838
/**
5939
* {@inheritdoc}
6040
*/
61-
public function encode(mixed $data, string $format, array $context = []): string
41+
public function encode(mixed $data, string $format, Context $context = null): string
6242
{
43+
$options = $this->getOptions($context);
44+
6345
$handle = fopen('php://temp,', 'w+');
6446

6547
if (!is_iterable($data)) {
@@ -79,37 +61,35 @@ public function encode(mixed $data, string $format, array $context = []): string
7961
}
8062
}
8163

82-
[$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom] = $this->getCsvOptions($context);
83-
8464
foreach ($data as &$value) {
8565
$flattened = [];
86-
$this->flatten($value, $flattened, $keySeparator, '', $escapeFormulas);
66+
$this->flatten($value, $flattened, $options->getKeySeparator(), '', $options->isEscapeFormulas());
8767
$value = $flattened;
8868
}
8969
unset($value);
9070

91-
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
71+
$headers = array_merge(array_values($options->getHeaders()), array_diff($this->extractHeaders($data), $options->getHeaders()));
9272

93-
if (!($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY])) {
94-
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
95-
if ("\n" !== ($context[self::END_OF_LINE] ?? $this->defaultContext[self::END_OF_LINE]) && 0 === fseek($handle, -1, \SEEK_CUR)) {
96-
fwrite($handle, $context[self::END_OF_LINE]);
73+
if ($options->isWithHeaders()) {
74+
fputcsv($handle, $headers, $options->getDelimiter(), $options->getEnclosure(), $options->getEscapeChar());
75+
if ("\n" !== $options->getEndOfLine() && 0 === fseek($handle, -1, \SEEK_CUR)) {
76+
fwrite($handle, $options->getEndOfLine());
9777
}
9878
}
9979

10080
$headers = array_fill_keys($headers, '');
10181
foreach ($data as $row) {
102-
fputcsv($handle, array_replace($headers, $row), $delimiter, $enclosure, $escapeChar);
103-
if ("\n" !== ($context[self::END_OF_LINE] ?? $this->defaultContext[self::END_OF_LINE]) && 0 === fseek($handle, -1, \SEEK_CUR)) {
104-
fwrite($handle, $context[self::END_OF_LINE]);
82+
fputcsv($handle, array_replace($headers, $row), $options->getDelimiter(), $options->getEnclosure(), $options->getEscapeChar());
83+
if ("\n" !== $options->getEndOfLine() && 0 === fseek($handle, -1, \SEEK_CUR)) {
84+
fwrite($handle, $options->getEndOfLine());
10585
}
10686
}
10787

10888
rewind($handle);
10989
$value = stream_get_contents($handle);
11090
fclose($handle);
11191

112-
if ($outputBom) {
92+
if ($options->isOutputUtf8Bom()) {
11393
if (!preg_match('//u', $value)) {
11494
throw new UnexpectedValueException('You are trying to add a UTF-8 BOM to a non UTF-8 text.');
11595
}
@@ -131,8 +111,10 @@ public function supportsEncoding(string $format): bool
131111
/**
132112
* {@inheritdoc}
133113
*/
134-
public function decode(string $data, string $format, array $context = []): mixed
114+
public function decode(string $data, string $format, Context $context = null): mixed
135115
{
116+
$options = $this->getOptions($context);
117+
136118
$handle = fopen('php://temp', 'r+');
137119
fwrite($handle, $data);
138120
rewind($handle);
@@ -146,22 +128,20 @@ public function decode(string $data, string $format, array $context = []): mixed
146128
$headerCount = [];
147129
$result = [];
148130

149-
[$delimiter, $enclosure, $escapeChar, $keySeparator, , , , $asCollection] = $this->getCsvOptions($context);
150-
151-
while (false !== ($cols = fgetcsv($handle, 0, $delimiter, $enclosure, $escapeChar))) {
131+
while (false !== ($cols = fgetcsv($handle, 0, $options->getDelimiter(), $options->getEnclosure(), $options->getEscapeChar()))) {
152132
$nbCols = \count($cols);
153133

154134
if (null === $headers) {
155135
$nbHeaders = $nbCols;
156136

157-
if ($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY]) {
137+
if (!$options->isWithHeaders()) {
158138
for ($i = 0; $i < $nbCols; ++$i) {
159139
$headers[] = [$i];
160140
}
161141
$headerCount = array_fill(0, $nbCols, 1);
162142
} else {
163143
foreach ($cols as $col) {
164-
$header = explode($keySeparator, $col);
144+
$header = explode($options->getKeySeparator(), $col);
165145
$headers[] = $header;
166146
$headerCount[] = \count($header);
167147
}
@@ -194,7 +174,7 @@ public function decode(string $data, string $format, array $context = []): mixed
194174
}
195175
fclose($handle);
196176

197-
if ($asCollection) {
177+
if ($options->isAsCollection()) {
198178
return $result;
199179
}
200180

@@ -214,6 +194,13 @@ public function supportsDecoding(string $format): bool
214194
return self::FORMAT === $format;
215195
}
216196

197+
private function getOptions(?Context $context): CsvEncoderOptions
198+
{
199+
$options = $context?->getOptions(CsvEncoderOptions::class);
200+
201+
return null !== $options ? $options->merge($this->defaultOptions) : $this->defaultOptions;
202+
}
203+
217204
/**
218205
* Flattens an array and generates keys including the path.
219206
*/
@@ -233,24 +220,6 @@ private function flatten(iterable $array, array &$result, string $keySeparator,
233220
}
234221
}
235222

236-
private function getCsvOptions(array $context): array
237-
{
238-
$delimiter = $context[self::DELIMITER_KEY] ?? $this->defaultContext[self::DELIMITER_KEY];
239-
$enclosure = $context[self::ENCLOSURE_KEY] ?? $this->defaultContext[self::ENCLOSURE_KEY];
240-
$escapeChar = $context[self::ESCAPE_CHAR_KEY] ?? $this->defaultContext[self::ESCAPE_CHAR_KEY];
241-
$keySeparator = $context[self::KEY_SEPARATOR_KEY] ?? $this->defaultContext[self::KEY_SEPARATOR_KEY];
242-
$headers = $context[self::HEADERS_KEY] ?? $this->defaultContext[self::HEADERS_KEY];
243-
$escapeFormulas = $context[self::ESCAPE_FORMULAS_KEY] ?? $this->defaultContext[self::ESCAPE_FORMULAS_KEY];
244-
$outputBom = $context[self::OUTPUT_UTF8_BOM_KEY] ?? $this->defaultContext[self::OUTPUT_UTF8_BOM_KEY];
245-
$asCollection = $context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY];
246-
247-
if (!\is_array($headers)) {
248-
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, get_debug_type($headers)));
249-
}
250-
251-
return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom, $asCollection];
252-
}
253-
254223
/**
255224
* @return string[]
256225
*/

0 commit comments

Comments
 (0)