Skip to content

Commit 5f8a731

Browse files
committed
handle csv deserialization
1 parent d76050d commit 5f8a731

22 files changed

+735
-401
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\RuntimeException;
15+
16+
/**
17+
* @author Mathias Arlaud <[email protected]>
18+
*
19+
* @internal
20+
*/
21+
final class CsvDecoder
22+
{
23+
/**
24+
* @param resource $resource
25+
* @param array<string, mixed> $context
26+
*
27+
* @return \Iterator<list<mixed>>
28+
*/
29+
public function decode(mixed $resource, array $context): \Iterator
30+
{
31+
try {
32+
while (false !== ($row = fgetcsv(
33+
$resource,
34+
separator: $context['csv_separator'] ?? ',',
35+
enclosure: $context['csv_enclosure'] ?? '"',
36+
escape: $context['csv_escape_char'] ?? '\\',
37+
))) {
38+
yield $row;
39+
}
40+
} catch (\Throwable) {
41+
throw new RuntimeException('Cannot read CSV resource.');
42+
}
43+
}
44+
}
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.

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

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
* @author Mathias Arlaud <[email protected]>
2323
*
2424
* @internal
25-
*
26-
* @template T of mixed
2725
*/
2826
abstract class Deserializer
2927
{
@@ -43,62 +41,53 @@ public function __construct(
4341
}
4442

4543
/**
46-
* @param T $data
47-
* @param array<string, mixed> $context
48-
*/
49-
abstract protected function deserializeScalar(mixed $data, Type $type, array $context): mixed;
50-
51-
/**
52-
* @param T $data
5344
* @param array<string, mixed> $context
5445
*/
55-
abstract protected function deserializeEnum(mixed $data, Type $type, array $context): mixed;
46+
abstract protected function deserializeScalar(mixed $dataOrResource, Type $type, array $context): mixed;
5647

5748
/**
58-
* @param T $data
5949
* @param array<string, mixed> $context
6050
*
6151
* @return \Iterator<mixed>|null
6252
*/
63-
abstract protected function deserializeList(mixed $data, Type $type, array $context): ?\Iterator;
53+
abstract protected function deserializeList(mixed $dataOrResource, Type $type, array $context): ?\Iterator;
6454

6555
/**
66-
* @param T $data
6756
* @param array<string, mixed> $context
6857
*
6958
* @return \Iterator<string, mixed>|null
7059
*/
71-
abstract protected function deserializeDict(mixed $data, Type $type, array $context): ?\Iterator;
60+
abstract protected function deserializeDict(mixed $dataOrResource, Type $type, array $context): ?\Iterator;
7261

7362
/**
74-
* @param T $data
7563
* @param array<string, mixed> $context
7664
*
7765
* @return \Iterator<string, mixed>|array<string, mixed>|null
7866
*/
79-
abstract protected function deserializeObjectProperties(mixed $data, Type $type, array $context): \Iterator|array|null;
67+
abstract protected function deserializeObjectProperties(mixed $dataOrResource, Type $type, array $context): \Iterator|array|null;
8068

8169
/**
82-
* @param T $data
8370
* @param array<string, mixed> $context
84-
*
85-
* @return T
8671
*/
87-
abstract protected function deserializeMixed(mixed $data, Type $type, array $context): mixed;
72+
abstract protected function deserializeMixed(mixed $dataOrResource, array $context): mixed;
8873

8974
/**
90-
* @param T $data
9175
* @param array<string, mixed> $context
9276
*
9377
* @return callable(): mixed
9478
*/
95-
abstract protected function propertyValueCallable(Type|UnionType $type, mixed $data, mixed $value, array $context): callable;
79+
abstract protected function propertyValueCallable(Type|UnionType $type, mixed $dataOrResource, mixed $value, array $context): callable;
9680

9781
/**
98-
* @param T $data
82+
* @param resource $resource
9983
* @param array<string, mixed> $context
10084
*/
101-
final public function deserialize(mixed $data, Type|UnionType $type, array $context): mixed
85+
abstract public function deserialize(mixed $resource, Type|UnionType $type, array $context): mixed;
86+
87+
/**
88+
* @param array<string, mixed> $context
89+
*/
90+
protected function doDeserialize(mixed $dataOrResource, Type|UnionType $type, array $context): mixed
10291
{
10392
if ($type instanceof UnionType) {
10493
if (!isset($context['union_selector'][$typeString = (string) $type])) {
@@ -110,7 +99,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
11099
}
111100

112101
if ($type->isScalar()) {
113-
$scalar = $this->deserializeScalar($data, $type, $context);
102+
$scalar = $this->deserializeScalar($dataOrResource, $type, $context);
114103

115104
if (null === $scalar) {
116105
if (!$type->isNullable()) {
@@ -134,7 +123,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
134123
}
135124

136125
if ($type->isEnum()) {
137-
$enum = $this->deserializeEnum($data, $type, $context);
126+
$enum = $this->deserializeScalar($dataOrResource, $type, $context);
138127

139128
if (null === $enum) {
140129
if (!$type->isNullable()) {
@@ -152,13 +141,11 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
152141
}
153142

154143
if ($type->isCollection()) {
155-
if ($type->isList()) {
156-
$collection = $this->deserializeList($data, $type, $context);
157-
} elseif ($type->isDict()) {
158-
$collection = $this->deserializeDict($data, $type, $context);
159-
} else {
160-
$collection = $this->deserializeMixed($data, $type, $context);
161-
}
144+
$collection = match (true) {
145+
$type->isList() => $this->deserializeList($dataOrResource, $type, $context),
146+
$type->isDict() => $this->deserializeDict($dataOrResource, $type, $context),
147+
default => $this->deserializeMixed($dataOrResource, $context),
148+
};
162149

163150
if (null === $collection) {
164151
if (!$type->isNullable()) {
@@ -174,15 +161,15 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
174161
if ($type->isObject()) {
175162
if (!$type->hasClass()) {
176163
$object = new \stdClass();
177-
foreach ($this->deserializeMixed($data, $type, $context) as $property => $value) {
164+
foreach ($this->deserializeMixed($dataOrResource, $context) as $property => $value) {
178165
$object->{$property} = $value;
179166
}
180167

181168
return $object;
182169
}
183170

184171
$className = $type->className();
185-
$objectProperties = $this->deserializeObjectProperties($data, $type, $context);
172+
$objectProperties = $this->deserializeObjectProperties($dataOrResource, $type, $context);
186173

187174
if (null === $objectProperties) {
188175
if (!$type->isNullable()) {
@@ -215,7 +202,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
215202
$hookResult = $hook(
216203
$reflection,
217204
$name,
218-
fn (string $type, array $context) => $this->propertyValueCallable(self::$cache['type'][$type] ??= TypeFactory::createFromString($type), $data, $value, $context)(),
205+
fn (string $type, array $context) => $this->propertyValueCallable(self::$cache['type'][$type] ??= TypeFactory::createFromString($type), $dataOrResource, $value, $context)(),
219206
$context,
220207
);
221208

@@ -241,7 +228,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
241228

242229
self::$cache['property_type'][$identifier] ??= TypeFactory::createFromString($this->reflectionTypeExtractor->extractFromProperty($reflection->getProperty($name)));
243230

244-
$valueCallables[$name] = $this->propertyValueCallable(self::$cache['property_type'][$identifier], $data, $value, $context);
231+
$valueCallables[$name] = $this->propertyValueCallable(self::$cache['property_type'][$identifier], $dataOrResource, $value, $context);
245232
}
246233

247234
if (isset($context['instantiator'])) {
@@ -267,6 +254,6 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
267254
return $object;
268255
}
269256

270-
return $this->deserializeMixed($data, $type, $context);
257+
return $this->deserializeMixed($dataOrResource, $context);
271258
}
272259
}

0 commit comments

Comments
 (0)