depage-xmldb v2.3.1
XML documents in a relational database
Loading...
Searching...
No Matches
XmlDb.php
Go to the documentation of this file.
1<?php
2
13
14namespace Depage\XmlDb;
15
16use Depage\XmlDb\Exceptions\XmlDbException;
17
19{
20 public $pdo;
21 protected $cache;
22 protected $dbNs;
23
24 protected $tablePrefix = 'dp_';
25 protected $tableDocs;
26 protected $tableXml;
27
28 protected $docIds = [];
29
30 protected $transactions = 0;
32
33 public $options;
43 public function __construct($tablePrefix, $pdo, $cache, $options = [])
44 {
45 $this->pdo = $pdo;
46 $this->pdo->setAttribute(\PDO::ATTR_ORACLE_NULLS, \PDO::NULL_NATURAL);
47
48 $this->cache = $cache;
49
50 $this->options = $options;
51
52 $this->dbNs = new XmlNs('db', 'http://cms.depagecms.net/ns/database');
53
54 $this->tablePrefix = $tablePrefix;
55 $this->tableDocs = $tablePrefix . '_xmldocs';
56 $this->tableXml = $tablePrefix . '_xmltree';
57 }
58
65 public function __get($property)
66 {
67 if (property_exists($this, $property)) {
68 return $this->$property;
69 }
70 }
71
79 public function docExists($docIdOrName)
80 {
81 if (!isset($this->docIds[$docIdOrName])) {
82 if ((int) $docIdOrName > 0) {
83 $id = (int) $docIdOrName;
84
85 // is already a doc-id
86 $query = $this->pdo->prepare(
87 "SELECT docs.name AS docname
88 FROM {$this->tableDocs} AS docs
89 WHERE docs.id = :doc_id"
90 );
91 $query->execute([
92 'doc_id' => $id,
93 ]);
94
95 $result = $query->fetchObject();
96 if ($result === false) {
97 // document does not exist
98 return false;
99 }
100
101 $name = $result->docname;
102 } else {
103 $name = $docIdOrName;
104 $docList = $this->getDocuments($name);
105
106 if (!isset($docList[$name])) {
107 // document does not exist
108 return false;
109 }
110
111 $id = $docList[$name]->getDocId();
112 }
113
114 $this->docIds[$id] = $id;
115 $this->docIds[$name] = $id;
116 }
117
118 return $this->docIds[$docIdOrName];
119 }
120
130 public function getDocuments($name = "", $type = "")
131 {
132 $docs = [];
133
134 $namequery = '';
135 $where = [];
136 $queryParam = [];
137
138 if ($name) {
139 $where[] = 'name = :name';
140 $queryParam['name'] = $name;
141 }
142 if ($type) {
143 $where[] = 'type = :type';
144 $queryParam['type'] = $type;
145 }
146 if (count($where) > 0) {
147 $namequery = "WHERE " . implode(" AND ", $where);
148 }
149
150
151 $query = $this->pdo->prepare(
152 "SELECT
153 docs.name AS name,
154 docs.id AS id,
155 docs.rootid AS rootid,
156 docs.type AS type
157 FROM {$this->tableDocs} AS docs
158 $namequery
159 ORDER BY docs.name ASC"
160 );
161
162 $query->execute($queryParam);
163
164 while ($doc = $query->fetchObject()) {
165 $docs[$doc->name] = new Document($this, $doc->id);
166 }
167
168 return $docs;
169 }
170
176 public function getDoc($docIdOrName)
177 {
178 $doc = false;
179
180 if ($docId = $this->docExists($docIdOrName)) {
181 $doc = new Document($this, $docId);
182 }
183
184 return $doc;
185 }
186
192 public function getDocByNodeId($nodeId)
193 {
194 $doc = false;
195
196 $query = $this->pdo->prepare(
197 "SELECT xml.id_doc AS id_doc
198 FROM {$this->tableXml} AS xml
199 WHERE xml.id = :nodeId"
200 );
201
202 $query->execute([
203 'nodeId' => $nodeId,
204 ]);
205 $result = $query->fetchObject();
206
207 if ($result && $docId = $this->docExists($result->id_doc)) {
208 $doc = new Document($this, $docId);
209 }
210
211 return $doc;
212 }
213
219 public function getDocInfo($docIdOrName)
220 {
221 if ($doc = $this->getDoc($docIdOrName)) {
222 return $doc->getDocInfo();
223 }
224
225 return false;
226 }
227
235 public function getDocXml($docIdOrName, $addIdAttribute = true)
236 {
237 $xml = false;
238
239 if ($docId = $this->docExists($docIdOrName)) {
240 $doc = new Document($this, $docId);
241 $xml = $doc->getXml($addIdAttribute);
242 }
243
244 return $xml;
245 }
246
254 public function getDocXmlXpath($docIdOrName, $xpath, $addIdAttribute = true)
255 {
256 $xml = false;
257
258 if ($docId = $this->docExists($docIdOrName)) {
259 $doc = new Document($this, $docId);
260 $xml = $doc->getSubdocByXpath($xpath, $addIdAttribute);
261 }
262
263 return $xml;
264 }
265
275 public function getSubdocByXpath($xpath, $addIdAttribute = true)
276 {
277 $subdoc = false;
278 $ids = $this->getNodeIdsByXpath($xpath);
279
280 if (isset($ids[0])) {
281 $subdoc = $this->getDocByNodeId($ids[0])->getSubdocByNodeId($ids[0], $addIdAttribute);
282 }
283
284 return $subdoc;
285 }
286
298 public function getNodeIdsByXpath($xpath, $docId = null)
299 {
300 $fallback = false;
301 $tableSql = [];
302 $params = [];
303 $condSql = [];
304 $parser = new XpathParser();
305 $xpathElements = $parser->parseXpathElements($xpath);
306 $levels = count($xpathElements) - 1;
307
308 foreach ($xpathElements as $level => $element) {
309 if ($level == 0) {
310 $tableSql[] = "SELECT l$levels.id FROM";
311 if ($element->divider == '/') {
312 $condSql[] = "l0.id_parent IS NULL";
313 }
314 } else {
315 $tableSql[] = 'INNER JOIN';
316 if ($element->divider == '/' && !$fallback) {
317 $parentLevel = $level - 1;
318 $condSql[] = "l$level.id_parent = l$parentLevel.id";
319 } else {
320 $fallback = true;
321 }
322 }
323
324 $tableSql[] = "{$this->tableXml} AS l$level";
325 $op = $parser->getConditionOperator($element->namespace, $element->name);
326 $condSql[] = "l$level.name $op :name$level";
327 $params["name$level"] = $parser->translateName($element->namespace, $element->name);
328
329 $hasPosition = $parser->parsePosition($element->condition);
330
331 if ($hasPosition) {
332 $fallback = true;
333 } elseif ($element->condition != '') {
334 if ($attributes = $parser->parseAttributes($element->condition)) {
335 // fetch by simple attributes: "ns:name[@attr1] ..."
336 $attributeCond = '';
337 foreach ($attributes as $i => $attribute) {
338 [$name, $operator, $value, $bool] = $attribute;
339
340 if ($bool) {
341 $attributeCond .= $parser->cleanOperator($bool);
342 }
343
344 if ($name == 'db:id') {
345 $attributeOperator = $parser->cleanOperator($operator);
346 $attributeCond .= " l$level.id {$parser->cleanOperator($operator)} :attr{$level}n{$i} ";
347 $params["attr{$level}n{$i}"] = $value;
348 } elseif ($operator == '=' || $operator == '') {
349 $attributeCond .= " l$level.value REGEXP :attr{$level}n{$i} ";
350 $regExValue = (is_null($value)) ? '.*' : $value;
351 $params["attr{$level}n{$i}"] = "(^| )$name=\"$regExValue\"( |$)";
352 } else {
353 $fallback = true;
354 }
355 }
356
357 if (!empty($attributeCond)) {
358 $condSql[] = $attributeCond;
359 }
360 } else {
361 $fallback = true;
362 }
363 }
364
365 if ($level == 0 && !is_null($docId)) {
366 $condSql[] = "l$level.id_doc = :docid$level";
367 $params["docid$level"] = $docId;
368 }
369 }
370
371 $ids = [];
372
373 if ($xpathElements) {
374 if ($fallback) {
375 $tableSql[0] = "SELECT DISTINCT l$level.id_doc FROM";
376 }
377 $where = '';
378 if (!empty($condSql)) {
379 $where = ' WHERE (' . implode(') AND (', $condSql) . ')';
380 }
381
382 $sql = implode(' ', $tableSql) . "$where ORDER BY l$level.id_parent, l$level.pos";
383
384 $query = $this->pdo->prepare($sql);
385 $query->execute($params);
386
387
388 foreach ($query->fetchAll() as $result) {
389 $ids[] = $result[0];
390 }
391 } else {
392 $fallback = true;
393
394 foreach ($this->getDocuments() as $doc) {
395 $ids[] = $doc->getDocId();
396 }
397 }
398
399 if ($fallback) {
400 if (is_null($docId)) {
401 $docIds = $ids;
402 } else {
403 $docIds = [$docId];
404 }
405 $nodeIds = $this->getNodeIdsByXpathDom($xpath, $docIds);
406 } else {
407 $nodeIds = $ids;
408 }
409
410 return $nodeIds;
411 }
412 protected function getNodeIdsByXpathDom($xpath, $docs = [])
413 {
414 $ids = [];
415
416 foreach ($docs as $doc) {
417 $domXpath = new \DomXpath($this->getDoc($doc)->getXml());
418 $list = $domXpath->query($xpath);
419 foreach ($list as $item) {
420 $ids[] = $item->attributes->getNamedItem('id')->nodeValue;
421 }
422 }
423
424 return $ids;
425 }
426
437 public function getIndexNodeData($nodeIds)
438 {
439 $nodeIds = array_values(array_unique(array_map('intval', $nodeIds)));
440
441 $nodeData = [];
442 if (empty($nodeIds)) {
443 return $nodeData;
444 }
445
446 $query = $this->pdo->prepare(
447 "SELECT xml.id, xml.id_doc, xml.type, xml.value
448 FROM {$this->tableXml} AS xml
449 WHERE xml.id IN (" . implode(", ", $nodeIds) . ")",
450 );
451 $query->execute();
452
453 foreach ($query->fetchAll(\PDO::FETCH_OBJ) as $row) {
454 $nodeData[(int) $row->id] = [
455 'doc_id' => (int) $row->id_doc,
456 'attributes' => ($row->type == 'ELEMENT_NODE')
457 ? Document::parseAttributeString($row->value)
458 : [],
459 ];
460 }
461
462 return $nodeData;
463 }
464
476 public function getIndexTextContent($nodeIds)
477 {
478 $nodeIds = array_values(array_unique(array_map('intval', $nodeIds)));
479
480 $texts = [];
481 if (empty($nodeIds)) {
482 return $texts;
483 }
484
486 $query = $this->pdo->prepare(
487 Helpers\TreeSql::descendants($this->tableXml, "id IN (" . implode(", ", $nodeIds) . ")", $pad) . "
488 SELECT tree.root_id, xml.value
489 FROM tree INNER JOIN {$this->tableXml} AS xml ON xml.id = tree.id
490 WHERE xml.type = 'TEXT_NODE'
491 ORDER BY tree.root_id ASC, tree.sortkey ASC",
492 );
493 $query->execute();
494
495 foreach ($query->fetchAll(\PDO::FETCH_OBJ) as $row) {
496 $rootId = (int) $row->root_id;
497
498 $texts[$rootId] = ($texts[$rootId] ?? '') . $row->value;
499 }
500
501 return $texts;
502 }
503
514 public function createDoc($doctype = 'Depage\XmlDb\XmlDoctypes\Base', $docName = null)
515 {
516 if (is_null($docName)) {
517 // generate generic docname based on doctype
518 $docName = '_' . substr($doctype, strrpos($doctype, "\\") + 1) . '_' . sha1(uniqid(dechex(mt_rand(256, 4095))));
519 }
520 if (!is_string($docName) || $this->docExists($docName)) {
521 throw new XmlDbException("Invalid or duplicate document name \"$docName\"");
522 }
523
524 $query = $this->pdo->prepare(
525 "INSERT {$this->tableDocs} SET name = :name, type = :type, ns = '', entities = ''"
526 );
527 $query->execute([
528 'name' => $docName,
529 'type' => $doctype,
530 ]);
531
532 $docId = $this->pdo->lastInsertId();
533 $document = new Document($this, $docId);
534
535 return $document;
536 }
537
545 public function duplicateDoc($docNameOrId, $newName = null)
546 {
547 $copy = false;
548 $original = $this->getDoc($docNameOrId);
549
550 if ($original !== false) {
551 $info = $original->getDocInfo();
552 $xml = $original->getXml(false);
553
554 $copy = $this->createDoc($info->type, $newName);
555 $copy->save($xml);
556 }
557
558 return $copy;
559 }
560
565 public function removeDoc($docId)
566 {
567 $result = false;
568 $docId = $this->docExists($docId);
569
570 if ($docId !== false) {
571 $query = $this->pdo->prepare(
572 "DELETE
573 FROM {$this->tableDocs}
574 WHERE id = :doc_id"
575 );
576 $query->execute([
577 'doc_id' => $docId,
578 ]);
579
580 $this->cache->delete(Helpers\CacheKey::forDocument($this->tableDocs, $docId));
581
582 $this->docIds = array_filter(
583 $this->docIds,
584 function ($id) use ($docId) {
585 return $id != $docId;
586 }
587 );
588
589 $result = true;
590 }
591
592 return $result;
593 }
594
602 public function updateSchema()
603 {
604 $schema = new \Depage\Db\Schema($this->pdo);
605
606 $pdoPrefix = $this->pdo->prefix;
608
609 $schema->setReplace(
610 function ($tableName) use ($pdoPrefix, $tablePrefix) {
611 if ($tableName == '_auth_user') {
612 return $pdoPrefix . $tableName;
613 } else {
614 return $tablePrefix . $tableName;
615 }
616 }
617 );
618
619 $this->pdo->exec('SET FOREIGN_KEY_CHECKS=0;');
620
621 // schema for xmlDb
622 $schema->loadGlob(__DIR__ . '/Sql/*.sql');
623 $schema->update();
624
625 $this->pdo->exec('SET FOREIGN_KEY_CHECKS=1;');
626 }
627
630 public function clearTables()
631 {
632 $this->pdo->query("DELETE FROM `{$this->tableDocs}`;");
633 //$this->pdo->query("ALTER TABLE `{$this->tableDocs}` AUTO_INCREMENT = 1;");
634
635 $this->docIds = [];
636 }
637
646 public function beginTransactionAltering()
647 {
648 $this->alteringTransaction = true;
649
650 return $this->beginTransactionNonAltering();
651 }
652
661 {
662 if ($this->transactions == 0) {
663 $this->beginPdoTransaction();
664 } else {
665 $this->createSavepoint($this->transactions);
666 }
667 $this->transactions++;
668
669 return $this->transactions;
670 }
671
680 public function endTransaction()
681 {
682 $altered = false;
683 $this->transactions--;
684
685 if ($this->transactions == 0) {
686 $this->commitPdoTransaction();
687
689 $this->alteringTransaction = false;
690 } else {
691 $this->releaseSavepoint($this->transactions);
692 }
693
694 return $altered;
695 }
696
704 public function rollBackTransaction()
705 {
706 if ($this->transactions == 0) {
707 return false;
708 }
709
710 $this->transactions--;
711
712 if ($this->transactions == 0) {
713 $this->rollBackPdoTransaction();
714 $this->alteringTransaction = false;
715 } else {
716 $this->rollbackToSavepoint($this->transactions);
717 $this->releaseSavepoint($this->transactions);
718 }
719
720 return true;
721 }
722
729 public function rollBack()
730 {
731 $this->transactions = 0;
732 $this->alteringTransaction = false;
733
734 return $this->rollBackPdoTransaction();
735 }
736
737 protected function beginPdoTransaction()
738 {
739 $this->pdo->beginTransaction();
740 }
741 protected function commitPdoTransaction()
742 {
743 $this->pdo->commit();
744 }
745 protected function rollBackPdoTransaction()
746 {
747 $rolledBack = false;
748
749 if ($this->pdo->inTransaction()) {
750 $this->pdo->rollBack();
751 $rolledBack = true;
752 }
753
754 return $rolledBack;
755 }
756 protected function createSavepoint($level)
757 {
758 if ($this->pdo->inTransaction()) {
759 $this->pdo->exec('SAVEPOINT xmldb_sp_' . (int) $level);
760 }
761 }
762 protected function releaseSavepoint($level)
763 {
764 if ($this->pdo->inTransaction()) {
765 $this->pdo->exec('RELEASE SAVEPOINT xmldb_sp_' . (int) $level);
766 }
767 }
768 protected function rollbackToSavepoint($level)
769 {
770 if ($this->pdo->inTransaction()) {
771 $this->pdo->exec('ROLLBACK TO SAVEPOINT xmldb_sp_' . (int) $level);
772 }
773 }
774}
775
776/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
static parseAttributeString($value)
parses a stored attribute string into a name => value map; used by getAttributes and by the batch val...
Definition Document.php:597
clearTables()
Removes SQL tables.
Definition XmlDb.php:630
endTransaction()
closes the innermost open transaction level
Definition XmlDb.php:680
getDocuments($name="", $type="")
gets available documents in database
Definition XmlDb.php:130
rollBackTransaction()
rolls back to the start of the innermost open transaction level, keeping outer transaction levels (if...
Definition XmlDb.php:704
createDoc($doctype='Depage\XmlDb\XmlDoctypes\Base', $docName=null)
CreateDoc.
Definition XmlDb.php:514
docExists($docIdOrName)
gets the doc-id of a xml-document by name or id and checks if the document exists
Definition XmlDb.php:79
__get($property)
Get properties (basically read-only).
Definition XmlDb.php:65
getNodeIdsByXpath($xpath, $docId=null)
gets node_ids by xpath
Definition XmlDb.php:298
getDocByNodeId($nodeId)
Get xmlDb\document.
Definition XmlDb.php:192
getDocXml($docIdOrName, $addIdAttribute=true)
gets a document as a DOMDocument
Definition XmlDb.php:235
getDocXmlXpath($docIdOrName, $xpath, $addIdAttribute=true)
gets the first node of a document that matches an xpath query
Definition XmlDb.php:254
duplicateDoc($docNameOrId, $newName=null)
duplicateDoc
Definition XmlDb.php:545
updateSchema()
updateSchema
Definition XmlDb.php:602
beginTransactionNonAltering()
opens a transaction that only reads the documents
Definition XmlDb.php:660
rollbackToSavepoint($level)
Definition XmlDb.php:768
getDoc($docIdOrName)
Get xmlDb\document.
Definition XmlDb.php:176
removeDoc($docId)
Definition XmlDb.php:565
getSubdocByXpath($xpath, $addIdAttribute=true)
gets document by xpath.
Definition XmlDb.php:275
__construct($tablePrefix, $pdo, $cache, $options=[])
creates an instance of the xmlDb
Definition XmlDb.php:43
releaseSavepoint($level)
Definition XmlDb.php:762
getNodeIdsByXpathDom($xpath, $docs=[])
Definition XmlDb.php:412
getIndexNodeData($nodeIds)
loads the document id and the attributes of the given nodes with a single query (used by the Indexer ...
Definition XmlDb.php:437
getIndexTextContent($nodeIds)
concatenates the descendant text nodes of the given nodes with a single query, in document order; tha...
Definition XmlDb.php:476
rollBack()
resets the transaction state completely and rolls back the database transaction; use to recover after...
Definition XmlDb.php:729
createSavepoint($level)
Definition XmlDb.php:756
getDocInfo($docIdOrName)
getDocInfo
Definition XmlDb.php:219
beginTransactionAltering()
opens a transaction that changes the documents
Definition XmlDb.php:646
A namespace prefix and the URI it is bound to.
Definition XmlNs.php:9
Parses XPath strings into XpathElement objects.
const SORTKEY_PAD
number of decimal digits used for the position part of the recursive CTE sortkey; limits the tree dep...
Interface of a read only accessor for xml documents.
Definition XmlGetter.php:12