depage-xmldb v2.3.1
XML documents in a relational database
Loading...
Searching...
No Matches
DocumentWriter.php
Go to the documentation of this file.
1<?php
2
12
13namespace Depage\XmlDb\Helpers;
14
15use Depage\XmlDb\Interfaces\DocumentWriterHost;
16use Depage\XmlDb\XmlNs;
17
30{
31 protected $pdo;
32
38 private $insertNodeQuery = null;
39
51 public function __construct(
52 $pdo,
53 protected string $table,
54 protected $docId,
55 protected XmlNs $dbNs,
56 protected string $idAttribute,
57 protected Positions $positions,
58 protected DocumentWriterHost $host
59 ) {
60 $this->pdo = $pdo;
61 }
62
71 public function getFreeNodeIds($preference = [])
72 {
73 if (!empty($preference)) {
74 $ids = str_repeat('?,', count($preference) - 1) . '?';
75 $query = $this->pdo->prepare("SELECT id FROM {$this->table} WHERE id IN ($ids)");
76 $query->execute($preference);
77 $results = $query->fetchAll(\PDO::FETCH_COLUMN);
78
79 $free = array_keys(array_flip(array_diff($preference, $results)));
80 } else {
81 $free = [];
82 }
83
84 return $free;
85 }
86
94 public function deleteNode($nodeId)
95 {
96 // get parent (needed by the doctype handler and as return value)
97 $targetId = $this->host->getParentIdById($nodeId);
98
99 if ($this->host->getDoctypeHandler()->onDeleteNode($nodeId, $targetId)) {
100 // delete the node
101 $query = $this->pdo->prepare(
102 "DELETE FROM {$this->table}
103 WHERE id_doc = :doc_id AND id = :node_id",
104 );
105 try {
106 $query->execute([
107 'doc_id' => $this->docId,
108 'node_id' => $nodeId,
109 ]);
110 } catch (\PDOException $e) {
111 $childquery = $this->pdo->prepare(
112 TreeSql::descendants($this->table, "id = :id AND id_doc = :doc_id", $this->host::SORTKEY_PAD) . "
113 SELECT id FROM tree ORDER BY sortkey DESC;",
114 );
115 $childquery->execute([
116 'doc_id' => $this->docId,
117 'id' => $nodeId,
118 ]);
119
120 // all ids have to be collected before the first delete,
121 // statements must not run while the result is still open;
122 // deleting deepest node first keeps the fk cascade shallow
123 $childIds = $childquery->fetchAll(\PDO::FETCH_COLUMN);
124 $childquery->closeCursor();
125
126 foreach ($childIds as $childId) {
127 $query->execute([
128 'doc_id' => $this->docId,
129 'node_id' => $childId,
130 ]);
131 }
132 }
133 }
134 return $targetId;
135 }
136
148 public function moveNode($nodeId, $targetId, $targetIndex)
149 {
150 $result = false;
151 $nodeParentId = $this->host->getParentIdById($nodeId);
152 $nodeIndex = $this->positions->indexById($nodeId);
153
154 // $targetIndex is the index the node has after the move; inside its
155 // own parent the node is not part of the child list that index
156 // counts, so it must not be counted there either (a move to the
157 // index it already has is then the "no move" case)
158 $targetIndex = $this->positions->normalizeIndex(
159 $targetId,
160 $targetIndex,
161 ($targetId == $nodeParentId) ? $nodeId : null
162 );
163
164 if ($targetId != $nodeParentId || $targetIndex != $nodeIndex) {
165 // with sparse positions the node only has to be reattached
166 // between its new neighbours, all other nodes keep their
167 // positions
168 $pos = $this->positions->positionForIndex($targetId, $targetIndex, $nodeId);
169
170 $query = $this->pdo->prepare(
171 "UPDATE {$this->table}
172 SET id_doc = :doc_id, id_parent = :target_id, pos = :pos
173 WHERE id = :node_id AND id_doc = :node_doc_id",
174 );
175 $query->execute([
176 'target_id' => $targetId,
177 'pos' => $pos,
178 'node_id' => $nodeId,
179 'doc_id' => $this->docId,
180 'node_doc_id' => $this->docId,
181 ]);
182
183 $this->host->updateLastChange();
184 $result = $nodeParentId;
185 }
186
187 return $result;
188 }
189
199 public function copyNode($nodeId, $targetId, $targetIndex)
200 {
201 $xmlDoc = $this->host->getSubdocByNodeId($nodeId, false);
202 $rootNode = $xmlDoc;
203 $saveId = $this->saveNodeIn($rootNode, $targetId, $targetIndex, true);
204
205 return $saveId;
206 }
207
213 public function saveAttributes($nodeId, $attributes)
214 {
215 $query = $this->pdo->prepare(
216 "UPDATE {$this->table} AS xml
217 SET xml.value = :attr_str
218 WHERE xml.id = :node_id AND xml.id_doc = :doc_id",
219 );
220 $success = $query->execute([
221 'node_id' => $nodeId,
222 'attr_str' => NodeContent::attributeString($attributes, $this->dbNs, $this->idAttribute),
223 'doc_id' => $this->docId,
224 ]);
225
226 $this->host->updateLastChange();
227
228 return $success;
229 }
230
235 public function saveNodePrivate($node)
236 {
237 // get all nodes in array
238 $nodeArray = [];
239 $this->getNodeArrayForSaving($nodeArray, $node);
240 $rootId = $nodeArray[0]['id'];
241
242 if (is_null($rootId)) {
243 $targetId = null;
244 $targetIndex = 0;
245 } else {
246 // set target_id/pos/doc
247 $targetId = $this->host->getParentIdById($rootId);
248 $targetIndex = $this->positions->indexById($rootId);
249
250 if ($targetId === false) {
251 $targetId = null;
252 }
253 if ($targetIndex === false) {
254 $targetIndex = null;
255 }
256
257 // delete old node
258 $this->deleteNode($rootId);
259 }
260
261 return $this->saveNodeArray($nodeArray, $targetId, $targetIndex, true);
262 }
263
274 public function saveNodeIn($node, $targetId, $targetIndex, $incChildren)
275 {
276 $parentId = $this->host->getParentIdById($targetId);
277
278 // delete child nodes, if target is document
279 if ($parentId === false) {
280 $this->pdo->exec('SET foreign_key_checks = 0;');
281
282 try {
283 $query = $this->pdo->prepare(
284 "DELETE
285 FROM {$this->table}
286 WHERE id_doc = :doc_id",
287 );
288 $query->execute([
289 'doc_id' => $this->docId,
290 ]);
291 } finally {
292 // foreign key checks are session scoped, keep them enabled
293 // even if the delete failed
294 $this->pdo->exec('SET foreign_key_checks = 1;');
295 }
296 }
297
298 // saveNodeToDb turns this index into the position between the
299 // neighbours it is inserted at
300 $targetIndex = $this->positions->normalizeIndex($targetId, $targetIndex);
301
302 // get all nodes in array
303 $nodeArray = [];
304 $this->getNodeArrayForSaving($nodeArray, $node);
305
306 return $this->saveNodeArray($nodeArray, $targetId, $targetIndex, $incChildren);
307 }
308
313 public function saveNodeArray($nodeArray, $targetId, $targetIndex, $incChildren)
314 {
315 $ids = [];
316
317 foreach ($nodeArray as $node) {
318 if ($node['id']) {
319 $ids[] = $node['id'];
320 }
321 }
322
323 $free = $this->getFreeNodeIds($ids);
324
325 foreach ($nodeArray as $i => $node) {
326 if (!is_null($node['id'])) {
327 $index = array_search($node['id'], $free);
328
329 if ($index === false) {
330 $nodeArray[$i]['id'] = null;
331 } else {
332 array_splice($free, $index, 1);
333 }
334 }
335 }
336
337 foreach ($nodeArray as $i => $node) {
338 if (is_null($node['id'])) {
339 $nodeArray[$i]['id'] = array_shift($free);
340 }
341 }
342
343 // save root node
344 $nodeArray[0]['id'] = $this->saveNodeToDb($nodeArray[0]['node'], $nodeArray[0]['id'], $targetId, $targetIndex, true);
345 $this->insertNodeQuery = null;
346
347 if ($incChildren) {
348 // save element nodes
349 $this->saveNodesByType($nodeArray, true);
350 // save other nodes
351 $this->saveNodesByType($nodeArray, false);
352 }
353
354 $this->host->updateLastChange();
355
356 return $nodeArray[0]['id'];
357 }
358
362 public function saveNodesByType(&$nodeArray, $xmlElementNode)
363 {
364 $batch = new NodeBatch($this->pdo, $this->table, (int) $this->docId, $this->host::SAVE_BATCH_SIZE);
365
366 // writing the tree level by level makes sure that every node's
367 // parent has been saved (and its auto increment id read back)
368 // before the node itself is written; within one level the
369 // parents are always already resolved, so no batch has to be
370 // interrupted
371 $depths = [0];
372 $maxDepth = 0;
373 foreach ($nodeArray as $i => $node) {
374 if ($i == 0) {
375 continue;
376 }
377 $depths[$i] = $depths[$node['parent_index']] + 1;
378 $maxDepth = max($maxDepth, $depths[$i]);
379 }
380
381 for ($level = 1; $level <= $maxDepth; $level++) {
382 foreach ($nodeArray as $i => $node) {
383 if (
384 $depths[$i] != $level
385 || (($node['node']->nodeType == XML_ELEMENT_NODE) != $xmlElementNode)
386 ) {
387 continue;
388 }
389
390 $parentId = $nodeArray[$node['parent_index']]['id'];
391
392 [$type, $name, $value] = NodeContent::fromDomNode($node['node'], $this->dbNs, $this->idAttribute);
393 $value = \Normalizer::normalize($value);
394
395 $batch->add($i, $node['id'], $parentId, ($node['pos'] + 1) * Positions::POS_GAP, $name, $value, $type);
396
397 if ($batch->isFull()) {
398 $this->setResolvedNodeIds($batch->write(), $nodeArray);
399 }
400 }
401
402 // the auto increment ids of this level have to be known
403 // before its children can reference them
404 $this->setResolvedNodeIds($batch->write(), $nodeArray);
405 }
406
407 $this->setResolvedNodeIds($batch->write(), $nodeArray);
408
409 $this->insertNodeQuery = null;
410 }
411
422 protected function setResolvedNodeIds($resolvedIds, &$nodeArray)
423 {
424 foreach ($resolvedIds as $index => $id) {
425 $nodeArray[$index]['id'] = $id;
426
427 if ($nodeArray[$index]['node']->nodeType == XML_ELEMENT_NODE) {
428 $nodeArray[$index]['node']->setAttributeNS(
429 $this->dbNs->uri,
430 $this->dbNs->ns . ':' . $this->idAttribute,
431 $id
432 );
433 }
434 }
435 }
436
446 public function getNodeArrayForSaving(&$nodeArray, $node, $parentIndex = null, $pos = 0, $stripwhitespace = true)
447 {
448 if (!$node) {
449 return;
450 }
451 $type = $node->nodeType;
452
453 if ($type === XML_DOCUMENT_NODE) {
454 $rootNode = $node->documentElement;
455 $this->getNodeArrayForSaving($nodeArray, $rootNode, $parentIndex, $pos, $stripwhitespace);
456 } elseif ($type === XML_ELEMENT_NODE) {
457 $id = NodeContent::dbIdOf($node, $this->dbNs, $this->idAttribute);
458 $nodeArray[] = [
459 'id' => $id,
460 'id_old' => $id,
461 'parent_index' => $parentIndex,
462 'pos' => $pos,
463 'node' => $node,
464 ];
465
466 $parentIndex = count($nodeArray) - 1;
467 $nodeName = (($node->prefix != '') ? $node->prefix . ':' : '') . $node->localName;
468
469 $preserveWhitespace = $this->host->getDoctypeHandler()->getPreserveWhitespace();
470
471 if (
472 !$stripwhitespace
473 || in_array($nodeName, $preserveWhitespace)
474 ) {
475 $stripwhitespace = false;
476 }
477
478 $tmpNode = $node->firstChild;
479 $i = 0;
480 while ($tmpNode != null) {
481 if (
482 $tmpNode->nodeType != XML_TEXT_NODE
483 || (
484 !$stripwhitespace
485 || trim($tmpNode->textContent) != ''
486 )
487 ) {
488 $this->getNodeArrayForSaving($nodeArray, $tmpNode, $parentIndex, $i, $stripwhitespace);
489 $i++;
490 }
491 $tmpNode = $tmpNode->nextSibling;
492 }
493 } else {
494 // is *_NODE
495 $nodeArray[] = [
496 'id' => null,
497 'id_old' => null,
498 'parent_index' => $parentIndex,
499 'pos' => $pos,
500 'node' => $node,
501 ];
502 }
503 }
504
518 public function saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition = false)
519 {
520 if (is_null($this->insertNodeQuery)) {
521 $this->insertNodeQuery = $this->pdo->prepare(
522 "INSERT {$this->table}
523 SET
524 id = :id_query,
525 id_parent = :target_id,
526 id_doc = :doc_id,
527 pos = :pos,
528 name = :name,
529 value = :value,
530 type = :type
531 ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)",
532 );
533 }
534
535 if (is_null($id) || !is_numeric($id)) {
536 $idQuery = null;
537 } else {
538 $idQuery = (int) $id;
539 }
540
541 if ($targetId !== null && $setPosition) {
542 // sparse positioning: the node gets a position between its
543 // future neighbours, the other children keep theirs; this is
544 // done for every node type, a text node stored with the plain
545 // index would sort in front of the sparse positions
546 $targetIndex = $this->positions->positionForIndex($targetId, $targetIndex);
547 }
548
549 // same tupel NodeContent calculates the subhashes from
550 [$nodeType, $nodeName, $nodeData] = NodeContent::fromDomNode($node, $this->dbNs, $this->idAttribute);
551 $nodeData = \Normalizer::normalize($nodeData);
552
553 $this->insertNodeQuery->execute([
554 'id_query' => $idQuery,
555 'target_id' => $targetId,
556 'pos' => $targetIndex,
557 'name' => $nodeName,
558 'value' => $nodeData,
559 'type' => $nodeType,
560 'doc_id' => $this->docId,
561 ]);
562
563 if (is_null($id)) {
564 $id = $this->pdo->lastInsertId();
565 }
566
567 if ($node->nodeType == XML_ELEMENT_NODE) {
568 $node->setAttributeNS($this->dbNs->uri, $this->dbNs->ns . ':' . $this->idAttribute, $id);
569 }
570
571 return $id;
572 }
573}
574
575/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
getFreeNodeIds($preference=[])
gets unused db-node-ids for saving nodes
saveNodesByType(&$nodeArray, $xmlElementNode)
writes all nodes of one type (element nodes first, the other node types after them) level by level
__construct( $pdo, protected string $table, protected $docId, protected XmlNs $dbNs, protected string $idAttribute, protected Positions $positions, protected DocumentWriterHost $host)
saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition=false)
saves a node to database
saveNodeIn($node, $targetId, $targetIndex, $incChildren)
saves a node into the child list of a target node
saveNodePrivate($node)
re-saves a node in place: the subtree replaces the stored nodes under the db:id its root carries
deleteNode($nodeId)
deletes a node in the database
saveNodeArray($nodeArray, $targetId, $targetIndex, $incChildren)
writes a flattened node tree; the ids the nodes carry are reused when they are free,...
saveAttributes($nodeId, $attributes)
writes the attribute string of a node
setResolvedNodeIds($resolvedIds, &$nodeArray)
stores the database ids a batch insert handed out for its nodes in the list of nodes to save (the par...
copyNode($nodeId, $targetId, $targetIndex)
copy node in database
moveNode($nodeId, $targetId, $targetIndex)
moves node in database
getNodeArrayForSaving(&$nodeArray, $node, $parentIndex=null, $pos=0, $stripwhitespace=true)
gets all nodes of a document in one array
static attributeString(array $attributes, XmlNs $dbNs, string $idAttribute)
builds the attribute string a node's value column holds from the attributes of a node,...
static dbIdOf($node, XmlNs $dbNs, string $idAttribute)
reads the database id out of the db:id attribute of a node ('' if the node does not carry one)
static fromDomNode($node, XmlNs $dbNs, string $idAttribute)
returns the (type, name, value) tupel a DOM node is saved as, in exactly the same way saveNodeToDb st...
const POS_GAP
distance between the sparse positions of sibling nodes
Definition Positions.php:40
static descendants(string $table, string $anchorCondition, int $pad)
builds the recursive common table expression selecting a node matched by $anchor and all of its desce...
Definition TreeSql.php:43
A namespace prefix and the URI it is bound to.
Definition XmlNs.php:9
what the write pipeline (Helpers\DocumentWriter) needs from the document it writes: the document meta...