Skip to content

Commit 41714e6

Browse files
committed
Issue #110: Revamp the breadth-first logic in the exporter.
1 parent 1919bc9 commit 41714e6

1 file changed

Lines changed: 113 additions & 44 deletions

File tree

src/Exporter/Exporter_ToYamlArray.php

Lines changed: 113 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@ class Exporter_ToYamlArray implements ExporterInterface {
2727
private array $exportersByClass = [];
2828

2929
/**
30-
* Object paths as array keys by object hash.
30+
* Objects to export later.
3131
*
32-
* @var array<int, string>
32+
* @var array<int, object>
3333
*/
34-
private array $objectPaths = [];
34+
private array $objects = [];
35+
36+
/**
37+
* Object occurences by object id and depth.
38+
*
39+
* @var array<int, array<int, list<array{key: int|string|null, path: string, ref: mixed}>>>
40+
*/
41+
private array $objectOccurences = [];
3542

3643
/**
3744
* Path in the tree.
@@ -128,55 +135,125 @@ public function withDefaultObjectFactory(string $class, \Closure $factory): stat
128135
*/
129136
public function export(mixed $value, int $depth = 2): mixed {
130137
// Don't pollute the main object cache in the main instance.
131-
$clone = clone $this;
132-
// Populate the object cache breadth-first.
133-
for ($i = 0; $i < $depth; ++$i) {
134-
$clone->exportRecursive($value, $i, null);
135-
}
136-
$export = $clone->exportRecursive($value, $depth, null);
137-
return $export;
138+
return (clone $this)->doExport($value, $depth);
138139
}
139140

140141
/**
141142
* @param mixed $value
142143
* @param int $depth
143-
* @param string|int|null $key
144144
*
145145
* @return mixed
146146
*/
147-
protected function exportRecursive(mixed $value, int $depth, string|int|null $key): mixed {
148-
if (\is_array($value)) {
149-
if ($value === []) {
150-
return [];
151-
}
152-
if ($depth <= 0) {
153-
if (array_is_list($value)) {
154-
return '[...]';
155-
}
156-
else {
157-
return '{...}';
147+
public function doExport(mixed $value, int $depth): mixed {
148+
// Populate the object cache breadth-first.
149+
$export =& $this->exportRecursive($value, $depth, null);
150+
151+
$canonical_object_paths = [];
152+
while ($objects = $this->objects) {
153+
$this->objects = [];
154+
foreach ($objects as $id => $object) {
155+
if (isset($canonical_object_paths[$id])) {
156+
continue;
158157
}
159-
}
160-
$result = [];
161-
foreach ($value as $k => $v) {
162-
$parents = $this->path;
163-
$this->path .= '[' . $k . ']';
158+
$object_best_depth = max(array_keys($this->objectOccurences[$id]));
159+
$object_best_occurence = array_shift($this->objectOccurences[$id][$object_best_depth]);
160+
assert($object_best_occurence !== null);
161+
// Overwrite the reference.
164162
try {
165-
$result[$k] = $this->exportRecursive($v, $depth - 1, $k);
163+
$original_path = $this->path;
164+
$this->path = $object_best_occurence['path'];
165+
$object_best_occurence['ref'] = $this->exportObject($object, $object_best_depth, $object_best_occurence['key']);
166166
}
167167
finally {
168-
$this->path = $parents;
168+
$this->path = $original_path;
169+
}
170+
$canonical_object_paths[$id] = $object_best_occurence['path'];
171+
}
172+
}
173+
174+
foreach ($this->objectOccurences as $id => $occurences_by_depth) {
175+
foreach ($occurences_by_depth as $occurences) {
176+
foreach ($occurences as $occurence) {
177+
// Overwrite the reference.
178+
$occurence['ref'] = ['_ref' => $canonical_object_paths[$id]];
169179
}
170180
}
181+
}
182+
183+
return $export;
184+
}
185+
186+
/**
187+
* @param mixed $value
188+
* @param int $depth
189+
* @param string|int|null $key
190+
*
191+
* @return mixed
192+
*/
193+
protected function &exportRecursive(mixed $value, int $depth, string|int|null $key): mixed {
194+
if (is_array($value)) {
195+
$result = $this->exportArrayRecursive($value, $depth);
196+
}
197+
elseif (is_object($value)) {
198+
$result =& $this->registerObject($value, $depth, $key);
199+
}
200+
elseif (\is_resource($value)) {
201+
$result = 'resource';
202+
}
203+
else {
204+
$result = $value;
205+
}
206+
207+
return $result;
208+
}
209+
210+
/**
211+
* @param array $value
212+
* @param int $depth
213+
*
214+
* @return mixed
215+
*/
216+
protected function exportArrayRecursive(array $value, int $depth): mixed {
217+
if ($value === []) {
218+
$result = [];
171219
return $result;
172220
}
173-
if (is_object($value)) {
174-
return $this->exportObject($value, $depth, $key);
221+
if ($depth <= 0) {
222+
if (array_is_list($value)) {
223+
$result = '[...]';
224+
}
225+
else {
226+
$result = '{...}';
227+
}
228+
return $result;
175229
}
176-
if (\is_resource($value)) {
177-
return 'resource';
230+
$result = [];
231+
foreach ($value as $k => $v) {
232+
$parents = $this->path;
233+
$this->path .= '[' . $k . ']';
234+
try {
235+
$result[$k] =& $this->exportRecursive($v, $depth - 1, $k);
236+
}
237+
finally {
238+
$this->path = $parents;
239+
}
178240
}
179-
return $value;
241+
return $result;
242+
}
243+
244+
/**
245+
* @param object $object
246+
* @param int $depth
247+
* @param string|int|null $key
248+
*
249+
* @return int
250+
*/
251+
protected function &registerObject(object $object, int $depth, string|int|null $key): int {
252+
$id = spl_object_id($object);
253+
$this->objects[$id] = $object;
254+
$ref = $id;
255+
$this->objectOccurences[$id][$depth][] = ['key' => $key, 'path' => $this->path, 'ref' => &$ref];
256+
return $ref;
180257
}
181258

182259
/**
@@ -187,14 +264,6 @@ protected function exportRecursive(mixed $value, int $depth, string|int|null $ke
187264
* @return mixed
188265
*/
189266
protected function exportObject(object $object, int $depth, int|string|null $key = null): mixed {
190-
$id = spl_object_id($object);
191-
$known_path = $this->objectPaths[$id] ?? null;
192-
if ($known_path === NULL) {
193-
$this->objectPaths[$id] = $this->path;
194-
}
195-
elseif ($known_path !== $this->path) {
196-
return ['_ref' => $known_path];
197-
}
198267
foreach ($this->exportersByClass as $class => $callback) {
199268
if ($object instanceof $class) {
200269
$export = $callback($object, $depth, $key, $this);
@@ -402,7 +471,7 @@ protected function exportObjectProperties(object $object, int $depth, bool $publ
402471
$parents = $this->path;
403472
$this->path .= '->' . $property->name;
404473
try {
405-
$export['$' . $property->name] = $this->exportRecursive($propertyValue, $depth - 1, null);
474+
$export['$' . $property->name] =& $this->exportRecursive($propertyValue, $depth - 1, null);
406475
}
407476
finally {
408477
$this->path = $parents;
@@ -433,7 +502,7 @@ protected function exportObjectGetterValues(object $object, int $depth): array {
433502
$parents = $this->path;
434503
$this->path .= '->' . $method->name . '()';
435504
try {
436-
$result[$method->name . '()'] = $this->exportRecursive($value, $depth - 1, null);
505+
$result[$method->name . '()'] =& $this->exportRecursive($value, $depth - 1, null);
437506
}
438507
finally {
439508
$this->path = $parents;

0 commit comments

Comments
 (0)