ILIAS  release_8 Revision v8.25-1-g13de6a5eca6
class.ilMaterializedPathTree.php
Go to the documentation of this file.
1<?php
2
3declare(strict_types=1);
4
29{
30 private const MAXIMUM_POSSIBLE_DEPTH = 100;
31
32 protected ilTree $tree;
33 protected ilDBInterface $db;
34 protected ilLogger $logger;
35
40 public function __construct(ilTree $a_tree)
41 {
42 global $DIC;
43
44 $this->tree = $a_tree;
45 $this->db = $DIC->database();
46 if (ilContext::getType() != "") {
47 $this->logger = $DIC->logger()->tree();
48 }
49 }
50
54 protected function getMaximumPossibleDepth(): int
55 {
57 }
58
62 public function getTree(): \ilTree
63 {
64 return $this->tree;
65 }
66
72 public function getSubTreeIds(int $a_node_id): array
73 {
74 $node = $this->getTree()->getNodeTreeData($a_node_id);
75 $query = 'SELECT child FROM ' . $this->getTree()->getTreeTable() . ' ' .
76 'WHERE path BETWEEN ' .
77 $this->db->quote($node['path'], 'text') . ' AND ' .
78 $this->db->quote($node['path'] . '.Z', 'text') . ' ' .
79 'AND child != %s ' .
80 'AND ' . $this->getTree()->getTreePk() . ' = %s';
81
82 $res = $this->db->queryF(
83 $query,
84 array('integer', 'integer'),
85 array($a_node_id, $this->getTree()->getTreeId())
86 );
87 $childs = [];
88 while ($row = $this->db->fetchAssoc($res)) {
89 $childs[] = (int) $row['child'];
90 }
91 return $childs;
92 }
93
98 public function getRelation(array $a_node_a, array $a_node_b): int
99 {
100 if ($a_node_a === [] || $a_node_b === []) {
102 }
103 if ($a_node_a['child'] == $a_node_b['child']) {
105 }
106 if (stripos($a_node_a['path'], $a_node_b['path'] . '.') === 0) {
108 }
109 if (stripos($a_node_b['path'], $a_node_a['path'] . '.') === 0) {
111 }
112 $path_a = substr($a_node_a['path'], 0, strrpos($a_node_a['path'], '.'));
113 $path_b = substr($a_node_b['path'], 0, strrpos($a_node_b['path'], '.'));
114 if ($a_node_a['path'] && $path_a === $path_b) {
116 }
118 }
119
123 public function getTrashSubTreeQuery(
124 array $a_node,
125 array $a_types,
126 bool $a_force_join_reference = true,
127 array $a_fields = []
128 ): string {
129 $type_str = '';
130 if (is_array($a_types)) {
131 if ($a_types) {
132 $type_str = "AND " . $this->db->in(
133 $this->getTree()->getObjectDataTable() . ".type",
134 $a_types,
135 false,
136 "text"
137 );
138 }
139 }
140
141 $join = '';
142 if ($type_str || $a_force_join_reference) {
143 $join = $this->getTree()->buildJoin();
144 }
145
146 $fields = '* ';
147 if (count($a_fields)) {
148 $fields = implode(',', $a_fields);
149 }
150
151 // @todo order by
152 $query = 'SELECT ' .
153 $fields . ' ' .
154 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
155 $join . ' ' .
156 'WHERE ' . $this->getTree()->getTreeTable() . '.path ' .
157 'BETWEEN ' .
158 $this->db->quote($a_node['path'], 'text') . ' AND ' .
159 $this->db->quote($a_node['path'] . '.Z', 'text') . ' ' .
160 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' < 0 ' .
161 $type_str . ' ' .
162 'ORDER BY ' . $this->getTree()->getTreeTable() . '.path';
163
164 return $query;
165 }
166
175 public function getSubTreeQuery(
176 array $a_node,
177 array $a_types = [],
178 bool $a_force_join_reference = true,
179 array $a_fields = []
180 ): string {
181 $type_str = '';
182 if (count($a_types)) {
183 if ($a_types) {
184 $type_str = "AND " . $this->db->in(
185 $this->getTree()->getObjectDataTable() . ".type",
186 $a_types,
187 false,
188 "text"
189 );
190 }
191 }
192
193 $join = '';
194 if ($type_str || $a_force_join_reference) {
195 $join = $this->getTree()->buildJoin();
196 }
197
198 $fields = '* ';
199 if (count($a_fields)) {
200 $fields = implode(',', $a_fields);
201 }
202
203 // @todo order by
204 $query = 'SELECT ' .
205 $fields . ' ' .
206 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
207 $join . ' ' .
208 'WHERE ' . $this->getTree()->getTreeTable() . '.path ' .
209 'BETWEEN ' .
210 $this->db->quote($a_node['path'], 'text') . ' AND ' .
211 $this->db->quote($a_node['path'] . '.Z', 'text') . ' ' .
212 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = ' . $this->db->quote(
213 $this->getTree()->getTreeId(),
214 'integer'
215 ) . ' ' .
216 $type_str . ' ' .
217 'ORDER BY ' . $this->getTree()->getTreeTable() . '.path';
218
219 return $query;
220 }
221
225 public function getPathIds(int $a_endnode, int $a_startnode = 0): array
226 {
227 $this->db->setLimit(1, 0);
228 $query = 'SELECT path FROM ' . $this->getTree()->getTreeTable() . ' ' .
229 'WHERE child = ' . $this->db->quote($a_endnode, 'integer') . ' ';
230 $res = $this->db->query($query);
231
232 $path = "";
233 while ($row = $this->db->fetchAssoc($res)) {
234 $path = (string) $row['path'];
235 }
236
237 $pathIds = array_map('intval', explode('.', $path));
238
239 if ($a_startnode != 0) {
240 while (count($pathIds) > 0 && $pathIds[0] != $a_startnode) {
241 array_shift($pathIds);
242 }
243 }
244 return $pathIds;
245 }
246
250 public function insertNode(int $a_node_id, int $a_parent_id, int $a_pos): void
251 {
252 $insert_node_callable = function (ilDBInterface $ilDB) use ($a_node_id, $a_parent_id, $a_pos): void {
253 // get path and depth of parent
254 $this->db->setLimit(1, 0);
255
256 $res = $this->db->queryF(
257 'SELECT parent, depth, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
258 'WHERE child = %s ' . ' ' .
259 'AND ' . $this->getTree()->getTreePk() . ' = %s',
260 array('integer', 'integer'),
261 array($a_parent_id, $this->getTree()->getTreeId())
262 );
263
264 $r = $this->db->fetchObject($res);
265
266 if ($r->parent === null) {
267 $this->logger->logStack(ilLogLevel::ERROR);
268 throw new ilInvalidTreeStructureException('Parent node not found in tree');
269 }
270
271 if ($r->depth >= $this->getMaximumPossibleDepth()) {
272 $this->logger->logStack(ilLogLevel::ERROR);
273 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
274 }
275
276 $parentPath = $r->path;
277 $depth = (int) $r->depth + 1;
278 $lft = 0;
279 $rgt = 0;
280
281 $this->db->insert(
282 $this->getTree()->getTreeTable(),
283 array($this->getTree()->getTreePk() => array('integer', $this->getTree()->getTreeId()),
284 'child' => array('integer', $a_node_id),
285 'parent' => array('integer', $a_parent_id),
286 'lft' => array('integer', $lft),
287 'rgt' => array('integer', $rgt),
288 'depth' => array('integer', $depth),
289 'path' => array('text', $parentPath . "." . $a_node_id)
290 )
291 );
292 };
293
294 // use ilAtomQuery to lock tables if tree is main tree
295 // otherwise just call this closure without locking
296 if ($this->getTree()->__isMainTree()) {
297 $ilAtomQuery = $this->db->buildAtomQuery();
298 $ilAtomQuery->addTableLock("tree");
299 $ilAtomQuery->addQueryCallable($insert_node_callable);
300 $ilAtomQuery->run();
301 } else {
302 $insert_node_callable($this->db);
303 }
304 }
305
309 public function deleteTree(int $a_node_id): void
310 {
311 $delete_tree_callable = function (ilDBInterface $ilDB) use ($a_node_id): void {
312 $query = 'SELECT * FROM ' . $this->getTree()->getTreeTable() . ' ' .
313 'WHERE ' . $this->getTree()->getTreeTable() . '.child = %s ' .
314 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = %s ';
315 $res = $this->db->queryF($query, array('integer', 'integer'), array(
316 $a_node_id,
317 $this->getTree()->getTreeId()
318 ));
319 $node = $this->db->fetchAssoc($res);
320
321 if($node === null) {
322 return; //Nothing to delete. $node does not exists
323 }
324
325 $query = 'DELETE FROM ' . $this->getTree()->getTreeTable() . ' ' .
326 'WHERE path BETWEEN ' . $this->db->quote($node['path'], 'text') . ' ' .
327 'AND ' . $this->db->quote($node['path'] . '.Z', 'text') . ' ' .
328 'AND ' . $this->getTree()->getTreePk() . ' = ' . $this->db->quote(
329 $this->getTree()->getTreeId(),
330 'integer'
331 );
332 $this->db->manipulate($query);
333 };
334
335 // get lft and rgt values. Don't trust parameter lft/rgt values of $a_node
336 if ($this->getTree()->__isMainTree()) {
337 $ilAtomQuery = $this->db->buildAtomQuery();
338 $ilAtomQuery->addTableLock('tree');
339 $ilAtomQuery->addQueryCallable($delete_tree_callable);
340 $ilAtomQuery->run();
341 } else {
342 $delete_tree_callable($this->db);
343 }
344 }
345
349 public function moveToTrash(int $a_node_id): void
350 {
351 $move_to_trash_callable = function (ilDBInterface $ilDB) use ($a_node_id): void {
352 $node = $this->getTree()->getNodeTreeData($a_node_id);
353
354 // Set the nodes deleted (negative tree id)
355 $this->db->manipulateF(
356 '
357 UPDATE ' . $this->getTree()->getTreeTable() . ' ' .
358 'SET tree = %s' . ' ' .
359 'WHERE ' . $this->getTree()->getTreePk() . ' = %s ' .
360 'AND path BETWEEN %s AND %s',
361 array('integer', 'integer', 'text', 'text'),
362 array(-$a_node_id, $this->getTree()->getTreeId(), $node['path'], $node['path'] . '.Z')
363 );
364 };
365
366 // use ilAtomQuery to lock tables if tree is main tree
367 // otherwise just call this closure without locking
368 if ($this->getTree()->__isMainTree()) {
369 $ilAtomQuery = $this->db->buildAtomQuery();
370 $ilAtomQuery->addTableLock("tree");
371
372 $ilAtomQuery->addQueryCallable($move_to_trash_callable);
373
374 $ilAtomQuery->run();
375 } else {
376 $move_to_trash_callable($this->db);
377 }
378 }
379
384 public function moveTree(int $a_source_id, int $a_target_id, int $a_position): void
385 {
386 $move_tree_callable = function (ilDBInterface $ilDB) use ($a_source_id, $a_target_id, $a_position): void {
387 // Receive node infos for source and target
388 $this->db->setLimit(2, 0);
389
390 $res = $this->db->query(
391 'SELECT depth, child, parent, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
392 'WHERE ' . $this->db->in('child', array($a_source_id, $a_target_id), false, 'integer') . ' ' .
393 'AND tree = ' . $this->db->quote($this->getTree()->getTreeId(), 'integer')
394 );
395
396 // Check in tree
397 if ($this->db->numRows($res) != 2) {
398 $this->logger->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
399 throw new InvalidArgumentException('Error moving subtree');
400 }
401
402 $source_depth = $target_depth = 0;
403 $source_path = $target_path = '';
404 $source_parent = 0;
405 while ($row = $this->db->fetchObject($res)) {
406 if ($row->child == $a_source_id) {
407 $source_path = $row->path;
408 $source_depth = $row->depth;
409 $source_parent = $row->parent;
410 } else {
411 $target_path = $row->path;
412 $target_depth = $row->depth;
413 }
414 }
415
416 if ($target_depth >= $source_depth) {
417 // We move nodes deeper into the tree. Therefore we need to
418 // check whether we might exceed the maximal path length.
419 // We use FOR UPDATE here, because we don't want anyone to
420 // insert new nodes while we move the subtree.
421
422 $res = $this->db->queryF(
423 'SELECT MAX(depth) max_depth ' .
424 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
425 'WHERE path BETWEEN %s AND %s' . ' ' .
426 'AND tree = %s ',
427 array('text', 'text', 'integer'),
428 array($source_path, $source_path . '.Z', $this->getTree()->getTreeId())
429 );
430
431 $row = $this->db->fetchObject($res);
432
433 if ($row->max_depth - $source_depth + $target_depth + 1 > $this->getMaximumPossibleDepth()) {
434 $this->logger->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
435 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
436 }
437 }
438 // Check target not child of source
439 if ((substr($target_path . '.', 0, strlen($source_path)) . '.') == $source_path . '.') {
440 $this->logger->logStack(ilLogLevel::ERROR, 'Target is child of source');
441 throw new InvalidArgumentException('Error moving subtree: target is child of source');
442 }
443 $depth_diff = $target_depth - $source_depth + 1;
444
445 // move subtree:
446 $query =
447 'UPDATE ' . $this->getTree()->getTreeTable() . ' ' .
448 'SET parent = CASE WHEN parent = ' . $this->db->quote($source_parent, 'integer') . ' ' .
449 'THEN ' . $this->db->quote($a_target_id, 'integer') . ' ' .
450 'ELSE parent END, path = ' .
451 $this->db->concat(array(
452 array($this->db->quote($target_path, 'text'), 'text'),
453 array($this->db->substr('path', strrpos('.' . $source_path, '.')), 'text')
454 )) . ' ' .
455 ',depth = depth + ' . $this->db->quote($depth_diff, 'integer') . ' ' .
456 'WHERE path BETWEEN ' . $this->db->quote($source_path, 'text') . ' ' .
457 'AND ' . $this->db->quote($source_path . '.Z', 'text') . ' ';
458
459 if (!$this->getTree()->__isMainTree()) {
460 $query .= ('AND ' . $this->db->quote($this->getTree()->getTreeId(), \ilDBConstants::T_INTEGER));
461 }
462 $this->db->manipulate($query);
463 };
464
465 if ($this->getTree()->__isMainTree()) {
466 $ilAtomQuery = $this->db->buildAtomQuery();
467 $ilAtomQuery->addTableLock("tree");
468 $ilAtomQuery->addQueryCallable($move_tree_callable);
469 $ilAtomQuery->run();
470 } else {
471 $move_tree_callable($this->db);
472 }
473 }
474
475 public static function createFromParentRelation(ilDBInterface $db): void
476 {
477 $result = $db->queryF('SELECT DISTINCT * FROM tree WHERE parent = %s', ['integer'], [0]);
478
479 while ($row = $db->fetchAssoc($result)) {
480 self::createMaterializedPath($db, 0, '');
481 }
482 }
483
484 private static function createMaterializedPath(ilDBInterface $db, int $parent, string $parentPath): void
485 {
486 $q = ' UPDATE tree
487 SET path = CONCAT(COALESCE(' . $db->quote($parentPath, 'text') . ', \'\'), COALESCE( ' . $db->cast(
488 "child",
489 "text"
490 ) . ' , \'\')) WHERE parent = %s';
491 $db->manipulateF($q, ['integer'], [$parent]);
492 $result = $db->queryF('SELECT child FROM tree WHERE parent = %s', ['integer'], [$parent]);
493
494 while ($row = $db->fetchAssoc($result)) {
495 self::createMaterializedPath(
496 $db,
497 (int) $row['child'],
498 $parentPath . $row['child'] . '.'
499 );
500 }
501 }
502
507 public function getSubtreeInfo(int $a_endnode_id): array
508 {
509 if ($this->getTree()->__isMainTree() && $this->getTree()->getTreeId() == 1) {
510 $treeClause1 = '';
511 $treeClause2 = '';
512 } else {
513 $treeClause1 = ' AND t1.' . $this->getTree()->getTreePk() . ' = ' . $this->db->quote(
514 $this->getTree()->getTreeId(),
515 'integer'
516 );
517 $treeClause2 = ' AND t2.' . $this->getTree()->getTreePk() . ' = ' . $this->db->quote(
518 $this->getTree()->getTreeId(),
519 'integer'
520 );
521 }
522
523 // first query for the path of the given node
524 $query = "
525 SELECT t1." . $this->getTree()->getTreePk() . ", t1.path
526 FROM " . $this->getTree()->getTreeTable() . " t1
527 WHERE t1.child = " . $this->db->quote($a_endnode_id, 'integer') .
528 $treeClause1;
529
530 $res = $this->db->query($query);
531 $row = $this->db->fetchAssoc($res);
532 if ($row[$this->getTree()->getTreePk()] ?? null == $this->getTree()->getTreeId()) {
533 $path = (string) $row['path'];
534 } else {
535 return [];
536 }
537
538 // then query for the nodes in that path
539 $query = "SELECT t2." . $this->getTree()->getTreePk() . ", t2.child child, t2.parent parent, type, t2.path path " .
540 "FROM " . $this->getTree()->getTreeTable() . " t2 " .
541 "JOIN " . $this->getTree()->getTableReference() . " obr ON t2.child = obr.ref_id " .
542 "JOIN " . $this->getTree()->getObjectDataTable() . " obd ON obr.obj_id = obd.obj_id " .
543 "WHERE t2.path BETWEEN " . $this->db->quote($path, 'text') . " AND " . $this->db->quote(
544 $path . '.Z',
545 'text'
546 ) .
547 $treeClause2 . ' ' .
548 "ORDER BY t2.path";
549
550 $res = $this->db->query($query);
551 $nodes = [];
552 while ($row = $this->db->fetchAssoc($res)) {
553 // filter out deleted items if tree is repository
554 if ($row[$this->getTree()->getTreePk()] != $this->getTree()->getTreeId()) {
555 continue;
556 }
557
558 $nodes[$row['child']]['child'] = (int) $row['child'];
559 $nodes[$row['child']]['parent'] = (int) $row['parent'];
560 $nodes[$row['child']]['type'] = (string) $row['type'];
561 $nodes[$row['child']]['path'] = (string) $row['path'];
562 }
563
564 $depth_first_compare = static function (array $a, array $b): int {
565 $a_exploded = explode('.', $a['path']);
566 $b_exploded = explode('.', $b['path']);
567
568 $a_padded = '';
569 foreach ($a_exploded as $num) {
570 $a_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
571 }
572 $b_padded = '';
573 foreach ($b_exploded as $num) {
574 $b_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
575 }
576
577 return strcasecmp($a_padded, $b_padded);
578 };
579
580 uasort($nodes, $depth_first_compare);
581
582 return $nodes;
583 }
584
588 public function validateParentRelations(): array
589 {
590 $query = 'select ' . $this->getTree()->getTreePk() .', child from ' . $this->getTree()->getTreeTable() . ' child where not exists ' .
591 '( ' .
592 'select child from ' . $this->getTree()->getTreeTable() . ' parent where child.parent = parent.child and ' .
593 '(child.path BETWEEN parent.path AND CONCAT(parent.path,' . $this->db->quote('Z', 'text') . ') )' . ')' .
594 'and ' . $this->getTree()->getTreePk() . ' = ' . $this->getTree()->getTreeId() . ' and child <> 1';
595 $res = $this->db->query($query);
596 $failures = array();
597 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
598 $failures[] = $row[$this->getTree()->getTreePk()];
599 }
600 return $failures;
601 }
602}
static getType()
Get context type.
Thrown if invalid tree strucutes are found.
Component logger with individual log levels by component id.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
deleteTree(int $a_node_id)
Delete tree.
getTrashSubTreeQuery(array $a_node, array $a_types, bool $a_force_join_reference=true, array $a_fields=[])
Get subtree query for trashed tree items.
validateParentRelations()
Validate the parent relations of the tree implementation For nested set, validate the lft,...
static createMaterializedPath(ilDBInterface $db, int $parent, string $parentPath)
__construct(ilTree $a_tree)
Constructor.
getMaximumPossibleDepth()
Get maximum possible depth.
getPathIds(int $a_endnode, int $a_startnode=0)
Get path ids from a startnode to a given endnode.int[]
moveTree(int $a_source_id, int $a_target_id, int $a_position)
Move a source subtree to target.
insertNode(int $a_node_id, int $a_parent_id, int $a_pos)
getSubTreeQuery(array $a_node, array $a_types=[], bool $a_force_join_reference=true, array $a_fields=[])
Get subtree query.
moveToTrash(int $a_node_id)
Move subtree to trash.
getRelation(array $a_node_a, array $a_node_b)
Get relation of two nodes.
getSubTreeIds(int $a_node_id)
Get subtree ids.
static createFromParentRelation(ilDBInterface $db)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const RELATION_EQUALS
const RELATION_PARENT
const RELATION_NONE
const RELATION_SIBLING
const RELATION_CHILD
global $DIC
Definition: feed.php:28
Interface ilDBInterface.
cast(string $a_field_name, string $a_dest_type)
quote($value, string $type)
manipulateF(string $query, array $types, array $values)
fetchAssoc(ilDBStatement $statement)
queryF(string $query, array $types, array $values)
Interface for tree implementations Currrently nested set or materialized path.
$path
Definition: ltiservices.php:32
$res
Definition: ltiservices.php:69
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
$query