Skip to content

Commit 84bc919

Browse files
committed
Improve documentation and CastToArray implementation
1 parent d0bb3b3 commit 84bc919

File tree

4 files changed

+342
-216
lines changed

4 files changed

+342
-216
lines changed

docs/9.0/reader/record-mapping.md

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
---
2+
layout: default
3+
title: Deserializing a Tabular Data
4+
---
5+
6+
# Mapping records to objects
7+
8+
<p class="message-notice">New in version <code>9.12.0</code></p>
9+
10+
## Converting an array to an object
11+
12+
If you prefer working with objects instead of typed arrays it is possible to map each record to
13+
a specified class. To do so a new `Serializer` class is introduced to expose a deserialization mechanism
14+
15+
The class exposes three (3) methods to ease `array` to `object` conversion:
16+
17+
- `Serializer::deserialize` which expect a single recrods as argument and returns on success an instance of the class.
18+
- `Serializer::deserializeAll` which expect a collection of records and returns a collection of class instances.
19+
- and the public static method `Serializer::map` which is a quick way to declare and converting a single record into an object.
20+
21+
```php
22+
use League\Csv\Serializer;
23+
24+
$record = [
25+
'date' => '2023-10-30',
26+
'temperature' => '-1.5',
27+
'place' => 'Berkeley',
28+
];
29+
30+
$weather = Serializer::map(Weather::class, $record);
31+
32+
// this is the same as writing the following
33+
$serializer = new Serializer(Weather::class, array_keys($record));
34+
$weather = $serializer->deserialize($record);
35+
36+
$collection = [$record];
37+
foreach ($serializer->deserializeAll($collection) as $weather) {
38+
// each $weather entry will be an instance of the Weather class;
39+
}
40+
```
41+
42+
If you are working with a class which implements the `TabularDataReader` interface you can use this functionality
43+
directly by calling the `TabularDataReader::map` method.
44+
45+
We can rewrite the last example as the following:
46+
47+
```php
48+
use League\Csv\Reader;
49+
50+
$csv = Reader::createFromString($document);
51+
$csv->setHeaderOffset(0);
52+
foreach ($csv->map($csv) as $weather) {
53+
// each $weather entry will be an instance of the Weather class;
54+
}
55+
```
56+
In the following sections we will explain the conversion and how you can control which field
57+
can be converter and how the conversion can be configured.
58+
59+
## Pre-requisite
60+
61+
The deserialization mechanism relies heavily on PHP's Reflection features and does not use the class constructor
62+
to perform the convertion. This means the object for which you are considering the conversion contains too much
63+
logic in its constructor, the current mechanism may either fail or produced unwanted results.
64+
The deserialization mechanism used works mainly with DTO or simple object without too many logics.
65+
66+
To work as intended the mechanism expects the following:
67+
68+
- the name of the class the array will be deserialized in;
69+
- information on how to convert cell value into object properties using dedicated attributes;
70+
71+
As an example if we assume we have the following CSV document:
72+
73+
```csv
74+
date,temperature,place
75+
2011-01-01,1,Galway
76+
2011-01-02,-1,Galway
77+
2011-01-03,0,Galway
78+
2011-01-01,6,Berkeley
79+
2011-01-02,8,Berkeley
80+
2011-01-03,5,Berkeley
81+
```
82+
83+
We can define a PHP DTO using the following class and the attributes.
84+
85+
```php
86+
<?php
87+
88+
use League\Csv\Serializer\Cell;
89+
use League\Csv\Serializer\Record;
90+
91+
#[Record]
92+
final readonly class Weather
93+
{
94+
public function __construct(
95+
public float $temperature,
96+
public Place $place,
97+
#[Cell(castArguments: ['format' => '!Y-m-d'])]
98+
public DateTimeImmutable $date;
99+
) {
100+
}
101+
}
102+
103+
enum Place
104+
{
105+
case Berkeley;
106+
case Galway;
107+
}
108+
```
109+
110+
To get instances of your object, you now can call the `Serializer::deserialize` method as show below:
111+
112+
```php
113+
use League\Csv\Reader;
114+
use League\Csv\Serializer
115+
116+
$csv = Reader::createFromString($document);
117+
$csv->setHeaderOffset(0);
118+
$serializer = new Serializer(Weather::class, $csv->header());
119+
foreach ($serializer->deserializeAll($csv) as $weather) {
120+
// each $weather entry will be an instance of the Weather class;
121+
}
122+
```
123+
124+
## Defining the mapping rules
125+
126+
The `Record` attribute is responsible for converting array values into the appropriate instance
127+
properties. This means that in order to use the `Record` attribute you are required to have
128+
an associative `array`.
129+
130+
The deserialization engine is able to cast the value into
131+
the appropriate type if it is a `string` or `null` and the object public properties ares typed with
132+
133+
- `null`
134+
- `mixed`
135+
- a scalar type (support for `true` and `false` type is also present)
136+
- any `Enum` object (backed or not)
137+
- `DateTime`, `DateTimeImmuntable` and any class that extends those two classes.
138+
- an `array`
139+
140+
When converting to a date object you can fine tune the conversion by optionally specifying the date
141+
format and timezone. You can do so using the `Cell` attribute. This attribute will override the automatic
142+
resolution and enable fine-tuning type casting on the property level.
143+
144+
```php
145+
use League\Csv\Serializer;
146+
use Carbon\CarbonImmutable;
147+
148+
#[Serializer\Cell(
149+
offset:'date',
150+
cast:Serializer\CastToDate::class,
151+
castArguments: [
152+
'format' => '!Y-m-d',
153+
'timezone' => 'Africa/Nairobi'
154+
])
155+
]
156+
public CarbonImmutable $observedOn;
157+
```
158+
159+
The above rule can be translated in plain english like this:
160+
161+
> convert the value of the associative array named `date` into a `CarbonImmutable` object
162+
> using the date format `!Y-m-d` and the `Africa/Nairobi` timezone. Once created,
163+
> inject the date instance into the `observedOn` property of the class.
164+
165+
The `Cell` attribute differs from the `Record` attribute as it can be used:
166+
167+
- on class properties and methods (public, protected or private).
168+
- with `array` as list (you are required, in this case, to specify the `offset` argument).
169+
170+
The `Cell` attribute can take up to three (3) arguments which are all optional:
171+
172+
- The `offset` argument which tell the engine which cell to use via its numeric or name offset. If not present
173+
the property name or the name of the first argument of the `setter` method will be used. In such case,
174+
you are required to specify the property names information.
175+
- The `cast` argument which accept the name of a class implementing the `TypeCasting` interface and responsible
176+
for type casting the cell value.
177+
- The `castArguments` which enable controlling typecasting by providing extra arguments to the `TypeCasting` class constructor
178+
179+
In any cases, if type casting fails, an exception will be thrown.
180+
181+
## Type casting the record value
182+
183+
The library comes bundles with four (4) type casting classes which relies on the property type information. All the
184+
built-in methods support the `nullable` type. They will return `null` if the cell value is the empty string or `null`
185+
only if the type is considered to be `nullable` otherwise they will throw an exception.
186+
All classes are defined under the `League\Csv\Serializer` namespace.
187+
188+
### CastToBuiltInType
189+
190+
Converts the array value to a scalar type or `null` depending on the property type information. This class has no
191+
specific configuration but will work with all the scalar type, the `true`, `null` and `false` value type as well as
192+
with the `mixed` type. Type casting is done using the `filter_var` functionality of the `ext-filter` extension.
193+
194+
### CastToEnum
195+
196+
Convert the array value to a PHP `Enum` it supported both "real" and backed enumeration. No configuration is needed
197+
if the value is not recognized an exception will be thrown.
198+
199+
### CastToDate
200+
201+
Converts the cell value into a PHP `DateTimeInterface` implementing object. You can optionally specify the date format and its timezone if needed.
202+
203+
### CastToArray
204+
205+
Converts the value into a PHP `array`. You are required to specify what type of conversion you desired (`list`, `json` or `csv`).
206+
207+
The following are example for each type:
208+
209+
```php
210+
$array['field1'] = "1,2,3,4"; //the string contains only a separator (type list)
211+
$arrat['field2'] = '"1","2","3","4"'; //the string contains delimiter and enclosure (type csv)
212+
$arrat['field3'] = '{"foo":"bar"}'; //the string is a json string (type json)
213+
```
214+
215+
in case of
216+
217+
- the `list` type you can configure the `delimiter`, by default it is the `,`;
218+
- the `csv` type you can configure the `delimiter` and the `enclosure`, by default they are respectively `,` and `"`;
219+
- the `json` type you can configure the `jsonDepth` and the `jsonFlags` options just like when using the `json_decode` arguments, the default are the same;
220+
221+
Here's a example for casting a string via the `json` type.
222+
223+
```php
224+
use League\Csv\Serializer;
225+
226+
#[Serializer\Cell(
227+
cast:Serializer\CastToArray::class,
228+
castArguments: [
229+
'type' => 'json',
230+
'jsonFlags' => JSON_BIGINT_AS_STRING
231+
])
232+
]
233+
public array $data;
234+
```
235+
236+
In the above example, the array has a JSON value associated with the key `data` and the `Serializer` will convert the
237+
JSON string into an `array` and use the `JSON_BIGINT_AS_STRING` option of the `json_decode` function.
238+
239+
### Creating your own TypeCasting class
240+
241+
You can also provide your own class to typecast the array value according to your own rules. To do so, first,
242+
specify your casting with the attribute:
243+
244+
```php
245+
use League\Csv\Serializer;
246+
#[Serializer\Cell(
247+
offset: 'rating',
248+
cast: IntegerRangeCasting::class,
249+
castArguments: ['min' => 0, 'max' => 5, 'default' => 2]
250+
)]
251+
private int $ratingScore;
252+
```
253+
254+
The `IntegerRangeCasting` will convert cell value and return data between `0` and `5` and default to `2` if
255+
the value is wrong or invalid. To allow your object to cast the cell value to your liking it needs to
256+
implement the `TypeCasting` interface. To do so, you must define a `toVariable` method that will return
257+
the correct value once converted.
258+
259+
```php
260+
use League\Csv\Serializer\TypeCasting;
261+
use League\Csv\Serializer\TypeCastingFailed;
262+
263+
/**
264+
* @implements TypeCasting<int|null>
265+
*/
266+
readonly class IntegerRangeCasting implements TypeCasting
267+
{
268+
public function __construct(
269+
private int $min,
270+
private int $max,
271+
private int $default,
272+
) {
273+
if ($max < $min) {
274+
throw new LogicException('The maximum value can not be lesser than the minimum value.');
275+
}
276+
}
277+
278+
public function toVariable(?string $value, string $type): ?int
279+
{
280+
// if the property is declared as nullable we exist early
281+
if (in_array($value, ['', null], true) && str_starts_with($type, '?')) {
282+
return null;
283+
}
284+
285+
//the type casting class must only work with property declared as integer
286+
if ('int' !== ltrim($type, '?')) {
287+
throw new TypeCastingFailed('The class '. self::class . ' can only work with integer typed property.');
288+
}
289+
290+
return filter_var(
291+
$value,
292+
FILTER_VALIDATE_INT,
293+
['options' => ['min' => $this->min, 'max' => $this->max, 'default' => $this->default]]
294+
);
295+
}
296+
}
297+
```
298+
299+
As you have probably noticed, the class constructor arguments are given to the `Cell` attribute via the
300+
`castArguments` which can provide more fine-grained behaviour.

0 commit comments

Comments
 (0)