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\VarExporter\Internal; |
13
|
|
|
|
14
|
|
|
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* @author Nicolas Grekas <[email protected]> |
18
|
|
|
* |
19
|
|
|
* @internal |
20
|
|
|
*/ |
21
|
|
|
class Exporter |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* Prepares an array of values for VarExporter. |
25
|
|
|
* |
26
|
|
|
* For performance this method is public and has no type-hints. |
27
|
|
|
* |
28
|
|
|
* @param array &$values |
29
|
|
|
* @param \SplObjectStorage $objectsPool |
30
|
|
|
* @param array &$refsPool |
31
|
|
|
* @param int &$objectsCount |
32
|
|
|
* @param bool &$valuesAreStatic |
33
|
|
|
* |
34
|
|
|
* @return array |
35
|
|
|
* |
36
|
|
|
* @throws NotInstantiableTypeException When a value cannot be serialized |
37
|
|
|
*/ |
38
|
|
|
public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic) |
39
|
|
|
{ |
40
|
|
|
$refs = $values; |
41
|
|
|
foreach ($values as $k => $value) { |
42
|
|
|
if (\is_resource($value)) { |
43
|
|
|
throw new NotInstantiableTypeException(get_resource_type($value).' resource'); |
44
|
|
|
} |
45
|
|
|
$refs[$k] = $objectsPool; |
46
|
|
|
|
47
|
|
|
if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) { |
48
|
|
|
$values[$k] = &$value; // Break hard references to make $values completely |
49
|
|
|
unset($value); // independent from the original structure |
50
|
|
|
$refs[$k] = $value = $values[$k]; |
51
|
|
|
if ($value instanceof Reference && 0 > $value->id) { |
52
|
|
|
$valuesAreStatic = false; |
53
|
|
|
++$value->count; |
54
|
|
|
continue; |
55
|
|
|
} |
56
|
|
|
$refsPool[] = [&$refs[$k], $value, &$value]; |
57
|
|
|
$refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
if (\is_array($value)) { |
61
|
|
|
if ($value) { |
|
|
|
|
62
|
|
|
$value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); |
63
|
|
|
} |
64
|
|
|
goto handle_value; |
65
|
|
|
} elseif (!\is_object($value) || $value instanceof \UnitEnum) { |
|
|
|
|
66
|
|
|
goto handle_value; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$valueIsStatic = false; |
70
|
|
|
if (isset($objectsPool[$value])) { |
71
|
|
|
++$objectsCount; |
72
|
|
|
$value = new Reference($objectsPool[$value][0]); |
73
|
|
|
goto handle_value; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
$class = $value::class; |
77
|
|
|
$reflector = Registry::$reflectors[$class] ??= Registry::getClassReflector($class); |
78
|
|
|
$properties = []; |
79
|
|
|
|
80
|
|
|
if ($reflector->hasMethod('__serialize')) { |
81
|
|
|
if (!$reflector->getMethod('__serialize')->isPublic()) { |
82
|
|
|
throw new \Error(\sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class)); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
if (!\is_array($serializeProperties = $value->__serialize())) { |
86
|
|
|
throw new \TypeError($class.'::__serialize() must return an array'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
if ($reflector->hasMethod('__unserialize')) { |
90
|
|
|
$properties = $serializeProperties; |
91
|
|
|
} else { |
92
|
|
|
foreach ($serializeProperties as $n => $v) { |
93
|
|
|
$p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; |
94
|
|
|
$c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; |
|
|
|
|
95
|
|
|
$properties[$c][$n] = $v; |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
goto prepare_value; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$sleep = null; |
103
|
|
|
$proto = Registry::$prototypes[$class]; |
104
|
|
|
|
105
|
|
|
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) { |
106
|
|
|
// ArrayIterator and ArrayObject need special care because their "flags" |
107
|
|
|
// option changes the behavior of the (array) casting operator. |
108
|
|
|
[$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto); |
109
|
|
|
|
110
|
|
|
// populates Registry::$prototypes[$class] with a new instance |
111
|
|
|
Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]); |
112
|
|
|
} elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) { |
113
|
|
|
// By implementing Serializable, SplObjectStorage breaks |
114
|
|
|
// internal references; let's deal with it on our own. |
115
|
|
|
foreach (clone $value as $v) { |
116
|
|
|
$properties[] = $v; |
117
|
|
|
$properties[] = $value[$v]; |
118
|
|
|
} |
119
|
|
|
$properties = ['SplObjectStorage' => ["\0" => $properties]]; |
120
|
|
|
$arrayValue = (array) $value; |
121
|
|
|
} elseif ($value instanceof \Serializable |
122
|
|
|
|| $value instanceof \__PHP_Incomplete_Class |
123
|
|
|
) { |
124
|
|
|
++$objectsCount; |
125
|
|
|
$objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; |
126
|
|
|
$value = new Reference($id); |
127
|
|
|
goto handle_value; |
128
|
|
|
} else { |
129
|
|
|
if (method_exists($class, '__sleep')) { |
130
|
|
|
if (!\is_array($sleep = $value->__sleep())) { |
131
|
|
|
trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE); |
132
|
|
|
$value = null; |
133
|
|
|
goto handle_value; |
134
|
|
|
} |
135
|
|
|
$sleep = array_flip($sleep); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
$arrayValue = (array) $value; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
$proto = (array) $proto; |
142
|
|
|
|
143
|
|
|
foreach ($arrayValue as $name => $v) { |
144
|
|
|
$i = 0; |
145
|
|
|
$n = (string) $name; |
146
|
|
|
if ('' === $n || "\0" !== $n[0]) { |
147
|
|
|
$p = $reflector->hasProperty($n) ? $reflector->getProperty($n) : null; |
148
|
|
|
$c = $p && (\PHP_VERSION_ID >= 80400 ? $p->isProtectedSet() || $p->isPrivateSet() : $p->isReadOnly()) ? $p->class : 'stdClass'; |
149
|
|
|
} elseif ('*' === $n[1]) { |
150
|
|
|
$n = substr($n, 3); |
151
|
|
|
$c = $reflector->getProperty($n)->class; |
152
|
|
|
if ('Error' === $c) { |
153
|
|
|
$c = 'TypeError'; |
154
|
|
|
} elseif ('Exception' === $c) { |
155
|
|
|
$c = 'ErrorException'; |
156
|
|
|
} |
157
|
|
|
} else { |
158
|
|
|
$i = strpos($n, "\0", 2); |
159
|
|
|
$c = substr($n, 1, $i - 1); |
160
|
|
|
$n = substr($n, 1 + $i); |
161
|
|
|
} |
162
|
|
|
if (null !== $sleep) { |
163
|
|
|
if (!isset($sleep[$name]) && (!isset($sleep[$n]) || ($i && $c !== $class))) { |
164
|
|
|
unset($arrayValue[$name]); |
165
|
|
|
continue; |
166
|
|
|
} |
167
|
|
|
unset($sleep[$name], $sleep[$n]); |
168
|
|
|
} |
169
|
|
|
if (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) { |
170
|
|
|
$properties[$c][$n] = $v; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
if ($sleep) { |
174
|
|
|
foreach ($sleep as $n => $v) { |
175
|
|
|
trigger_error(\sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
if (method_exists($class, '__unserialize')) { |
179
|
|
|
$properties = $arrayValue; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
prepare_value: |
183
|
|
|
$objectsPool[$value] = [$id = \count($objectsPool)]; |
184
|
|
|
$properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); |
185
|
|
|
++$objectsCount; |
186
|
|
|
$objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)]; |
187
|
|
|
|
188
|
|
|
$value = new Reference($id); |
189
|
|
|
|
190
|
|
|
handle_value: |
191
|
|
|
if ($isRef) { |
192
|
|
|
unset($value); // Break the hard reference created above |
193
|
|
|
} elseif (!$valueIsStatic) { |
194
|
|
|
$values[$k] = $value; |
195
|
|
|
} |
196
|
|
|
$valuesAreStatic = $valueIsStatic && $valuesAreStatic; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
return $values; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
public static function export($value, $indent = '') |
203
|
|
|
{ |
204
|
|
|
switch (true) { |
205
|
|
|
case \is_int($value) || \is_float($value): return var_export($value, true); |
206
|
|
|
case [] === $value: return '[]'; |
207
|
|
|
case false === $value: return 'false'; |
208
|
|
|
case true === $value: return 'true'; |
209
|
|
|
case null === $value: return 'null'; |
210
|
|
|
case '' === $value: return "''"; |
211
|
|
|
case $value instanceof \UnitEnum: return '\\'.ltrim(var_export($value, true), '\\'); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
if ($value instanceof Reference) { |
215
|
|
|
if (0 <= $value->id) { |
216
|
|
|
return '$o['.$value->id.']'; |
217
|
|
|
} |
218
|
|
|
if (!$value->count) { |
219
|
|
|
return self::export($value->value, $indent); |
220
|
|
|
} |
221
|
|
|
$value = -$value->id; |
222
|
|
|
|
223
|
|
|
return '&$r['.$value.']'; |
224
|
|
|
} |
225
|
|
|
$subIndent = $indent.' '; |
226
|
|
|
|
227
|
|
|
if (\is_string($value)) { |
228
|
|
|
$code = \sprintf("'%s'", addcslashes($value, "'\\")); |
229
|
|
|
|
230
|
|
|
$code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", function ($m) use ($subIndent) { |
231
|
|
|
$m[1] = \sprintf('\'."%s".\'', str_replace( |
232
|
|
|
["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'], |
233
|
|
|
['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'], |
234
|
|
|
$m[1] |
235
|
|
|
)); |
236
|
|
|
|
237
|
|
|
if ("'" === $m[2]) { |
238
|
|
|
return substr($m[1], 0, -2); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if (str_ends_with($m[1], 'n".\'')) { |
242
|
|
|
return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return $m[1].$m[2]; |
246
|
|
|
}, $code, -1, $count); |
247
|
|
|
|
248
|
|
|
if ($count && str_starts_with($code, "''.")) { |
249
|
|
|
$code = substr($code, 3); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return $code; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
if (\is_array($value)) { |
256
|
|
|
$j = -1; |
257
|
|
|
$code = ''; |
258
|
|
|
foreach ($value as $k => $v) { |
259
|
|
|
$code .= $subIndent; |
260
|
|
|
if (!\is_int($k) || 1 !== $k - $j) { |
261
|
|
|
$code .= self::export($k, $subIndent).' => '; |
262
|
|
|
} |
263
|
|
|
if (\is_int($k) && $k > $j) { |
264
|
|
|
$j = $k; |
265
|
|
|
} |
266
|
|
|
$code .= self::export($v, $subIndent).",\n"; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return "[\n".$code.$indent.']'; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if ($value instanceof Values) { |
273
|
|
|
$code = $subIndent."\$r = [],\n"; |
274
|
|
|
foreach ($value->values as $k => $v) { |
275
|
|
|
$code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n"; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
return "[\n".$code.$indent.']'; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
if ($value instanceof Registry) { |
282
|
|
|
return self::exportRegistry($value, $indent, $subIndent); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if ($value instanceof Hydrator) { |
286
|
|
|
return self::exportHydrator($value, $indent, $subIndent); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
throw new \UnexpectedValueException(\sprintf('Cannot export value of type "%s".', get_debug_type($value))); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
private static function exportRegistry(Registry $value, string $indent, string $subIndent): string |
293
|
|
|
{ |
294
|
|
|
$code = ''; |
295
|
|
|
$serializables = []; |
296
|
|
|
$seen = []; |
297
|
|
|
$prototypesAccess = 0; |
298
|
|
|
$factoriesAccess = 0; |
299
|
|
|
$r = '\\'.Registry::class; |
300
|
|
|
$j = -1; |
301
|
|
|
|
302
|
|
|
foreach ($value->classes as $k => $class) { |
303
|
|
|
if (':' === ($class[1] ?? null)) { |
304
|
|
|
$serializables[$k] = $class; |
305
|
|
|
continue; |
306
|
|
|
} |
307
|
|
|
if (!Registry::$instantiableWithoutConstructor[$class]) { |
308
|
|
|
if (is_subclass_of($class, 'Serializable') && !method_exists($class, '__unserialize')) { |
309
|
|
|
$serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}'; |
310
|
|
|
} else { |
311
|
|
|
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}'; |
312
|
|
|
} |
313
|
|
|
if (is_subclass_of($class, 'Throwable')) { |
314
|
|
|
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0"; |
315
|
|
|
$serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4); |
316
|
|
|
} |
317
|
|
|
continue; |
318
|
|
|
} |
319
|
|
|
$code .= $subIndent.(1 !== $k - $j ? $k.' => ' : ''); |
320
|
|
|
$j = $k; |
321
|
|
|
$eol = ",\n"; |
322
|
|
|
$c = '['.self::export($class).']'; |
323
|
|
|
|
324
|
|
|
if ($seen[$class] ?? false) { |
325
|
|
|
if (Registry::$cloneable[$class]) { |
326
|
|
|
++$prototypesAccess; |
327
|
|
|
$code .= 'clone $p'.$c; |
328
|
|
|
} else { |
329
|
|
|
++$factoriesAccess; |
330
|
|
|
$code .= '$f'.$c.'()'; |
331
|
|
|
} |
332
|
|
|
} else { |
333
|
|
|
$seen[$class] = true; |
334
|
|
|
if (Registry::$cloneable[$class]) { |
335
|
|
|
$code .= 'clone ('.($prototypesAccess++ ? '$p' : '($p = &'.$r.'::$prototypes)').$c.' ?? '.$r.'::p'; |
336
|
|
|
} else { |
337
|
|
|
$code .= '('.($factoriesAccess++ ? '$f' : '($f = &'.$r.'::$factories)').$c.' ?? '.$r.'::f'; |
338
|
|
|
$eol = '()'.$eol; |
339
|
|
|
} |
340
|
|
|
$code .= '('.substr($c, 1, -1).'))'; |
341
|
|
|
} |
342
|
|
|
$code .= $eol; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
if (1 === $prototypesAccess) { |
346
|
|
|
$code = str_replace('($p = &'.$r.'::$prototypes)', $r.'::$prototypes', $code); |
347
|
|
|
} |
348
|
|
|
if (1 === $factoriesAccess) { |
349
|
|
|
$code = str_replace('($f = &'.$r.'::$factories)', $r.'::$factories', $code); |
350
|
|
|
} |
351
|
|
|
if ('' !== $code) { |
352
|
|
|
$code = "\n".$code.$indent; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
if ($serializables) { |
356
|
|
|
$code = $r.'::unserialize(['.$code.'], '.self::export($serializables, $indent).')'; |
357
|
|
|
} else { |
358
|
|
|
$code = '['.$code.']'; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
return '$o = '.$code; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string |
365
|
|
|
{ |
366
|
|
|
$code = ''; |
367
|
|
|
foreach ($value->properties as $class => $properties) { |
368
|
|
|
$code .= $subIndent.' '.self::export($class).' => '.self::export($properties, $subIndent.' ').",\n"; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
$code = [ |
372
|
|
|
self::export($value->registry, $subIndent), |
373
|
|
|
self::export($value->values, $subIndent), |
374
|
|
|
'' !== $code ? "[\n".$code.$subIndent.']' : '[]', |
375
|
|
|
self::export($value->value, $subIndent), |
376
|
|
|
self::export($value->wakeups, $subIndent), |
377
|
|
|
]; |
378
|
|
|
|
379
|
|
|
return '\\'.$value::class."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')'; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* @param \ArrayIterator|\ArrayObject $value |
384
|
|
|
* @param \ArrayIterator|\ArrayObject $proto |
385
|
|
|
*/ |
386
|
|
|
private static function getArrayObjectProperties($value, $proto): array |
387
|
|
|
{ |
388
|
|
|
$reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject'; |
389
|
|
|
$reflector = Registry::$reflectors[$reflector] ??= Registry::getClassReflector($reflector); |
390
|
|
|
|
391
|
|
|
$properties = [ |
392
|
|
|
$arrayValue = (array) $value, |
393
|
|
|
$reflector->getMethod('getFlags')->invoke($value), |
394
|
|
|
$value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator', |
395
|
|
|
]; |
396
|
|
|
|
397
|
|
|
$reflector = $reflector->getMethod('setFlags'); |
398
|
|
|
$reflector->invoke($proto, \ArrayObject::STD_PROP_LIST); |
399
|
|
|
|
400
|
|
|
if ($properties[1] & \ArrayObject::STD_PROP_LIST) { |
401
|
|
|
$reflector->invoke($value, 0); |
402
|
|
|
$properties[0] = (array) $value; |
403
|
|
|
} else { |
404
|
|
|
$reflector->invoke($value, \ArrayObject::STD_PROP_LIST); |
405
|
|
|
$arrayValue = (array) $value; |
406
|
|
|
} |
407
|
|
|
$reflector->invoke($value, $properties[1]); |
408
|
|
|
|
409
|
|
|
if ([[], 0, 'ArrayIterator'] === $properties) { |
410
|
|
|
$properties = []; |
411
|
|
|
} else { |
412
|
|
|
if ('ArrayIterator' === $properties[2]) { |
413
|
|
|
unset($properties[2]); |
414
|
|
|
} |
415
|
|
|
$properties = [$reflector->class => ["\0" => $properties]]; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
return [$arrayValue, $properties]; |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.