23
23
* @author Mathias Arlaud <[email protected] >
24
24
*
25
25
* @internal
26
+ *
27
+ * @template T of mixed
26
28
*/
27
- class Deserializer
29
+ abstract class Deserializer
28
30
{
29
31
/**
30
32
* @var array<string, array<string, mixed>>
@@ -42,221 +44,202 @@ public function __construct(
42
44
}
43
45
44
46
/**
47
+ * @param T $data
45
48
* @param array<string, mixed> $context
46
49
*/
47
- public function deserialize (mixed $ data , Type |UnionType $ type , array $ context ): mixed
48
- {
49
- if ($ type instanceof UnionType) {
50
- if (!isset ($ context ['union_selector ' ][$ typeString = (string ) $ type ])) {
51
- throw new UnexpectedValueException (sprintf ('Cannot guess type to use for "%s", you may specify a type in "$context[ \'union_selector \'][ \'%1$s \']". ' , (string ) $ type ));
52
- }
53
-
54
- /** @var Type $type */
55
- $ type = (self ::$ cache ['type ' ][$ typeString ] ??= TypeFactory::createFromString ($ context ['union_selector ' ][$ typeString ]));
56
- }
57
-
58
- $ result = match (true ) {
59
- $ type ->isScalar () => $ this ->deserializeScalar ($ data , $ type , $ context ),
60
- $ type ->isCollection () => $ this ->deserializeCollection ($ data , $ type , $ context ),
61
- $ type ->isEnum () => $ this ->deserializeEnum ($ data , $ type , $ context ),
62
- $ type ->isObject () => $ this ->deserializeObject ($ data , $ type , $ context ),
63
-
64
- default => throw new UnsupportedTypeException ($ type ),
65
- };
66
-
67
- if (null === $ result && !$ type ->isNullable ()) {
68
- throw new UnexpectedValueException (sprintf ('Unexpected "null" value for "%s" type. ' , (string ) $ type ));
69
- }
70
-
71
- return $ result ;
72
- }
50
+ abstract protected function deserializeScalar (mixed $ data , Type $ type , array $ context ): mixed ;
73
51
74
52
/**
53
+ * @param T $data
75
54
* @param array<string, mixed> $context
76
55
*/
77
- protected function deserializeScalar (mixed $ data , Type $ type , array $ context ): int |string |bool |float |null
78
- {
79
- if (null === $ data ) {
80
- return null ;
81
- }
82
-
83
- try {
84
- return match ($ type ->name ()) {
85
- 'int ' => (int ) $ data ,
86
- 'float ' => (float ) $ data ,
87
- 'string ' => (string ) $ data ,
88
- 'bool ' => (bool ) $ data ,
89
- default => throw new LogicException (sprintf ('Unhandled "%s" scalar cast ' , $ type ->name ())),
90
- };
91
- } catch (\Throwable ) {
92
- throw new UnexpectedValueException (sprintf ('Cannot cast "%s" to "%s" ' , get_debug_type ($ data ), (string ) $ type ));
93
- }
94
- }
56
+ abstract protected function deserializeEnum (mixed $ data , Type $ type , array $ context ): mixed ;
95
57
96
58
/**
59
+ * @param T $data
97
60
* @param array<string, mixed> $context
98
61
*
99
- * @return \Iterator<mixed>|\Iterator<string, mixed>|list<mixed>|array<string, mixed>| null
62
+ * @return \Iterator<mixed>|null
100
63
*/
101
- protected function deserializeCollection (mixed $ data , Type $ type , array $ context ): \Iterator |array |null
102
- {
103
- if (null === $ data ) {
104
- return null ;
105
- }
106
-
107
- $ data = $ this ->deserializeCollectionItems ($ data , $ type ->collectionValueType (), $ context );
64
+ abstract protected function deserializeList (mixed $ data , Type $ type , array $ context ): ?\Iterator ;
108
65
109
- return $ type ->isIterable () ? $ data : iterator_to_array ($ data );
110
- }
66
+ /**
67
+ * @param T $data
68
+ * @param array<string, mixed> $context
69
+ *
70
+ * @return \Iterator<string, mixed>|null
71
+ */
72
+ abstract protected function deserializeDict (mixed $ data , Type $ type , array $ context ): ?\Iterator ;
111
73
112
74
/**
75
+ * @param T $data
113
76
* @param array<string, mixed> $context
77
+ *
78
+ * @return \Iterator<string, mixed>|array<string, mixed>|null
114
79
*/
115
- protected function deserializeEnum (mixed $ data , Type $ type , array $ context ): ?\BackedEnum
116
- {
117
- if (null === $ data ) {
118
- return null ;
119
- }
80
+ abstract protected function deserializeObjectProperties (mixed $ data , Type $ type , array $ context ): \Iterator |array |null ;
120
81
121
- try {
122
- return ($ type ->className ())::from ($ data );
123
- } catch (\ValueError $ e ) {
124
- throw new UnexpectedValueException (sprintf ('Unexpected "%s" value for "%s" backed enumeration. ' , $ data , $ type ));
125
- }
126
- }
82
+ /**
83
+ * @param T $data
84
+ * @param array<string, mixed> $context
85
+ *
86
+ * @return callable(): mixed
87
+ */
88
+ abstract protected function propertyValueCallable (Type |UnionType $ type , mixed $ data , mixed $ value , array $ context ): callable ;
127
89
128
90
/**
91
+ * @param T $data
129
92
* @param array<string, mixed> $context
130
93
*/
131
- protected function deserializeObject (mixed $ data , Type $ type , array $ context ): ? object
94
+ final public function deserialize (mixed $ data , Type | UnionType $ type , array $ context ): mixed
132
95
{
133
- if (null === $ data ) {
134
- return null ;
135
- }
136
-
137
- $ hook = null ;
96
+ if ($ type instanceof UnionType) {
97
+ if (!isset ($ context ['union_selector ' ][$ typeString = (string ) $ type ])) {
98
+ throw new UnexpectedValueException (sprintf ('Cannot guess type to use for "%s", you may specify a type in "$context[ \'union_selector \'][ \'%1$s \']". ' , (string ) $ type ));
99
+ }
138
100
139
- if (isset ($ context ['hooks ' ]['deserialize ' ][$ className = $ type ->className ()])) {
140
- $ hook = $ context ['hooks ' ]['deserialize ' ][$ className ];
141
- } elseif (isset ($ context ['hooks ' ]['deserialize ' ]['object ' ])) {
142
- $ hook = $ context ['hooks ' ]['deserialize ' ]['object ' ];
101
+ /** @var Type $type */
102
+ $ type = (self ::$ cache ['type ' ][$ typeString ] ??= TypeFactory::createFromString ($ context ['union_selector ' ][$ typeString ]));
143
103
}
144
104
145
- if (null !== $ hook ) {
146
- /** @var array{type?: string, context?: array<string, mixed>} $hookResult */
147
- $ hookResult = $ hook ((string ) $ type , $ context );
105
+ if ($ type ->isScalar ()) {
106
+ $ scalar = $ this ->deserializeScalar ($ data , $ type , $ context );
107
+
108
+ if (null === $ scalar ) {
109
+ if (!$ type ->isNullable ()) {
110
+ throw new UnexpectedValueException (sprintf ('Unexpected "null" value for "%s" type. ' , (string ) $ type ));
111
+ }
148
112
149
- if (isset ($ hookResult ['type ' ])) {
150
- /** @var Type $type */
151
- $ type = (self ::$ cache ['type ' ][$ hookResult ['type ' ]] ??= TypeFactory::createFromString ($ hookResult ['type ' ]));
113
+ return null ;
152
114
}
153
115
154
- $ context = $ hookResult ['context ' ] ?? $ context ;
116
+ try {
117
+ return match ($ type ->name ()) {
118
+ 'int ' => (int ) $ scalar ,
119
+ 'float ' => (float ) $ scalar ,
120
+ 'string ' => (string ) $ scalar ,
121
+ 'bool ' => (bool ) $ scalar ,
122
+ default => throw new LogicException (sprintf ('Unhandled "%s" scalar cast ' , $ type ->name ())),
123
+ };
124
+ } catch (\Throwable ) {
125
+ throw new UnexpectedValueException (sprintf ('Cannot cast "%s" to "%s" ' , get_debug_type ($ scalar ), (string ) $ type ));
126
+ }
155
127
}
156
128
157
- /** @var \ReflectionClass<object> $reflection */
158
- $ reflection = ( self :: $ cache [ ' class_reflection ' ][ $ typeString = ( string ) $ type ] ??= new \ ReflectionClass ( $ type -> className ()) );
129
+ if ( $ type -> isEnum ()) {
130
+ $ enum = $ this -> deserializeEnum ( $ data , $ type , $ context );
159
131
160
- /** @var array<string, callable(): mixed> $propertiesValues */
161
- $ propertiesValues = [];
132
+ if (null === $ enum ) {
133
+ if (!$ type ->isNullable ()) {
134
+ throw new UnexpectedValueException (sprintf ('Unexpected "null" value for "%s" type. ' , (string ) $ type ));
135
+ }
162
136
163
- foreach ( $ data as $ k => $ v ) {
164
- $ hook = null ;
137
+ return null ;
138
+ }
165
139
166
- if ( isset ( $ context [ ' hooks ' ][ ' deserialize ' ][( $ className = $ reflection -> getName ()). ' [ ' . $ k . ' ] ' ])) {
167
- $ hook = $ context [ ' hooks ' ][ ' deserialize ' ][ $ className. ' [ ' . $ k . ' ] ' ] ;
168
- } elseif ( isset ( $ context [ ' hooks ' ][ ' deserialize ' ][ ' property ' ]) ) {
169
- $ hook = $ context [ ' hooks ' ][ ' deserialize ' ][ ' property ' ] ;
140
+ try {
141
+ return ( $ type -> className ()):: from ( $ enum ) ;
142
+ } catch ( \ ValueError $ e ) {
143
+ throw new UnexpectedValueException ( sprintf ( ' Unexpected "%s" value for "%s" backed enumeration. ' , $ enum , $ type )) ;
170
144
}
145
+ }
171
146
172
- $ propertyName = $ k ;
147
+ if ($ type ->isCollection ()) {
148
+ $ collection = $ type ->isList () ? $ this ->deserializeList ($ data , $ type , $ context ) : $ this ->deserializeDict ($ data , $ type , $ context );
173
149
174
- if (null !== $ hook ) {
175
- $ hookResult = $ this ->executePropertyHook ($ hook , $ reflection , $ k , $ v , $ data , $ context );
150
+ if (null === $ collection ) {
151
+ if (!$ type ->isNullable ()) {
152
+ throw new UnexpectedValueException (sprintf ('Unexpected "null" value for "%s" type. ' , (string ) $ type ));
153
+ }
176
154
177
- $ propertyName = $ hookResult ['name ' ] ?? $ propertyName ;
178
- $ context = $ hookResult ['context ' ] ?? $ context ;
155
+ return null ;
179
156
}
180
157
181
- self ::$ cache ['class_has_property ' ][$ propertyIdentifier = $ typeString .$ propertyName ] ??= $ reflection ->hasProperty ($ propertyName );
158
+ return $ type ->isIterable () ? $ collection : iterator_to_array ($ collection );
159
+ }
160
+
161
+ if ($ type ->isObject ()) {
162
+ $ objectProperties = $ this ->deserializeObjectProperties ($ data , $ type , $ context );
163
+
164
+ if (null === $ objectProperties ) {
165
+ if (!$ type ->isNullable ()) {
166
+ throw new UnexpectedValueException (sprintf ('Unexpected "null" value for "%s" type. ' , (string ) $ type ));
167
+ }
182
168
183
- if (!self ::$ cache ['class_has_property ' ][$ propertyIdentifier ]) {
184
- continue ;
169
+ return null ;
185
170
}
186
171
187
- if (isset ($ hookResult ['value_provider ' ])) {
188
- $ propertiesValues [$ propertyName ] = $ hookResult ['value_provider ' ];
172
+ if (null !== $ hook = $ context ['hooks ' ]['deserialize ' ][$ type ->className ()] ?? $ context ['hooks ' ]['deserialize ' ]['object ' ] ?? null ) {
173
+ /** @var array{type?: string, context?: array<string, mixed>} $hookResult */
174
+ $ hookResult = $ hook ((string ) $ type , $ context );
175
+
176
+ if (isset ($ hookResult ['type ' ])) {
177
+ /** @var Type $type */
178
+ $ type = (self ::$ cache ['type ' ][$ hookResult ['type ' ]] ??= TypeFactory::createFromString ($ hookResult ['type ' ]));
179
+ }
189
180
190
- continue ;
181
+ $ context = $ hookResult [ ' context ' ] ?? $ context ;
191
182
}
192
183
193
- self ::$ cache ['property_type ' ][$ propertyIdentifier ] ??= TypeFactory::createFromString ($ this ->reflectionTypeExtractor ->extractFromProperty ($ reflection ->getProperty ($ propertyName )));
184
+ /** @var \ReflectionClass<object> $reflection */
185
+ $ reflection = (self ::$ cache ['class_reflection ' ][$ typeString = (string ) $ type ] ??= new \ReflectionClass ($ type ->className ()));
194
186
195
- $ propertiesValues [ $ propertyName ] = $ this -> propertyValue ( self :: $ cache [ ' property_type ' ][ $ propertyIdentifier ], $ v , $ data , $ context );
196
- }
187
+ /** @var array<string, callable(): mixed> $valueCallables */
188
+ $ valueCallables = [];
197
189
198
- if (isset ($ context ['instantiator ' ])) {
199
- return $ context ['instantiator ' ]($ reflection , $ propertiesValues , $ context );
200
- }
190
+ foreach ($ objectProperties as $ name => $ value ) {
191
+ if (null !== $ hook = $ context ['hooks ' ]['deserialize ' ][$ reflection ->getName ().'[ ' .$ name .'] ' ] ?? $ context ['hooks ' ]['deserialize ' ]['property ' ] ?? null ) {
192
+ $ hookResult = $ hook (
193
+ $ reflection ,
194
+ $ name ,
195
+ fn (string $ type , array $ context ) => $ this ->propertyValueCallable (self ::$ cache ['type ' ][$ type ] ??= TypeFactory::createFromString ($ type ), $ data , $ value , $ context )(),
196
+ $ context ,
197
+ );
201
198
202
- $ object = new ($ reflection ->getName ())();
199
+ $ name = $ hookResult ['name ' ] ?? $ name ;
200
+ $ context = $ hookResult ['context ' ] ?? $ context ;
201
+ }
203
202
204
- foreach ($ propertiesValues as $ property => $ value ) {
205
- try {
206
- $ object ->{$ property } = $ value ();
207
- } catch (\TypeError |UnexpectedValueException $ e ) {
208
- $ exception = new UnexpectedValueException ($ e ->getMessage (), previous: $ e );
203
+ self ::$ cache ['class_has_property ' ][$ identifier = $ typeString .$ name ] ??= $ reflection ->hasProperty ($ name );
204
+
205
+ if (!self ::$ cache ['class_has_property ' ][$ identifier ]) {
206
+ continue ;
207
+ }
208
+
209
+ if (isset ($ hookResult ['value_provider ' ])) {
210
+ $ valueCallables [$ name ] = $ hookResult ['value_provider ' ];
209
211
210
- if (!($ context ['collect_errors ' ] ?? false )) {
211
- throw $ exception ;
212
+ continue ;
212
213
}
213
214
214
- $ context ['collected_errors ' ][] = $ exception ;
215
+ self ::$ cache ['property_type ' ][$ identifier ] ??= TypeFactory::createFromString ($ this ->reflectionTypeExtractor ->extractFromProperty ($ reflection ->getProperty ($ name )));
216
+
217
+ $ valueCallables [$ name ] = $ this ->propertyValueCallable (self ::$ cache ['property_type ' ][$ identifier ], $ data , $ value , $ context );
215
218
}
216
- }
217
219
218
- return $ object ;
219
- }
220
+ if (isset ($ context ['instantiator ' ])) {
221
+ return $ context ['instantiator ' ]($ reflection , $ valueCallables , $ context );
222
+ }
220
223
221
- /**
222
- * @param callable(\ReflectionClass<object>, string, callable(string, array<string, mixed>): mixed, array<string, mixed>): array{name?: string, value_provider?: callable(): mixed, context?: array<string, mixed>} $hook
223
- * @param \ReflectionClass<object> $reflection
224
- * @param array<string, mixed> $context
225
- *
226
- * @return array{name?: string, value_provider?: callable(): mixed, context?: array<string, mixed>}
227
- */
228
- protected function executePropertyHook (callable $ hook , \ReflectionClass $ reflection , string $ key , mixed $ value , mixed $ data , array $ context ): array
229
- {
230
- return $ hook (
231
- $ reflection ,
232
- $ key ,
233
- function (string $ type , array $ context ) use ($ value ): mixed {
234
- return $ this ->deserialize ($ value , self ::$ cache ['type ' ][$ type ] ??= TypeFactory::createFromString ($ type ), $ context );
235
- },
236
- $ context ,
237
- );
238
- }
224
+ $ object = new ($ reflection ->getName ())();
239
225
240
- /**
241
- * @param array<string, mixed> $context
242
- *
243
- * @return callable(): mixed
244
- */
245
- protected function propertyValue (Type |UnionType $ type , mixed $ value , mixed $ data , array $ context ): callable
246
- {
247
- return fn () => $ this ->deserialize ($ value , $ type , $ context );
248
- }
226
+ foreach ($ valueCallables as $ name => $ callable ) {
227
+ try {
228
+ $ object ->{$ name } = $ callable ();
229
+ } catch (\TypeError |UnexpectedValueException $ e ) {
230
+ $ exception = new UnexpectedValueException ($ e ->getMessage (), previous: $ e );
249
231
250
- /**
251
- * @param array<string, mixed>|list<mixed> $collection
252
- * @param array<string, mixed> $context
253
- *
254
- * @return \Iterator<mixed>|\Iterator<string, mixed>
255
- */
256
- private function deserializeCollectionItems (array $ collection , Type |UnionType $ type , array $ context ): \Iterator
257
- {
258
- foreach ($ collection as $ key => $ value ) {
259
- yield $ key => $ this ->deserialize ($ value , $ type , $ context );
232
+ if (!($ context ['collect_errors ' ] ?? false )) {
233
+ throw $ exception ;
234
+ }
235
+
236
+ $ context ['collected_errors ' ][] = $ exception ;
237
+ }
238
+ }
239
+
240
+ return $ object ;
260
241
}
242
+
243
+ throw new UnsupportedTypeException ($ type );
261
244
}
262
245
}
0 commit comments