ILIAS  release_7 Revision v7.30-3-g800a261c036
class.ilMaterializedPathTree.php
Go to the documentation of this file.
1<?php
2/* Copyright (c) 1998-2009 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4include_once './Services/Tree/interfaces/interface.ilTreeImplementation.php';
5
17{
19 private $tree = null;
20
25 public function __construct(ilTree $a_tree)
26 {
27 $this->tree = $a_tree;
28 }
29
30
35 protected function getMaximumPossibleDepth()
36 {
38 }
39
44 public function getTree()
45 {
46 return $this->tree;
47 }
48
54 public function getSubTreeIds($a_node_id)
55 {
56 global $DIC;
57
58 $ilDB = $DIC['ilDB'];
59
60 $node = $this->getTree()->getNodeTreeData($a_node_id);
61 $query = 'SELECT child FROM ' . $this->getTree()->getTreeTable() . ' ' .
62 'WHERE path BETWEEN ' .
63 $ilDB->quote($node['path'], 'text') . ' AND ' .
64 $ilDB->quote($node['path'] . '.Z', 'text') . ' ' .
65 'AND child != %s ' .
66 'AND ' . $this->getTree()->getTreePk() . ' = %s';
67
68 $res = $ilDB->queryF(
69 $query,
70 array('integer', 'integer'),
71 array($a_node_id, $this->getTree()->getTreeId())
72 );
73 while ($row = $ilDB->fetchAssoc($res)) {
74 $childs[] = $row['child'];
75 }
76 return $childs ? $childs : array();
77 }
78
85 public function getRelation($a_node_a, $a_node_b)
86 {
87 if ($a_node_a['child'] == $a_node_b['child']) {
88 ilLoggerFactory::getLogger('tree')->debug('EQUALS');
90 }
91 if (stripos($a_node_a['path'], $a_node_b['path'] . '.') === 0) {
92 ilLoggerFactory::getLogger('tree')->debug('CHILD');
94 }
95 if (stripos($a_node_b['path'], $a_node_a['path'] . '.') === 0) {
96 ilLoggerFactory::getLogger('tree')->debug('PARENT');
98 }
99 $path_a = substr($a_node_a['path'], 0, strrpos($a_node_a['path'], '.'));
100 $path_b = substr($a_node_b['path'], 0, strrpos($a_node_b['path'], '.'));
101
102 ilLoggerFactory::getLogger('tree')->debug('Comparing ' . $path_a . ' ' . 'with ' . $path_b);
103
104 if ($a_node_a['path'] and (strcmp($path_a, $path_b) === 0)) {
105 ilLoggerFactory::getLogger('tree')->debug('SIBLING');
107 }
108
109 ilLoggerFactory::getLogger('tree')->debug('NONE');
111 }
112
116 public function getTrashSubTreeQuery($a_node, $a_types, $a_force_join_reference = true, $a_fields = [])
117 {
118 global $DIC;
119
120 $ilDB = $DIC->database();
121
122 $type_str = '';
123 if (is_array($a_types)) {
124 if ($a_types) {
125 $type_str = "AND " . $ilDB->in($this->getTree()->getObjectDataTable() . ".type", $a_types, false, "text");
126 }
127 } elseif (strlen($a_types)) {
128 $type_str = "AND " . $this->getTree()->getObjectDataTable() . ".type = " . $ilDB->quote($a_types, "text");
129 }
130
131 $join = '';
132 if ($type_str or $a_force_join_reference) {
133 $join = $this->getTree()->buildJoin();
134 }
135
136 $fields = '* ';
137 if (count($a_fields)) {
138 $fields = implode(',', $a_fields);
139 }
140
141 // @todo order by
142 $query = 'SELECT ' .
143 $fields . ' ' .
144 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
145 $join . ' ' .
146 'WHERE ' . $this->getTree()->getTreeTable() . '.path ' .
147 'BETWEEN ' .
148 $ilDB->quote($a_node['path'], 'text') . ' AND ' .
149 $ilDB->quote($a_node['path'] . '.Z', 'text') . ' ' .
150 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' < 0 ' .
151 $type_str . ' ' .
152 'ORDER BY ' . $this->getTree()->getTreeTable() . '.path';
153
154 return $query;
155 }
156
166 public function getSubTreeQuery($a_node, $a_types = '', $a_force_join_reference = true, $a_fields = array())
167 {
168 global $DIC;
169
170 $ilDB = $DIC['ilDB'];
171
172 $type_str = '';
173 if (is_array($a_types)) {
174 if ($a_types) {
175 $type_str = "AND " . $ilDB->in($this->getTree()->getObjectDataTable() . ".type", $a_types, false, "text");
176 }
177 } elseif (strlen($a_types)) {
178 $type_str = "AND " . $this->getTree()->getObjectDataTable() . ".type = " . $ilDB->quote($a_types, "text");
179 }
180
181 $join = '';
182 if ($type_str or $a_force_join_reference) {
183 $join = $this->getTree()->buildJoin();
184 }
185
186 $fields = '* ';
187 if (count($a_fields)) {
188 $fields = implode(',', $a_fields);
189 }
190
191 // @todo order by
192 $query = 'SELECT ' .
193 $fields . ' ' .
194 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
195 $join . ' ' .
196 'WHERE ' . $this->getTree()->getTreeTable() . '.path ' .
197 'BETWEEN ' .
198 $ilDB->quote($a_node['path'], 'text') . ' AND ' .
199 $ilDB->quote($a_node['path'] . '.Z', 'text') . ' ' .
200 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer') . ' ' .
201 $type_str . ' ' .
202 'ORDER BY ' . $this->getTree()->getTreeTable() . '.path';
203
204 return $query;
205 }
206
213 public function getPathIds($a_endnode, $a_startnode = 0)
214 {
215 global $DIC;
216
217 $ilDB = $DIC['ilDB'];
218
219 $ilDB->setLimit(1);
220 $query = 'SELECT path FROM ' . $this->getTree()->getTreeTable() . ' ' .
221 'WHERE child = ' . $ilDB->quote($a_endnode, 'integer') . ' ';
222 $res = $ilDB->query($query);
223
224 $path = null;
225 while ($row = $ilDB->fetchAssoc($res)) {
226 $path = $row['path'];
227 }
228
229 $pathIds = explode('.', $path);
230
231 if ($a_startnode != 0) {
232 while (count($pathIds) > 0 && $pathIds[0] != $a_startnode) {
233 array_shift($pathIds);
234 }
235 }
236 return $pathIds;
237 }
238
247 public function insertNode($a_node_id, $a_parent_id, $a_pos)
248 {
249 global $DIC;
250
251 $ilDB = $DIC['ilDB'];
252
253 $insert_node_callable = function (ilDBInterface $ilDB) use ($a_node_id, $a_parent_id, $a_pos) {
254 // get path and depth of parent
255 $ilDB->setLimit(1);
256
257 $res = $ilDB->queryF(
258 'SELECT parent, depth, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
259 'WHERE child = %s ' . ' ' .
260 'AND ' . $this->getTree()->getTreePk() . ' = %s',
261 array('integer', 'integer'),
262 array($a_parent_id, $this->getTree()->getTreeId())
263 );
264
265
266 $r = $ilDB->fetchObject($res);
267
268 if ($r->parent === null) {
270 throw new ilInvalidTreeStructureException('Parent node not found in tree');
271 }
272
273 if ($r->depth >= $this->getMaximumPossibleDepth()) {
275 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
276 }
277
278 $parentPath = $r->path;
279 $depth = $r->depth + 1;
280 $lft = 0;
281 $rgt = 0;
282
283
284 $ilDB->insert($this->getTree()->getTreeTable(), array($this->getTree()->getTreePk() => array('integer', $this->getTree()->getTreeId()),
285 'child' => array('integer', $a_node_id),
286 'parent' => array('integer', $a_parent_id),
287 'lft' => array('integer', $lft),
288 'rgt' => array('integer', $rgt),
289 'depth' => array('integer', $depth),
290 'path' => array('text', $parentPath . "." . $a_node_id)));
291 };
292
293 // use ilAtomQuery to lock tables if tree is main tree
294 // otherwise just call this closure without locking
295 if ($this-> getTree()->__isMainTree()) {
296 $ilAtomQuery = $ilDB->buildAtomQuery();
297 $ilAtomQuery->addTableLock("tree");
298
299 $ilAtomQuery->addQueryCallable($insert_node_callable);
300
301 $ilAtomQuery->run();
302 } else {
303 $insert_node_callable($ilDB);
304 }
305 }
306
307
314 public function deleteTree($a_node_id)
315 {
316 global $DIC;
317
318 $ilDB = $DIC['ilDB'];
319 $delete_tree_callable = function (ilDBInterface $ilDB) use ($a_node_id) {
320 $query = 'SELECT * FROM ' . $this->getTree()->getTreeTable() . ' ' .
321 'WHERE ' . $this->getTree()->getTreeTable() . '.child = %s ' .
322 'AND ' . $this->getTree()->getTreeTable() . '.' . $this->getTree()->getTreePk() . ' = %s ';
323 $res = $ilDB->queryF($query, array('integer','integer'), array(
324 $a_node_id,
325 $this->getTree()->getTreeId()));
326 $row = $ilDB->fetchAssoc($res);
327
328 $query = 'DELETE FROM ' . $this->getTree()->getTreeTable() . ' ' .
329 'WHERE path BETWEEN ' . $ilDB->quote($row['path'], 'text') . ' ' .
330 'AND ' . $ilDB->quote($row['path'] . '.Z', 'text') . ' ' .
331 'AND ' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
332 $ilDB->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 = $ilDB->buildAtomQuery();
338 $ilAtomQuery->addTableLock('tree');
339 $ilAtomQuery->addQueryCallable($delete_tree_callable);
340 $ilAtomQuery->run();
341 } else {
342 $delete_tree_callable($ilDB);
343 }
344
345 return true;
346 }
347
354 public function moveToTrash($a_node_id)
355 {
356 global $DIC;
357
358 $ilDB = $DIC['ilDB'];
359
360 $move_to_trash_callable = function (ilDBInterface $ilDB) use ($a_node_id) {
361 $node = $this->getTree()->getNodeTreeData($a_node_id);
362
363 // Set the nodes deleted (negative tree id)
364 $ilDB->manipulateF(
365 '
366 UPDATE ' . $this->getTree()->getTreeTable() . ' ' .
367 'SET tree = %s' . ' ' .
368 'WHERE ' . $this->getTree()->getTreePk() . ' = %s ' .
369 'AND path BETWEEN %s AND %s',
370 array('integer', 'integer', 'text', 'text'),
371 array(-$a_node_id, $this->getTree()->getTreeId(), $node['path'], $node['path'] . '.Z')
372 );
373 };
374
375 // use ilAtomQuery to lock tables if tree is main tree
376 // otherwise just call this closure without locking
377 if ($this->getTree()->__isMainTree()) {
378 $ilAtomQuery = $ilDB->buildAtomQuery();
379 $ilAtomQuery->addTableLock("tree");
380
381 $ilAtomQuery->addQueryCallable($move_to_trash_callable);
382
383 $ilAtomQuery->run();
384 } else {
385 $move_to_trash_callable($ilDB);
386 }
387
388 return true;
389 }
390
400 public function moveTree($a_source_id, $a_target_id, $a_position)
401 {
402 global $DIC;
403
404 $ilDB = $DIC['ilDB'];
405
406 $move_tree_callable = function (ilDBInterface $ilDB) use ($a_source_id, $a_target_id, $a_position) {
407 // Receive node infos for source and target
408 $ilDB->setLimit(2);
409
410 $res = $ilDB->query(
411 'SELECT depth, child, parent, path FROM ' . $this->getTree()->getTreeTable() . ' ' .
412 'WHERE ' . $ilDB->in('child', array($a_source_id, $a_target_id), false, 'integer') . ' ' .
413 'AND tree = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer')
414 );
415
416 // Check in tree
417 if ($ilDB->numRows($res) != 2) {
418 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
419 throw new InvalidArgumentException('Error moving subtree');
420 }
421
422 while ($row = $ilDB->fetchObject($res)) {
423 if ($row->child == $a_source_id) {
424 $source_path = $row->path;
425 $source_depth = $row->depth;
426 $source_parent = $row->parent;
427 } else {
428 $target_path = $row->path;
429 $target_depth = $row->depth;
430 }
431 }
432
433 if ($target_depth >= $source_depth) {
434 // We move nodes deeper into the tree. Therefore we need to
435 // check whether we might exceed the maximal path length.
436 // We use FOR UPDATE here, because we don't want anyone to
437 // insert new nodes while we move the subtree.
438
439 $res = $ilDB->queryF(
440 'SELECT MAX(depth) max_depth ' .
441 'FROM ' . $this->getTree()->getTreeTable() . ' ' .
442 'WHERE path BETWEEN %s AND %s' . ' ' .
443 'AND tree = %s ',
444 array('text', 'text', 'integer'),
445 array($source_path, $source_path . '.Z', $this->getTree()->getTreeId())
446 );
447
448 $row = $ilDB->fetchObject($res);
449
450 if ($row->max_depth - $source_depth + $target_depth + 1 > $this->getMaximumPossibleDepth()) {
451 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Objects not found in tree');
452 throw new ilInvalidTreeStructureException('Maximum tree depth exceeded');
453 }
454 }
455 // Check target not child of source
456 if (substr($target_path . '.', 0, strlen($source_path) . '.') == $source_path . '.') {
457 ilLoggerFactory::getLogger('tree')->logStack(ilLogLevel::ERROR, 'Target is child of source');
458 throw new InvalidArgumentException('Error moving subtree: target is child of source');
459 }
460 $depth_diff = $target_depth - $source_depth + 1;
461
462 // move subtree:
463 $query =
464 'UPDATE ' . $this->getTree()->getTreeTable() . ' ' .
465 'SET parent = CASE WHEN parent = ' . $ilDB->quote($source_parent, 'integer') . ' ' .
466 'THEN ' . $ilDB->quote($a_target_id, 'integer') . ' ' .
467 'ELSE parent END, path = ' .
468 $ilDB->concat(array(
469 array($ilDB->quote($target_path, 'text'), 'text'),
470 array($ilDB->substr('path', strrpos('.' . $source_path, '.')), 'text'))) . ' ' .
471 ',depth = depth + ' . $ilDB->quote($depth_diff, 'integer') . ' ' .
472 'WHERE path BETWEEN ' . $ilDB->quote($source_path, 'text') . ' ' .
473 'AND ' . $ilDB->quote($source_path . '.Z', 'text') . ' ';
474
475 if (!$this->getTree()->__isMainTree()) {
476 $query .= ('AND ' . $ilDB->quote($this->getTree()->getTreeId(), \ilDBConstants::T_INTEGER));
477 }
478 \ilLoggerFactory::getLogger('tree')->debug('Query is: ' . $query);
479 $ilDB->manipulate($query);
480 };
481
482
483 if ($this->getTree()->__isMainTree()) {
484 $ilAtomQuery = $ilDB->buildAtomQuery();
485 $ilAtomQuery->addTableLock("tree");
486 $ilAtomQuery->addQueryCallable($move_tree_callable);
487 $ilAtomQuery->run();
488 } else {
489 $move_tree_callable($ilDB);
490 }
491
492 return true;
493 }
494
495
496 public static function createFromParentReleation()
497 {
498 global $DIC;
499
500 $ilDB = $DIC['ilDB'];
501
502 $r = $ilDB->queryF('SELECT DISTINCT * FROM tree WHERE parent = %s', array('integer'), array(0));
503
504 while ($row = $ilDB->fetchAssoc($r)) {
506
507 if ($success !== true) {
508 }
509 }
510 }
511
517 private static function createMaterializedPath($parent, $parentPath)
518 {
519 global $DIC;
520
521 $ilDB = $DIC['ilDB'];
522 $q = ' UPDATE tree
523 SET path = CONCAT(COALESCE(' . $ilDB->quote($parentPath, 'text') . ', \'\'), COALESCE( ' . $ilDB->cast("child", "text") . ' , \'\'))
524 WHERE parent = %s';
525 $r = $ilDB->manipulateF($q, array('integer'), array($parent));
526
527 $r = $ilDB->queryF('SELECT child FROM tree WHERE parent = %s', array('integer'), array($parent));
528
529 while ($row = $ilDB->fetchAssoc($r)) {
530 self::createMaterializedPath($row['child'], $parentPath . $row['child'] . '.');
531 }
532
533 return true;
534 }
535
540 public function getSubtreeInfo($a_endnode_id)
541 {
542 global $DIC;
543
544 $ilDB = $DIC['ilDB'];
545
546 if ($this->getTree()->__isMainTree() && $this->getTree()->getTreeId() == 1) {
547 $treeClause1 = '';
548 $treeClause2 = '';
549 } else {
550 $treeClause1 = ' AND t1.' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
551 $treeClause2 = ' AND t2.' . $this->getTree()->getTreePk() . ' = ' . $ilDB->quote($this->getTree()->getTreeId(), 'integer');
552 }
553
554 // first query for the path of the given node
555 $query = "
556 SELECT t1." . $this->getTree()->getTreePk() . ", t1.path
557 FROM " . $this->getTree()->getTreeTable() . " t1
558 WHERE t1.child = " . $ilDB->quote($a_endnode_id, 'integer') .
559 $treeClause1;
560
561 $res = $ilDB->query($query);
562 $row = $ilDB->fetchAssoc($res);
563 if ($row[$this->getTree()->getTreePk()] == $this->getTree()->getTreeId()) {
564 $path = $row['path'];
565 } else {
566 return [];
567 }
568
569 // then query for the nodes in that path
570 $query = "SELECT t2." . $this->getTree()->getTreePk() . ", t2.child child, type, t2.path path " .
571 "FROM " . $this->getTree()->getTreeTable() . " t2 " .
572 "JOIN " . $this->getTree()->getTableReference() . " obr ON t2.child = obr.ref_id " .
573 "JOIN " . $this->getTree()->getObjectDataTable() . " obd ON obr.obj_id = obd.obj_id " .
574 "WHERE t2.path BETWEEN " . $ilDB->quote($path, 'text') . " AND " . $ilDB->quote($path . '.Z', 'text') .
575 $treeClause2 . ' ' .
576 "ORDER BY t2.path";
577
578
579 $res = $ilDB->query($query);
580 $nodes = [];
581 while ($row = $ilDB->fetchAssoc($res)) {
582 // filter out deleted items if tree is repository
583 if ($row[$this->getTree()->getTreePk()] != $this->getTree()->getTreeId()) {
584 continue;
585 }
586
587 $nodes[$row['child']]['child'] = $row['child'];
588 $nodes[$row['child']]['type'] = $row['type'];
589 $nodes[$row['child']]['path'] = $row['path'];
590 }
591
592 $depth_first_compare = static function ($a, $b) {
593 $a_exploded = explode('.', $a['path']);
594 $b_exploded = explode('.', $b['path']);
595
596 $a_padded = '';
597 foreach ($a_exploded as $num) {
598 $a_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
599 }
600 $b_padded = '';
601 foreach ($b_exploded as $num) {
602 $b_padded .= (str_pad((string) $num, 14, '0', STR_PAD_LEFT));
603 }
604
605 return strcasecmp($a_padded, $b_padded);
606 };
607
608 uasort($nodes, $depth_first_compare);
609
610 return (array) $nodes;
611 }
612
617 public function validateParentRelations()
618 {
619 global $DIC;
620
621 $ilDB = $DIC['ilDB'];
622
623 $query = 'select child from ' . $this->getTree()->getTreeTable() . ' child where not exists ' .
624 '( ' .
625 'select child from ' . $this->getTree()->getTreeTable() . ' parent where child.parent = parent.child and ' .
626 '(child.path BETWEEN parent.path AND CONCAT(parent.path,' . $ilDB->quote('Z', 'text') . ') )' . ')' .
627 'and ' . $this->getTree()->getTreePk() . ' = ' . $this->getTree()->getTreeId() . ' and child <> 1';
628 $res = $ilDB->query($query);
629
630 ilLoggerFactory::getLogger('tree')->debug($query);
631
632 $failures = array();
633 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
634 $failures[] = $row[$this->getTree()->getTreePk()];
635 }
636 return $failures;
637 }
638}
$success
Definition: Utf8Test.php:86
An exception for terminatinating execution or to throw for unit testing.
Thrown if invalid tree strucutes are found.
static getLogger($a_component_id)
Get component logger.
Base class for materialize path based trees Based on implementation of Werner Randelshofer.
moveTree($a_source_id, $a_target_id, $a_position)
move source subtree to target node
getPathIds($a_endnode, $a_startnode=0)
Get path ids.
validateParentRelations()
Validaate parent relations.
getTrashSubTreeQuery($a_node, $a_types, $a_force_join_reference=true, $a_fields=[])
Get subtree query for trashed tree items.mixed
getSubTreeIds($a_node_id)
Get subtree ids.
deleteTree($a_node_id)
Delete a subtree.
getSubTreeQuery($a_node, $a_types='', $a_force_join_reference=true, $a_fields=array())
Get subtree query.
__construct(ilTree $a_tree)
Constructor.
getMaximumPossibleDepth()
Get maximum possible depth.
moveToTrash($a_node_id)
Move subtree to trash.
getRelation($a_node_a, $a_node_b)
Get relation of two nodes.
insertNode($a_node_id, $a_parent_id, $a_pos)
Insert new node under parent node.
static createMaterializedPath($parent, $parentPath)
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
global $DIC
Definition: goto.php:24
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface for tree implementations Currrently nested set or materialize path.
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
$query
foreach($_POST as $key=> $value) $res
global $ilDB