ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilMaterializedPathTree.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
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 protected 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.
Base class for materialize path based trees Based on implementation of Werner Randelshofer.
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)
Tree class data representation in hierachical trees using the Nested Set Model with Gaps by Joe Celco...
const RELATION_EQUALS
const RELATION_PARENT
const RELATION_NONE
const RELATION_SIBLING
const RELATION_CHILD
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:30
$res
Definition: ltiservices.php:69
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
global $DIC
Definition: shib_login.php:26
$q
Definition: shib_logout.php:23