|
| 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