depage-xmldb
v2.3.1
XML documents in a relational database
Toggle main menu visibility
Loading...
Searching...
No Matches
Positions.php
Go to the documentation of this file.
1
<?php
2
12
13
namespace
Depage\XmlDb\Helpers;
14
15
use Depage\XmlDb\Exceptions\XmlDbException;
16
35
class
Positions
36
{
40
public
const
POS_GAP
= 65536;
41
46
public
const
POS_MIN_GAP
= 16;
47
55
public
const
SCRATCH_OFFSET
= 1125899906842624;
56
57
protected
$pdo
;
58
66
public
function
__construct
(
67
$pdo
,
68
protected
string
$table,
69
protected
$docId,
70
protected
int
$batchSize =
CaseUpdate::DEFAULT_SIZE
71
) {
72
$this->pdo =
$pdo
;
73
}
74
82
public
function
siblings
($parentId): array
83
{
84
$query = $this->pdo->prepare(
85
"SELECT xml.id, xml.pos
86
FROM {$this->table} AS xml
87
WHERE xml.id_parent = :parent_id AND xml.id_doc = :doc_id
88
ORDER BY xml.pos ASC, xml.id ASC"
,
89
);
90
$query->execute([
91
'parent_id'
=> $parentId,
92
'doc_id'
=> $this->docId,
93
]);
94
95
return
$query->fetchAll(\PDO::FETCH_OBJ);
96
}
97
108
private
function
siblingsWithout($parentId, $excludeId =
null
): array
109
{
110
$siblings = $this->
siblings
($parentId);
111
112
if
(!is_null($excludeId)) {
113
foreach
($siblings as $i => $sibling) {
114
if
($sibling->id == $excludeId) {
115
unset($siblings[$i]);
116
$siblings = array_values($siblings);
117
break
;
118
}
119
}
120
}
121
122
return
$siblings;
123
}
124
139
private
function
clampIndex($targetIndex,
int
$numSiblings): int
140
{
141
if
(!is_numeric($targetIndex) || (
int
) $targetIndex < 0) {
142
return
$numSiblings;
143
}
144
145
return
min((
int
) $targetIndex, $numSiblings);
146
}
147
161
public
function
normalizeIndex
($parentId, $targetIndex, $excludeId =
null
)
162
{
163
return
$this->clampIndex(
164
$targetIndex,
165
count($this->siblingsWithout($parentId, $excludeId))
166
);
167
}
168
179
public
function
indexById
($id)
180
{
181
$query = $this->pdo->prepare(
182
"SELECT xml.id_parent, xml.pos
183
FROM {$this->table} AS xml
184
WHERE xml.id = :id AND xml.id_doc = :doc_id
185
LIMIT 1"
,
186
);
187
$query->execute([
188
'id'
=> $id,
189
'doc_id'
=> $this->docId,
190
]);
191
192
if
(!($node = $query->fetchObject())) {
193
return
false
;
194
}
195
196
$query = $this->pdo->prepare(
197
"SELECT COUNT(*) AS sibling_index
198
FROM {$this->table} AS xml
199
WHERE xml.id_parent = :parent_id AND xml.id_doc = :doc_id AND xml.pos < :pos"
,
200
);
201
$query->execute([
202
'parent_id'
=> $node->id_parent,
203
'pos'
=> $node->pos,
204
'doc_id'
=> $this->docId,
205
]);
206
$result = $query->fetchObject();
207
208
return
(
int
) $result->sibling_index;
209
}
210
217
public
function
appendIndex
($parentId)
218
{
219
$query = $this->pdo->prepare(
220
"SELECT COUNT(*) AS pos
221
FROM {$this->table} AS xml
222
WHERE xml.id_parent = :target_id AND id_doc = :doc_id"
,
223
);
224
$query->execute([
225
'target_id'
=> $parentId,
226
'doc_id'
=> $this->docId,
227
]);
228
229
if
($object = $query->fetchObject()) {
230
$result = $object->pos;
231
}
else
{
232
$result =
null
;
233
}
234
235
return
$result;
236
}
237
253
public
function
positionForIndex
($parentId, $index, $excludeId =
null
)
254
{
255
$siblings = $this->siblingsWithout($parentId, $excludeId);
256
257
$numSiblings = count($siblings);
258
$index = $this->clampIndex($index, $numSiblings);
259
260
if
($index >= $numSiblings) {
261
// appending does not need room between two neighbours
262
$lastPos = $numSiblings > 0 ? (int) $siblings[$numSiblings - 1]->pos : 0;
263
264
return
$lastPos + self::POS_GAP;
265
}
266
267
$prevPos = $index > 0 ? (int) $siblings[$index - 1]->pos : 0;
268
$nextPos = (int) $siblings[$index]->pos;
269
270
if
($nextPos - $prevPos - 1 < self::POS_MIN_GAP) {
271
$this->
rebalance
($parentId);
272
273
return
$this->
positionForIndex
($parentId, $index, $excludeId);
274
}
275
276
return
intdiv($prevPos + $nextPos, 2);
277
}
278
288
public
function
rebalance
($parentId, $order =
null
): void
289
{
290
$siblings = $this->
siblings
($parentId);
291
292
if
(count($siblings) == 0) {
293
return
;
294
}
295
296
$current = [];
297
foreach
($siblings as $sibling) {
298
$current[] = (int) $sibling->id;
299
}
300
301
if
(is_null($order)) {
302
// keep the order that is stored in the database
303
$ids = $current;
304
}
else
{
305
// reorder into the given child order (used by the patch
306
// applier)
307
$ids = array_map(
'intval'
, $order);
308
309
if
(count($ids) != count($current) || array_diff($ids, $current) || array_diff($current, $ids)) {
310
throw
new
XmlDbException
(
"Can not reorder the children of node {$parentId}, the node list does not match."
);
311
}
312
}
313
314
// the unique sibling index is checked row by row, so the positions
315
// have to be moved out of the way first: adding a big offset in
316
// descending order cannot collide with a sibling that has not
317
// been updated yet, and the offset is big enough to keep the
318
// scratch positions clear of the final positions that are
319
// assigned in the update below
320
$this->pdo->exec(
321
"UPDATE {$this->table}
322
SET pos = pos + "
. self::SCRATCH_OFFSET .
"
323
WHERE id_doc = "
. (
int
) $this->docId .
" AND id_parent = "
. (
int
) $parentId .
"
324
ORDER BY pos DESC"
325
);
326
327
// every sibling gets a position of its own, so writing the final
328
// positions in batches cannot collide with the unique sibling
329
// index either
330
$positions =
new
CaseUpdate
($this->pdo, $this->table, $this->batchSize);
331
foreach
($ids as $index => $id) {
332
$positions->add($id, ($index + 1) * self::POS_GAP);
333
}
334
$positions->write(
'pos'
, [
'id_doc'
=> $this->docId]);
335
}
336
}
337
338
/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
Depage\XmlDb\Exceptions\XmlDbException
Definition
XmlDbException.php:10
Depage\XmlDb\Helpers\CaseUpdate
Definition
CaseUpdate.php:27
Depage\XmlDb\Helpers\CaseUpdate\DEFAULT_SIZE
const DEFAULT_SIZE
number of rows written with a single statement
Definition
CaseUpdate.php:33
Depage\XmlDb\Helpers\Positions
Definition
Positions.php:36
Depage\XmlDb\Helpers\Positions\normalizeIndex
normalizeIndex($parentId, $targetIndex, $excludeId=null)
gets the index a node is inserted at in the child list of a parent node for the targetIndex a caller ...
Definition
Positions.php:161
Depage\XmlDb\Helpers\Positions\indexById
indexById($id)
gets the index of a node in the child list of its parent node (number of siblings before it),...
Definition
Positions.php:179
Depage\XmlDb\Helpers\Positions\$pdo
$pdo
Definition
Positions.php:57
Depage\XmlDb\Helpers\Positions\POS_MIN_GAP
const POS_MIN_GAP
the child list is rebalanced once the space between two neighbours drops below this gap
Definition
Positions.php:46
Depage\XmlDb\Helpers\Positions\POS_GAP
const POS_GAP
distance between the sparse positions of sibling nodes
Definition
Positions.php:40
Depage\XmlDb\Helpers\Positions\SCRATCH_OFFSET
const SCRATCH_OFFSET
adding this offset to all positions of a child list (in descending order, before assigning the final ...
Definition
Positions.php:55
Depage\XmlDb\Helpers\Positions\__construct
__construct( $pdo, protected string $table, protected $docId, protected int $batchSize=CaseUpdate::DEFAULT_SIZE)
Definition
Positions.php:66
Depage\XmlDb\Helpers\Positions\positionForIndex
positionForIndex($parentId, $index, $excludeId=null)
calculates the sparse pos value for a node to be inserted at the given index of a parent node's child...
Definition
Positions.php:253
Depage\XmlDb\Helpers\Positions\siblings
siblings($parentId)
gets id and pos of all children of a parent node in document order
Definition
Positions.php:82
Depage\XmlDb\Helpers\Positions\appendIndex
appendIndex($parentId)
gets the index a child would get when appended to the child list, i.e.
Definition
Positions.php:217
Depage\XmlDb\Helpers\Positions\rebalance
rebalance($parentId, $order=null)
renumbers the children of a parent node to have an even distance of self\POS_GAP between their positi...
Definition
Positions.php:288
Helpers
Positions.php
Generated on
for depage-xmldb by
doxygen
1.18.0
For an overview of all docs, go to
docs.depage.net