Skip to content

Commit 4a72f65

Browse files
committed
handle csv deserialization
1 parent d76050d commit 4a72f65

27 files changed

+793
-418
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\SerDes\Internal\Deserialize\Csv;
13+
14+
use Symfony\Component\SerDes\Exception\InvalidResourceException;
15+
16+
/**
17+
* @author Mathias Arlaud <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
final class CsvDecoder
22+
{
23+
private const UTF8_BOM = "\xEF\xBB\xBF";
24+
25+
/**
26+
* @param resource $resource
27+
* @param array<string, mixed> $context
28+
*
29+
* @return \Iterator<list<mixed>>
30+
*
31+
* @throws InvalidResourceException
32+
*/
33+
public function decode(mixed $resource, array $context): \Iterator
34+
{
35+
try {
36+
/** @var string $content */
37+
$content = stream_get_contents($resource, \strlen(self::UTF8_BOM));
38+
} catch (\Throwable) {
39+
throw new InvalidResourceException($resource);
40+
}
41+
42+
if (self::UTF8_BOM !== $content) {
43+
rewind($resource);
44+
}
45+
46+
try {
47+
while (false !== ($row = fgetcsv(
48+
$resource,
49+
separator: $context['csv_separator'] ?? ',',
50+
enclosure: $context['csv_enclosure'] ?? '"',
51+
escape: $context['csv_escape_char'] ?? '\\',
52+
))) {
53+
yield $row;
54+
}
55+
} catch (\Throwable) {
56+
throw new InvalidResourceException($resource);
57+
}
58+
}
59+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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\SerDes\Internal\Deserialize\Csv;
13+
14+
use Symfony\Component\SerDes\Exception\InvalidArgumentException;
15+
use Symfony\Component\SerDes\Internal\Deserialize\Deserializer;
16+
use Symfony\Component\SerDes\Internal\Type;
17+
use Symfony\Component\SerDes\Internal\UnionType;
18+
use Symfony\Component\SerDes\Type\ReflectionTypeExtractor;
19+
20+
/**
21+
* @author Mathias Arlaud <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
final class CsvDeserializer extends Deserializer
26+
{
27+
public function __construct(
28+
ReflectionTypeExtractor $reflectionTypeExtractor,
29+
private readonly CsvDecoder $decoder,
30+
private readonly bool $lazy,
31+
) {
32+
parent::__construct($reflectionTypeExtractor);
33+
}
34+
35+
public function deserialize(mixed $resource, Type|UnionType $type, array $context): mixed
36+
{
37+
if ($type instanceof UnionType || !$type->isList()) {
38+
throw new InvalidArgumentException(sprintf('Expecting type to be a list, but got "%s".', (string) $type));
39+
}
40+
41+
$rows = $this->decoder->decode($resource, $context);
42+
43+
$context['csv_headers'] = $rows->current();
44+
$context['csv_depth'] = 0;
45+
46+
$rowsIterator = new \LimitIterator($rows, 1);
47+
$collectionValueType = $type->collectionValueType();
48+
49+
if ($this->lazy) {
50+
return $this->lazyDeserialize($rowsIterator, $collectionValueType, $context);
51+
}
52+
53+
return array_map(
54+
fn (array $r): mixed => $this->doDeserialize($r, $collectionValueType, $context),
55+
iterator_to_array($rowsIterator, preserve_keys: false),
56+
);
57+
}
58+
59+
protected function deserializeScalar(mixed $data, Type $type, array $context): mixed
60+
{
61+
if (0 === $context['csv_depth']) {
62+
$data = reset($data);
63+
}
64+
65+
if ('' === $data && $type->isNullable()) {
66+
return null;
67+
}
68+
69+
return $data;
70+
}
71+
72+
protected function deserializeList(mixed $data, Type $type, array $context): \Iterator
73+
{
74+
if (!\is_array($data)) {
75+
throw $this->tooDeepException();
76+
}
77+
78+
$collectionValueType = $type->collectionValueType();
79+
++$context['csv_depth'];
80+
81+
foreach ($data as $value) {
82+
yield $this->doDeserialize($value, $collectionValueType, $context);
83+
}
84+
}
85+
86+
protected function deserializeDict(mixed $data, Type $type, array $context): \Iterator
87+
{
88+
if (!\is_array($data)) {
89+
throw $this->tooDeepException();
90+
}
91+
92+
$collectionValueType = $type->collectionValueType();
93+
++$context['csv_depth'];
94+
95+
foreach ($data as $index => $value) {
96+
yield $context['csv_headers'][$index] => $this->doDeserialize($value, $collectionValueType, $context);
97+
}
98+
}
99+
100+
protected function deserializeObjectProperties(mixed $data, Type $type, array $context): \Iterator
101+
{
102+
if (!\is_array($data)) {
103+
throw $this->tooDeepException();
104+
}
105+
106+
foreach ($data as $index => $value) {
107+
if ('' === $value) {
108+
continue;
109+
}
110+
111+
yield $context['csv_headers'][$index] => $value;
112+
}
113+
}
114+
115+
protected function deserializeMixed(mixed $data, array $context): mixed
116+
{
117+
if (0 === $context['csv_depth']) {
118+
return reset($data);
119+
}
120+
121+
return $data;
122+
}
123+
124+
protected function propertyValueCallable(Type|UnionType $type, mixed $data, mixed $value, array $context): callable
125+
{
126+
++$context['csv_depth'];
127+
128+
return fn () => $this->doDeserialize($value, $type, $context);
129+
}
130+
131+
/**
132+
* @param \Iterator<list<mixed>> $rows
133+
* @param array<string, mixed> $context
134+
*
135+
* @return \Iterator<mixed>
136+
*/
137+
private function lazyDeserialize(\Iterator $rows, Type|UnionType $collectionValueType, array $context): \Iterator
138+
{
139+
foreach ($rows as $row) {
140+
yield $this->doDeserialize($row, $collectionValueType, $context);
141+
}
142+
}
143+
144+
private function tooDeepException(): \Exception
145+
{
146+
return new InvalidArgumentException('Expecting type with at most two dimensions.');
147+
}
148+
}

src/Symfony/Component/SerDes/Internal/Deserialize/DecoderFactory.php

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)