70 public function plan(array $nodeArray,
bool $checkRoot =
false): ?array
72 if (count($nodeArray) == 0) {
76 $rootId = $nodeArray[0][
'id'];
83 $rootId = (int) $rootId;
86 $query = $this->pdo->prepare(
87 "SELECT xml.id, xml.id_parent, xml.type, xml.name, xml.value, xml.subhash
88 FROM {$this->table} AS xml
89 WHERE xml.id_doc = :doc_id
90 ORDER BY xml.id_parent ASC, xml.pos ASC",
93 'doc_id' => $this->docId,
98 while ($row = $query->fetchObject()) {
99 $oldRows[(int) $row->id] = $row;
101 if (!is_null($row->id_parent)) {
102 $oldChildren[(int) $row->id_parent][] = $row;
105 $query->closeCursor();
107 if (!isset($oldRows[$rootId])) {
124 $parents = [$rootId];
125 while (!empty($parents)) {
126 $parentId = array_shift($parents);
129 foreach ($oldChildren[$parentId] ?? [] as $childRow) {
130 $parents[] = (int) $childRow->id;
136 foreach ($nodeArray as $index => $node) {
137 if (!is_null($node[
'parent_index'])) {
138 $newChildren[$node[
'parent_index']][] = $index;
147 for ($index = count($nodeArray) - 1; $index >= 0; $index--) {
148 $content[$index] = $this->
nodeContent($nodeArray[$index][
'node']);
149 $content[$index][2] = \Normalizer::normalize($content[$index][2]);
152 foreach ($newChildren[$index] ?? [] as $childIndex) {
153 $childHashes[] = $hashes[$childIndex];
156 $hashes[$index] =
NodeContent::hash($content[$index][0], $content[$index][1], $content[$index][2], $childHashes);
160 'root_id' => $rootId,
171 'old_nodes' => $oldSubtree,
172 'new_nodes' => count($nodeArray),
176 $queue = [[0, $rootId]];
178 while (!empty($queue)) {
179 [$index, $oldId] = array_shift($queue);
181 $oldRow = $oldRows[$oldId];
183 if (!is_null($oldRow->subhash) && $oldRow->subhash === $hashes[$index]) {
185 $plan[
'stats'][
'identical']++;
193 [$type, $name, $value] = $content[$index];
195 if ($oldRow->type != $type || (
string) ($oldRow->name ??
'') !== (
string) ($name ??
'') || $oldRow->value !== $value) {
196 $plan[
'updates'][] = [
201 $plan[
'stats'][
'updates']++;
204 $newKids = $newChildren[$index] ?? [];
205 $oldKids = $oldChildren[$oldId] ?? [];
210 foreach ($oldKids as $pos => $row) {
211 $oldPosById[(int) $row->id] = $pos;
217 foreach ($newKids as $newIndex) {
218 $id = $nodeArray[$newIndex][
'id'];
222 if (!empty($id) && isset($oldPosById[(
int) $id]) && !isset($usedOld[$oldPosById[(
int) $id]])) {
223 $pairOfNew[$newIndex] = $oldPosById[(int) $id];
224 $usedOld[$oldPosById[(int) $id]] =
true;
231 $restNew = array_values(array_diff($newKids, array_keys($pairOfNew)));
232 $restOld = array_values(array_diff(array_keys($oldKids), array_keys($usedOld)));
237 while (!empty($restNew) && !empty($restOld)) {
238 $newIndex = array_shift($restNew);
239 $oldPos = array_shift($restOld);
242 $pairOfNew[$newIndex] = $oldPos;
244 $insertNew[] = $newIndex;
245 $deleteOld[] = $oldPos;
248 $insertNew = array_merge($insertNew, $restNew);
249 $deleteOld = array_merge($deleteOld, $restOld);
251 foreach ($deleteOld as $oldPos) {
252 $plan[
'deletes'][] = (int) $oldKids[$oldPos]->
id;
254 $plan[
'stats'][
'deletes'] += count($deleteOld);
259 $survivorsNewOrder = [];
261 foreach ($newKids as $newIndex) {
262 if (isset($pairOfNew[$newIndex])) {
263 $childOldId = (int) $oldKids[$pairOfNew[$newIndex]]->
id;
265 $layout[] = [
'id' => $childOldId];
266 $survivorsNewOrder[] = $childOldId;
268 $layout[] = [
'insert' => count($plan[
'inserts'])];
270 $plan[
'inserts'][] = [
271 'parent_id' => $oldId,
272 'node' => $nodeArray[$newIndex][
'node'],
276 $plan[
'stats'][
'inserts'] += count($insertNew);
278 $pairedOld = array_values($pairOfNew);
280 $survivorsStoredOrder = [];
281 foreach ($oldKids as $pos => $row) {
282 if (in_array($pos, $pairedOld)) {
283 $survivorsStoredOrder[] = (int) $row->id;
287 if (count($insertNew) > 0 || $survivorsNewOrder != $survivorsStoredOrder) {
288 $plan[
'renumber'][$oldId] = $layout;
289 $plan[
'stats'][
'renumber']++;
293 foreach ($pairOfNew as $newIndex => $oldPos) {
294 $queue[] = [$newIndex, (int) $oldKids[$oldPos]->
id];
302 $changed = count($plan[
'inserts']) + count($plan[
'deletes']);
304 if ($changed > self::MAX_CHANGE_RATIO * max($oldSubtree, count($nodeArray))) {