depage-xmldb stores XML documents in a relational database and gives you an object oriented API to read, query and modify them.
This page walks through the setup and through the everyday operations. The XPath dialect, the doctype handlers and the version history are described on their own pages.
The library needs PHP 8.3 or newer and MySQL 8.0 or MariaDB 10.2 or newer. It builds on three sibling libraries, which are installed as dependencies:
| Library | Purpose |
|---|---|
| depage/db | PDO wrapper and schema migration |
| depage/xml | serializable DOMDocument subclass |
| depage/cache | cache with interchangeable backends |
The first argument of the constructor is the table prefix. The library uses three tables, which are derived from it:
| Table | Content |
|---|---|
| dp_xmldocs | one row per document: name, doctype, root node, namespaces |
| dp_xmltree | the nodes of all documents |
| dp_history | the versions of the documents |
The prefix of the Pdo object and the table prefix of the XmlDb object are independent of each other: the Pdo prefix is used for the tables of other libraries (for example _auth_user), the XmlDb prefix for its own tables.
The optional fourth argument contains settings of the whole instance:
userId is written into the lastchange_uid column of a document whenever it is changed, and is the default for Document::updateLastChange().
updateSchema() reads the SQL files in the Sql directory and applies the statements that are not yet applied, in the order of their version numbers. It is idempotent, so it can be called on every deployment.
A document is addressed by its name or by its id:
The first argument of createDoc() is the class name of the doctype handler that is responsible for the document. It is stored in the type column of the document and instantiated on demand, see doctype handlers. If no name is given, a unique one is generated.
The same queries are available on the XmlDb object, where they search in all documents or in one document:
getSubdocByXpath() returns the first matching node only. Use getNodeIdsByXpath() to get all of them.
The information about a document itself is available as an object:
The library stores the id of a node in the document itself, using the db: namespace (http://cms.depagecms.net/ns/database):
| Attribute | Meaning |
|---|---|
| db:id | the database id of the node, the only way to address it |
| db:lastchange | time of the last change of the document |
| db:lastchangeUid | id of the user who changed the document last |
| db:dataid | an optional id of your own, it is never interpreted by the library |
The db:id attributes are added when a document is read and can be removed again with getXml(false) and getSubdocByNodeId($id, false). They are also the mechanism that makes incremental saves work: a node that carries its db:id is recognized as the node it was before.
save() deletes all nodes of the document and writes it again. The node ids of the old document are lost, unless the new document carries them.
savePatch() does the same, but keeps the parts of the tree that did not change:
The document that is passed to savePatch() should carry the db:id attributes of the current document, for example the result of getXml(). Nodes without a db:id are matched against the remaining children of their parent by position. When the document can not be patched — the root node was replaced, the document carries no ids or too much has changed — a full save() is done instead. getLastPatchStats() tells you which of the two happened ('fallback' => true).
All of these methods work with the db:id of a node:
That last argument is the targetIndex: the 0-based index the node gets in the list of the children of its new parent. -1 appends the node at the end and an index behind the last child is clamped to the end. The index counts all children of the target node, so a text node between two elements counts too — getIndexById() returns the same index a targetIndex refers to.
The same targetIndex is used by addNode, addNodeByName, moveNode and copyNode; moveNode and moveNodeBefore/moveNodeAfter resolve it against the child list without the node that is moved, so moveNode($id, $parent, $i) always leaves the node at index $i, whether it moves towards the front or the back of its own child list.
Reading single values of a node:
Every change is written inside a transaction. You can open your own to group several changes, and to roll them back if something goes wrong:
Transactions can be nested. The outermost level starts the database transaction, the inner levels use savepoints, so a rollback of an inner level keeps the changes of the outer one.
beginTransactionNonAltering() opens a transaction that only reads. It is used internally for reading a document, so that the data of a document is consistent while it is read.
endTransaction() returns true when the outermost transaction has changed the document. Only then the cache of the document is dropped and the onDocumentChange() hook of the doctype handler is called.
The Indexer resolves an xpath query to the values of the matched nodes. It is meant for search indexes and for reading values without building a DOM.
Every result is an IndexerResult object:
A node without the targeted attribute yields no result at all, a node with an empty attribute yields an empty string. The number of queries does not depend on the number of results.