Skip to content

Commit 219e7d9

Browse files
committed
Improve Denormalizer internal codebase
1 parent 8813656 commit 219e7d9

File tree

7 files changed

+182
-171
lines changed

7 files changed

+182
-171
lines changed

docs/9.0/reader/record-mapping.md

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use League\Csv\Reader;
1717

1818
$csv = Reader::createFromString($document);
1919
$csv->setHeaderOffset(0);
20-
foreach ($csv->getObjects(Weather::class) as $weather) {
21-
// each $weather entry will be an instance of the Weather class;
20+
foreach ($csv->getObjects(ClimaticRecord::class) as $weather) {
21+
// each $weather entry will be an instance of the ClimaticRecord class;
2222
}
2323
```
2424

@@ -43,38 +43,44 @@ As an example if we assume we have the following CSV document:
4343

4444
```csv
4545
date,temperature,place
46-
2011-01-01,,Galway
47-
2011-01-02,-1,Galway
48-
2011-01-03,0,Galway
49-
2011-01-01,6,Berkeley
50-
2011-01-02,8,Berkeley
51-
2011-01-03,5,Berkeley
46+
2011-01-01,,Abidjan
47+
2011-01-02,24,Abidjan
48+
2011-01-03,17,Abidjan
49+
2011-01-01,18,Yamoussoukro
50+
2011-01-02,23,Yamoussoukro
51+
2011-01-03,21,Yamoussoukro
5252
```
5353

5454
We can define a PHP DTO using the following properties.
5555

5656
```php
5757
<?php
5858

59-
final class Weather
59+
final class ClimaticRecord
6060
{
61+
private ?DateTimeImmutable $date = null,
62+
6163
public function __construct(
62-
public readonly ?float $temperature,
6364
public readonly Place $place,
64-
private DateTimeImmutable $date,
65+
public readonly ?float $temperature,
6566
) {
6667
}
6768

6869
public function setDate(string $date): void
6970
{
7071
$this->date = new DateTimeImmutable($date, new DateTimeZone('Africa/Abidjan'));
7172
}
73+
74+
public function getDate(): DateTimeImmutable
75+
{
76+
return $this->date;
77+
}
7278
}
7379

7480
enum Place
7581
{
76-
case Berkeley;
77-
case Galway;
82+
case Yamoussoukro;
83+
case Abidjan;
7884
}
7985
```
8086

@@ -86,8 +92,8 @@ use League\Csv\Reader;
8692

8793
$csv = Reader::createFromString($document);
8894
$csv->setHeaderOffset(0);
89-
foreach ($csv->getObjects(Weather::class) {
90-
// each $weather entry will be an instance of the Weather class;
95+
foreach ($csv->getObjects(ClimaticRecord::class) as $instance) {
96+
// each $instance entry will be an instance of the ClimaticRecord class;
9197
}
9298
```
9399

@@ -170,15 +176,15 @@ use League\Csv\Serializer\Denormalizer;
170176

171177
$csv = Reader::createFromString($document);
172178
$csv->setHeaderOffset(0);
173-
foreach ($csv->getObjects(Weather::class) {
179+
foreach ($csv->getObjects(ClimaticRecord::class) {
174180
// the first record contains an empty string for temperature
175181
// it is converted into the null value and handle by the
176182
// default conversion type casting;
177183
}
178184

179185
Denormalizer::disallowEmptyStringAsNull();
180186

181-
foreach ($csv->getObjects(Weather::class) {
187+
foreach ($csv->getObjects(ClimaticRecord::class) {
182188
// a TypeCastingFailed exception is thrown because we
183189
// can not convert the empty string into a valid
184190
// temperature property value
@@ -235,7 +241,7 @@ use League\Csv\Serializer\Cell;
235241
#[Cell(
236242
offset:1,
237243
cast:Serializer\CastToEnum::class,
238-
castArguments: ['default' => 'Galway', 'enum' => Place::class]
244+
castArguments: ['default' => 'Abidjan', 'enum' => Place::class]
239245
)]
240246
public function setPlace(mixed $place): void
241247
{
@@ -244,7 +250,7 @@ public function setPlace(mixed $place): void
244250
```
245251

246252
> convert the value of the array whose offset is `1` into a `Place` Enum
247-
> if the value is null resolve the string `Galway` to `Place::Galway`. Once created,
253+
> if the value is null resolve the string `Abidjan` to `Place::Abidjan`. Once created,
248254
> call the method `setPlace` with the created `Place` enum filling the `$place` argument.
249255
250256
<p class="notice">Using this class with a <code>mixed</code> type without providing the <code>enum</code> parameter will trigger an exception.</p>
@@ -485,40 +491,52 @@ final class CastToNaira implements TypeCasting
485491
<p class="message-info">While the built-in <code>TypeCasting</code> classes do not support Intersection Type, your own
486492
implementing class can support them via inspection of the <code>$reflectionProperty</code> argument.</p>
487493

488-
## Using the feature outside the TabularDataReader
494+
## Using the feature without a TabularDataReader
489495

490496
The feature can be used outside the package usage via the `Denormalizer` class.
491497

492498
The class exposes four (4) methods to ease `array` to `object` conversion:
493499

494-
- `Denormalizer::denormalizeAll` and `Denormalizer::assignAll` which convert a collection of records into a collection of instances of a specified class.
495-
- `Denormalizer::denormalize` and `Denormalizer::assign` which convert a single record into a new instance of the specified class.
500+
- `Denormalizer::denormalizeAll` and `Denormalizer::assignAll` to convert a collection of records into a collection of instances of a specified class.
501+
- `Denormalizer::denormalize` and `Denormalizer::assign` to convert a single record into a new instance of the specified class.
502+
503+
Since we are not leveraging the `TabularDataReader` we must explicitly tell the class how to link array keys and class properties.
504+
Once instantiated you can reuse the instance to independently convert a single record or a collection of `array`.
496505

497506
```php
498507
use League\Csv\Serializer\Denormalizer;
499508

500509
$record = [
501510
'date' => '2023-10-30',
502511
'temperature' => '-1.5',
503-
'place' => 'Berkeley',
512+
'place' => 'Yamoussoukro',
504513
];
505514

506515
//a complete collection of records as shown below
507516
$collection = [$record];
508517
//we first instantiate the denormalizer
509-
$denormalizer = new Denormalizer(Weather::class, ['date', 'temperature', 'place']);
518+
//and we provide the information to map record key to the class properties
519+
$denormalizer = new Denormalizer(ClimaticRecord::class, ['date', 'temperature', 'place']);
510520
$weather = $denormalizer->denormalize($record); //we convert 1 record into 1 instance
511521

512522
foreach ($denormalizer->denormalizeAll($collection) as $weather) {
513-
// each $weather entry will be an instance of the Weather class;
523+
// each $weather entry will be an instance of the ClimaticRecord class;
514524
}
525+
```
526+
527+
To complete the feature 2 static methods are provided if you only need to denormalization once,
528+
`Denormalizer::assign` will automatically use the `array` keys as property names. Whereas,
529+
you still need to give the property list to `Denormalizer::assignAll` to allow the class
530+
to work with any given iterable structure of `array`.
515531

532+
```php
533+
<?php
516534
// you can use the alternate syntactic sugar methods
517535
// if you only need to use the mechanism once
518-
$weather = Denormalizer::assign(Weather::class, $record);
536+
$weather = Denormalizer::assign(ClimaticRecord::class, $record);
519537

520-
foreach (Denormalizer::assignAll(Weather::class, $collection, ['date', 'temperature', 'place']) as $weather) {
521-
// each $weather entry will be an instance of the Weather class;
538+
foreach (Denormalizer::assignAll(ClimaticRecord::class, $collection, ['date', 'temperature', 'place']) as $weather) {
539+
// each $weather entry will be an instance of the ClimaticRecord class;
522540
}
523541
```
524542

docs/9.0/reader/tabular-data-reader.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,14 @@ returned iterator. Added column will only contain the `null` value.
121121

122122
<p class="message-notice">Added in version <code>9.12.0</code> for <code>Reader</code> and <code>ResultSet</code>.</p>
123123

124-
If you prefer working with objects instead of typed arrays it is possible to convert each record using
125-
the `getObjects` method. This method will convert each array record into your specified object.
126-
127-
To get instances of your object, you are required to call the `getObjects` method as show below:
124+
If you prefer working with objects instead of arrays it is possible to deserialize your CSV document using
125+
the `getObjects` method. This method will convert each CSV record into your specified class instances.
128126

129127
```php
130128
$csv = Reader::createFromString($document);
131129
$csv->setHeaderOffset(0);
132-
foreach ($csv->getObjects(Weather::class) as $weather) {
133-
// each $weather entry will be an instance of the Weather class;
130+
foreach ($csv->getObjects(ClimaticRecord::class) as $instance) {
131+
// each $instance entry will be an instance of the Weather class;
134132
}
135133
```
136134

src/Serializer/CastToEnum.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@
2626
*/
2727
class CastToEnum implements TypeCasting
2828
{
29-
/** @var class-string */
29+
/** @var class-string<UnitEnum|BackedEnum> */
3030
private readonly string $class;
3131
private readonly bool $isNullable;
3232
private readonly BackedEnum|UnitEnum|null $default;
3333

3434
/**
35-
* @param ?class-string $enum
35+
* @param ?class-string<UnitEnum|BackedEnum> $enum
3636
*
3737
* @throws MappingFailed
3838
*/
@@ -42,7 +42,7 @@ public function __construct(
4242
?string $enum = null,
4343
) {
4444
[$type, $reflection, $this->isNullable] = $this->init($reflectionProperty);
45-
/** @var class-string $class */
45+
/** @var class-string<UnitEnum|BackedEnum> $class */
4646
$class = $reflection->getName();
4747
if (Type::Mixed->equals($type)) {
4848
if (null === $enum || !enum_exists($enum)) {
@@ -85,7 +85,7 @@ private function cast(string $value): BackedEnum|UnitEnum
8585

8686
$backedValue = 'int' === $enum->getBackingType()?->getName() ? filter_var($value, Type::Int->filterFlag()) : $value;
8787

88-
return $this->class::from($backedValue);
88+
return $this->class::from($backedValue); /* @phpstan-ignore-line */
8989
} catch (Throwable $exception) {
9090
throw throw TypeCastingFailed::dueToInvalidValue($value, $this->class, $exception);
9191
}

src/Serializer/ClosureCasting.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static function register(string $type, Closure $closure): void
7777
self::$casters[$type] = match (true) {
7878
class_exists($type),
7979
interface_exists($type),
80-
Type::tryFrom($type)?->isBuiltIn() ?? false => $closure,
80+
Type::tryFrom($type) instanceof Type => $closure,
8181
default => throw new MappingFailed('The `'.$type.'` could not be register.'),
8282
};
8383
}

0 commit comments

Comments
 (0)