| Total Complexity | 158 |
| Total Lines | 1134 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like NestedTreeRepositoryTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use NestedTreeRepositoryTrait, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 22 | trait NestedTreeRepositoryTrait |
||
| 23 | { |
||
| 24 | use TreeRepositoryTrait; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * {@inheritDoc} |
||
| 28 | */ |
||
| 29 | public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') |
||
| 30 | { |
||
| 31 | $meta = $this->getClassMetadata(); |
||
| 32 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 33 | $qb = $this->getQueryBuilder(); |
||
| 34 | $qb |
||
| 35 | ->select('node') |
||
| 36 | ->from($config['useObjectClass'], 'node') |
||
| 37 | ->where($qb->expr()->isNull('node.'.$config['parent'])) |
||
| 38 | ; |
||
| 39 | |||
| 40 | if ($sortByField !== null) { |
||
| 41 | $qb->orderBy('node.'.$sortByField, strtolower($direction) === 'asc' ? 'asc' : 'desc'); |
||
| 42 | } else { |
||
| 43 | $qb->orderBy('node.'.$config['left'], 'ASC'); |
||
| 44 | } |
||
| 45 | |||
| 46 | return $qb; |
||
| 47 | } |
||
| 48 | |||
| 49 | /** |
||
| 50 | * {@inheritDoc} |
||
| 51 | */ |
||
| 52 | public function getRootNodesQuery($sortByField = null, $direction = 'asc') |
||
| 53 | { |
||
| 54 | return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); |
||
| 55 | } |
||
| 56 | |||
| 57 | /** |
||
| 58 | * {@inheritDoc} |
||
| 59 | */ |
||
| 60 | public function getRootNodes($sortByField = null, $direction = 'asc') |
||
| 61 | { |
||
| 62 | return $this->getRootNodesQuery($sortByField, $direction)->getResult(); |
||
| 63 | } |
||
| 64 | |||
| 65 | /** |
||
| 66 | * Persists node in given position strategy |
||
| 67 | */ |
||
| 68 | protected function persistAs($node, $child = null, $position = Nested::FIRST_CHILD) |
||
| 69 | { |
||
| 70 | $em = $this->getEntityManager(); |
||
| 71 | $wrapped = new EntityWrapper($node, $em); |
||
| 72 | $meta = $this->getClassMetadata(); |
||
| 73 | $config = $this->listener->getConfiguration($em, $meta->name); |
||
| 74 | |||
| 75 | $siblingInPosition = null; |
||
| 76 | if ($child !== null) { |
||
| 77 | switch ($position) { |
||
| 78 | case Nested::PREV_SIBLING: |
||
| 79 | case Nested::NEXT_SIBLING: |
||
| 80 | $sibling = new EntityWrapper($child, $em); |
||
| 81 | $newParent = $sibling->getPropertyValue($config['parent']); |
||
| 82 | if (null === $newParent && isset($config['root'])) { |
||
| 83 | throw new UnexpectedValueException("Cannot persist sibling for a root node, tree operation is not possible"); |
||
| 84 | } |
||
| 85 | $siblingInPosition = $child; |
||
| 86 | $child = $newParent; |
||
| 87 | break; |
||
| 88 | } |
||
| 89 | $wrapped->setPropertyValue($config['parent'], $child); |
||
| 90 | } |
||
| 91 | |||
| 92 | $wrapped->setPropertyValue($config['left'], 0); // simulate changeset |
||
| 93 | $oid = spl_object_hash($node); |
||
| 94 | $this->listener->getStrategy($em, $meta->name)->setNodePosition($oid, $position, $siblingInPosition); |
||
| 95 | $em->persist($node); |
||
| 96 | |||
| 97 | return $this; |
||
| 98 | } |
||
| 99 | |||
| 100 | /** |
||
| 101 | * Persists given $node as first child of tree |
||
| 102 | * |
||
| 103 | * @param $node |
||
| 104 | * @return self |
||
| 105 | */ |
||
| 106 | public function persistAsFirstChild($node) |
||
| 107 | { |
||
| 108 | return $this->persistAs($node, null, Nested::FIRST_CHILD); |
||
| 109 | } |
||
| 110 | |||
| 111 | /** |
||
| 112 | * Persists given $node as first child of $parent node |
||
| 113 | * |
||
| 114 | * @param $node |
||
| 115 | * @param $parent |
||
| 116 | * @return self |
||
| 117 | */ |
||
| 118 | public function persistAsFirstChildOf($node, $parent) |
||
| 119 | { |
||
| 120 | return $this->persistAs($node, $parent, Nested::FIRST_CHILD); |
||
| 121 | } |
||
| 122 | |||
| 123 | /** |
||
| 124 | * Persists given $node as last child of tree |
||
| 125 | * |
||
| 126 | * @param $node |
||
| 127 | * @return self |
||
| 128 | */ |
||
| 129 | public function persistAsLastChild($node) |
||
| 130 | { |
||
| 131 | return $this->persistAs($node, null, Nested::LAST_CHILD); |
||
| 132 | } |
||
| 133 | |||
| 134 | /** |
||
| 135 | * Persists given $node as last child of $parent node |
||
| 136 | * |
||
| 137 | * @param $node |
||
| 138 | * @param $parent |
||
| 139 | * @return self |
||
| 140 | */ |
||
| 141 | public function persistAsLastChildOf($node, $parent) |
||
| 142 | { |
||
| 143 | return $this->persistAs($node, $parent, Nested::LAST_CHILD); |
||
| 144 | } |
||
| 145 | |||
| 146 | /** |
||
| 147 | * Persists given $node next to $sibling node |
||
| 148 | * |
||
| 149 | * @param $node |
||
| 150 | * @param $sibling |
||
| 151 | * @return self |
||
| 152 | */ |
||
| 153 | public function persistAsNextSiblingOf($node, $sibling) |
||
| 154 | { |
||
| 155 | return $this->persistAs($node, $sibling, Nested::NEXT_SIBLING); |
||
| 156 | } |
||
| 157 | |||
| 158 | /** |
||
| 159 | * Persists given $node previous to $sibling node |
||
| 160 | * |
||
| 161 | * @param $node |
||
| 162 | * @param $sibling |
||
| 163 | * @return self |
||
| 164 | */ |
||
| 165 | public function persistAsPrevSiblingOf($node, $sibling) |
||
| 166 | { |
||
| 167 | return $this->persistAs($node, $sibling, Nested::PREV_SIBLING); |
||
| 168 | } |
||
| 169 | |||
| 170 | /** |
||
| 171 | * Persists given $node same as first child of it's parent |
||
| 172 | * |
||
| 173 | * @param $node |
||
| 174 | * @return self |
||
| 175 | */ |
||
| 176 | public function persistAsNextSibling($node) |
||
| 177 | { |
||
| 178 | return $this->persistAs($node, null, Nested::NEXT_SIBLING); |
||
| 179 | } |
||
| 180 | |||
| 181 | /** |
||
| 182 | * Persists given $node same as last child of it's parent |
||
| 183 | * |
||
| 184 | * @param $node |
||
| 185 | * @return self |
||
| 186 | */ |
||
| 187 | public function persistAsPrevSibling($node) |
||
| 188 | { |
||
| 189 | return $this->persistAs($node, null, Nested::PREV_SIBLING); |
||
| 190 | } |
||
| 191 | |||
| 192 | /** |
||
| 193 | * Get the Tree path query builder by given $node |
||
| 194 | * |
||
| 195 | * @param object $node |
||
| 196 | * |
||
| 197 | * @throws InvalidArgumentException - if input is not valid |
||
| 198 | * |
||
| 199 | * @return \Doctrine\ORM\QueryBuilder |
||
| 200 | */ |
||
| 201 | public function getPathQueryBuilder($node) |
||
| 202 | { |
||
| 203 | $meta = $this->getClassMetadata(); |
||
| 204 | if (!$node instanceof $meta->name) { |
||
| 205 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 206 | } |
||
| 207 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 208 | $wrapped = new EntityWrapper($node, $this->getEntityManager()); |
||
| 209 | if (!$wrapped->hasValidIdentifier()) { |
||
| 210 | throw new InvalidArgumentException("Node is not managed by UnitOfWork"); |
||
| 211 | } |
||
| 212 | $left = $wrapped->getPropertyValue($config['left']); |
||
| 213 | $right = $wrapped->getPropertyValue($config['right']); |
||
| 214 | $qb = $this->getQueryBuilder(); |
||
| 215 | $qb->select('node') |
||
| 216 | ->from($config['useObjectClass'], 'node') |
||
| 217 | ->where($qb->expr()->lte('node.'.$config['left'], $left)) |
||
| 218 | ->andWhere($qb->expr()->gte('node.'.$config['right'], $right)) |
||
| 219 | ->orderBy('node.'.$config['left'], 'ASC') |
||
| 220 | ; |
||
| 221 | if (isset($config['root'])) { |
||
| 222 | $root = $wrapped->getPropertyValue($config['root']); |
||
| 223 | $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); |
||
| 224 | $qb->setParameter('rid', $root); |
||
| 225 | } |
||
| 226 | |||
| 227 | return $qb; |
||
| 228 | } |
||
| 229 | |||
| 230 | /** |
||
| 231 | * Get the Tree path query by given $node |
||
| 232 | * |
||
| 233 | * @param object $node |
||
| 234 | * |
||
| 235 | * @return \Doctrine\ORM\Query |
||
| 236 | */ |
||
| 237 | public function getPathQuery($node) |
||
| 238 | { |
||
| 239 | return $this->getPathQueryBuilder($node)->getQuery(); |
||
| 240 | } |
||
| 241 | |||
| 242 | /** |
||
| 243 | * Get the Tree path of Nodes by given $node |
||
| 244 | * |
||
| 245 | * @param object $node |
||
| 246 | * |
||
| 247 | * @return array - list of Nodes in path |
||
| 248 | */ |
||
| 249 | public function getPath($node) |
||
| 250 | { |
||
| 251 | return $this->getPathQuery($node)->getResult(); |
||
| 252 | } |
||
| 253 | |||
| 254 | /** |
||
| 255 | * @see getChildrenQueryBuilder |
||
| 256 | */ |
||
| 257 | public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 258 | { |
||
| 259 | $meta = $this->getClassMetadata(); |
||
| 260 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 261 | |||
| 262 | $qb = $this->getQueryBuilder(); |
||
| 263 | $qb->select('node') |
||
| 264 | ->from($config['useObjectClass'], 'node') |
||
| 265 | ; |
||
| 266 | if ($node !== null) { |
||
| 267 | if ($node instanceof $meta->name) { |
||
| 268 | $wrapped = new EntityWrapper($node, $this->getEntityManager()); |
||
| 269 | if (!$wrapped->hasValidIdentifier()) { |
||
| 270 | throw new InvalidArgumentException("Node is not managed by UnitOfWork"); |
||
| 271 | } |
||
| 272 | if ($direct) { |
||
| 273 | $qb->where($qb->expr()->eq('node.'.$config['parent'], ':pid')); |
||
| 274 | $qb->setParameter('pid', $wrapped->getIdentifier()); |
||
| 275 | } else { |
||
| 276 | $left = $wrapped->getPropertyValue($config['left']); |
||
| 277 | $right = $wrapped->getPropertyValue($config['right']); |
||
| 278 | if ($left && $right) { |
||
| 279 | $qb->where($qb->expr()->lt('node.'.$config['right'], $right)); |
||
| 280 | $qb->andWhere($qb->expr()->gt('node.'.$config['left'], $left)); |
||
| 281 | } |
||
| 282 | } |
||
| 283 | if (isset($config['root'])) { |
||
| 284 | $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); |
||
| 285 | $qb->setParameter('rid', $wrapped->getPropertyValue($config['root'])); |
||
| 286 | } |
||
| 287 | if ($includeNode) { |
||
| 288 | $idField = $meta->getSingleIdentifierFieldName(); |
||
| 289 | $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode'); |
||
| 290 | $qb->setParameter('rootNode', $node); |
||
| 291 | } |
||
| 292 | } else { |
||
| 293 | throw new \InvalidArgumentException("Node is not related to this repository"); |
||
| 294 | } |
||
| 295 | } else { |
||
| 296 | if ($direct) { |
||
| 297 | $qb->where($qb->expr()->isNull('node.'.$config['parent'])); |
||
| 298 | } |
||
| 299 | } |
||
| 300 | if (!$sortByField) { |
||
| 301 | $qb->orderBy('node.'.$config['left'], 'ASC'); |
||
| 302 | } elseif (is_array($sortByField)) { |
||
| 303 | $fields = ''; |
||
| 304 | foreach ($sortByField as $field) { |
||
| 305 | $fields .= 'node.'.$field.','; |
||
| 306 | } |
||
| 307 | $fields = rtrim($fields, ','); |
||
| 308 | $qb->orderBy($fields, $direction); |
||
| 309 | } else { |
||
| 310 | if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) { |
||
| 311 | $qb->orderBy('node.'.$sortByField, $direction); |
||
| 312 | } else { |
||
| 313 | throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); |
||
| 314 | } |
||
| 315 | } |
||
| 316 | |||
| 317 | return $qb; |
||
| 318 | } |
||
| 319 | |||
| 320 | /** |
||
| 321 | * @see getChildrenQuery |
||
| 322 | */ |
||
| 323 | public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 324 | { |
||
| 325 | return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); |
||
| 326 | } |
||
| 327 | |||
| 328 | /** |
||
| 329 | * @see getChildren |
||
| 330 | */ |
||
| 331 | public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 332 | { |
||
| 333 | $q = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); |
||
| 334 | |||
| 335 | return $q->getResult(); |
||
| 336 | } |
||
| 337 | |||
| 338 | /** |
||
| 339 | * {@inheritDoc} |
||
| 340 | */ |
||
| 341 | public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 342 | { |
||
| 343 | return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); |
||
| 344 | } |
||
| 345 | |||
| 346 | /** |
||
| 347 | * {@inheritDoc} |
||
| 348 | */ |
||
| 349 | public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 350 | { |
||
| 351 | return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); |
||
| 352 | } |
||
| 353 | |||
| 354 | /** |
||
| 355 | * {@inheritDoc} |
||
| 356 | */ |
||
| 357 | public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) |
||
| 358 | { |
||
| 359 | return $this->children($node, $direct, $sortByField, $direction, $includeNode); |
||
| 360 | } |
||
| 361 | |||
| 362 | /** |
||
| 363 | * Get tree leafs query builder |
||
| 364 | * |
||
| 365 | * @param object $root - root node in case of root tree is required |
||
| 366 | * @param string $sortByField - field name to sort by |
||
| 367 | * @param string $direction - sort direction : "ASC" or "DESC" |
||
| 368 | * |
||
| 369 | * @throws InvalidArgumentException - if input is not valid |
||
| 370 | * |
||
| 371 | * @return \Doctrine\ORM\QueryBuilder |
||
| 372 | */ |
||
| 373 | public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC') |
||
| 374 | { |
||
| 375 | $meta = $this->getClassMetadata(); |
||
| 376 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 377 | |||
| 378 | if (isset($config['root']) && null === $root) { |
||
| 379 | throw new InvalidArgumentException("If tree has root, getLeafs method requires any node of this tree"); |
||
| 380 | } |
||
| 381 | |||
| 382 | $qb = $this->getQueryBuilder(); |
||
| 383 | $qb->select('node') |
||
| 384 | ->from($config['useObjectClass'], 'node') |
||
| 385 | ->where($qb->expr()->eq('node.'.$config['right'], '1 + node.'.$config['left'])) |
||
| 386 | ; |
||
| 387 | if (isset($config['root'])) { |
||
| 388 | if ($root instanceof $meta->name) { |
||
| 389 | $wrapped = new EntityWrapper($root, $this->getEntityManager()); |
||
| 390 | $rootId = $wrapped->getPropertyValue($config['root']); |
||
| 391 | if (!$rootId) { |
||
| 392 | throw new InvalidArgumentException("Root node must be managed"); |
||
| 393 | } |
||
| 394 | $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); |
||
| 395 | $qb->setParameter('rid', $rootId); |
||
| 396 | } else { |
||
| 397 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 398 | } |
||
| 399 | } |
||
| 400 | if (!$sortByField) { |
||
| 401 | if (isset($config['root'])) { |
||
| 402 | $qb->addOrderBy('node.'.$config['root'], 'ASC'); |
||
| 403 | } |
||
| 404 | $qb->addOrderBy('node.'.$config['left'], 'ASC'); |
||
| 405 | } else { |
||
| 406 | if ($meta->hasField($sortByField) && in_array(strtolower($direction), array('asc', 'desc'))) { |
||
| 407 | $qb->orderBy('node.'.$sortByField, $direction); |
||
| 408 | } else { |
||
| 409 | throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); |
||
| 410 | } |
||
| 411 | } |
||
| 412 | |||
| 413 | return $qb; |
||
| 414 | } |
||
| 415 | |||
| 416 | /** |
||
| 417 | * Get tree leafs query |
||
| 418 | * |
||
| 419 | * @param object $root - root node in case of root tree is required |
||
| 420 | * @param string $sortByField - field name to sort by |
||
| 421 | * @param string $direction - sort direction : "ASC" or "DESC" |
||
| 422 | * |
||
| 423 | * @return \Doctrine\ORM\Query |
||
| 424 | */ |
||
| 425 | public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC') |
||
| 426 | { |
||
| 427 | return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery(); |
||
| 428 | } |
||
| 429 | |||
| 430 | /** |
||
| 431 | * Get list of leaf nodes of the tree |
||
| 432 | * |
||
| 433 | * @param object $root - root node in case of root tree is required |
||
| 434 | * @param string $sortByField - field name to sort by |
||
| 435 | * @param string $direction - sort direction : "ASC" or "DESC" |
||
| 436 | * |
||
| 437 | * @return array |
||
| 438 | */ |
||
| 439 | public function getLeafs($root = null, $sortByField = null, $direction = 'ASC') |
||
| 442 | } |
||
| 443 | |||
| 444 | /** |
||
| 445 | * Get the query builder for next siblings of the given $node |
||
| 446 | * |
||
| 447 | * @param object $node |
||
| 448 | * @param bool $includeSelf - include the node itself |
||
| 449 | * |
||
| 450 | * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid |
||
| 451 | * |
||
| 452 | * @return \Doctrine\ORM\QueryBuilder |
||
| 453 | */ |
||
| 454 | public function getNextSiblingsQueryBuilder($node, $includeSelf = false) |
||
| 455 | { |
||
| 456 | $meta = $this->getClassMetadata(); |
||
| 457 | if (!$node instanceof $meta->name) { |
||
| 458 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 459 | } |
||
| 460 | $wrapped = new EntityWrapper($node, $this->getEntityManager()); |
||
| 461 | if (!$wrapped->hasValidIdentifier()) { |
||
| 462 | throw new InvalidArgumentException("Node is not managed by UnitOfWork"); |
||
| 463 | } |
||
| 464 | |||
| 465 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 466 | $parent = $wrapped->getPropertyValue($config['parent']); |
||
| 467 | if (isset($config['root']) && !$parent) { |
||
| 468 | throw new InvalidArgumentException("Cannot get siblings from tree root node"); |
||
| 469 | } |
||
| 470 | |||
| 471 | $left = $wrapped->getPropertyValue($config['left']); |
||
| 472 | |||
| 473 | $qb = $this->getQueryBuilder(); |
||
| 474 | $qb->select('node') |
||
| 475 | ->from($config['useObjectClass'], 'node') |
||
| 476 | ->where( |
||
| 477 | $includeSelf ? |
||
| 478 | $qb->expr()->gte('node.'.$config['left'], $left) : |
||
| 479 | $qb->expr()->gt('node.'.$config['left'], $left) |
||
| 480 | ) |
||
| 481 | ->orderBy("node.{$config['left']}", 'ASC') |
||
| 482 | ; |
||
| 483 | if ($parent) { |
||
| 484 | $wrappedParent = new EntityWrapper($parent, $this->getEntityManager()); |
||
| 485 | $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); |
||
| 486 | $qb->setParameter('pid', $wrappedParent->getIdentifier()); |
||
| 487 | } else { |
||
| 488 | $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); |
||
| 489 | } |
||
| 490 | |||
| 491 | return $qb; |
||
| 492 | } |
||
| 493 | |||
| 494 | /** |
||
| 495 | * Get the query for next siblings of the given $node |
||
| 496 | * |
||
| 497 | * @param object $node |
||
| 498 | * @param bool $includeSelf - include the node itself |
||
| 499 | * |
||
| 500 | * @return \Doctrine\ORM\Query |
||
| 501 | */ |
||
| 502 | public function getNextSiblingsQuery($node, $includeSelf = false) |
||
| 503 | { |
||
| 504 | return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery(); |
||
| 505 | } |
||
| 506 | |||
| 507 | /** |
||
| 508 | * Find the next siblings of the given $node |
||
| 509 | * |
||
| 510 | * @param object $node |
||
| 511 | * @param bool $includeSelf - include the node itself |
||
| 512 | * |
||
| 513 | * @return array |
||
| 514 | */ |
||
| 515 | public function getNextSiblings($node, $includeSelf = false) |
||
| 516 | { |
||
| 517 | return $this->getNextSiblingsQuery($node, $includeSelf)->getResult(); |
||
| 518 | } |
||
| 519 | |||
| 520 | /** |
||
| 521 | * Get query builder for previous siblings of the given $node |
||
| 522 | * |
||
| 523 | * @param object $node |
||
| 524 | * @param bool $includeSelf - include the node itself |
||
| 525 | * |
||
| 526 | * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid |
||
| 527 | * |
||
| 528 | * @return \Doctrine\ORM\QueryBuilder |
||
| 529 | */ |
||
| 530 | public function getPrevSiblingsQueryBuilder($node, $includeSelf = false) |
||
| 531 | { |
||
| 532 | $meta = $this->getClassMetadata(); |
||
| 533 | if (!$node instanceof $meta->name) { |
||
| 534 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 535 | } |
||
| 536 | $wrapped = new EntityWrapper($node, $this->getEntityManager()); |
||
| 537 | if (!$wrapped->hasValidIdentifier()) { |
||
| 538 | throw new InvalidArgumentException("Node is not managed by UnitOfWork"); |
||
| 539 | } |
||
| 540 | |||
| 541 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 542 | $parent = $wrapped->getPropertyValue($config['parent']); |
||
| 543 | if (isset($config['root']) && !$parent) { |
||
| 544 | throw new InvalidArgumentException("Cannot get siblings from tree root node"); |
||
| 545 | } |
||
| 546 | |||
| 547 | $left = $wrapped->getPropertyValue($config['left']); |
||
| 548 | |||
| 549 | $qb = $this->getQueryBuilder(); |
||
| 550 | $qb->select('node') |
||
| 551 | ->from($config['useObjectClass'], 'node') |
||
| 552 | ->where( |
||
| 553 | $includeSelf ? |
||
| 554 | $qb->expr()->lte('node.'.$config['left'], $left) : |
||
| 555 | $qb->expr()->lt('node.'.$config['left'], $left) |
||
| 556 | ) |
||
| 557 | ->orderBy("node.{$config['left']}", 'ASC') |
||
| 558 | ; |
||
| 559 | if ($parent) { |
||
| 560 | $wrappedParent = new EntityWrapper($parent, $this->getEntityManager()); |
||
| 561 | $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); |
||
| 562 | $qb->setParameter('pid', $wrappedParent->getIdentifier()); |
||
| 563 | } else { |
||
| 564 | $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); |
||
| 565 | } |
||
| 566 | |||
| 567 | return $qb; |
||
| 568 | } |
||
| 569 | |||
| 570 | /** |
||
| 571 | * Get query for previous siblings of the given $node |
||
| 572 | * |
||
| 573 | * @param object $node |
||
| 574 | * @param bool $includeSelf - include the node itself |
||
| 575 | * |
||
| 576 | * @throws \Gedmo\Exception\InvalidArgumentException - if input is invalid |
||
| 577 | * |
||
| 578 | * @return \Doctrine\ORM\Query |
||
| 579 | */ |
||
| 580 | public function getPrevSiblingsQuery($node, $includeSelf = false) |
||
| 581 | { |
||
| 582 | return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery(); |
||
| 583 | } |
||
| 584 | |||
| 585 | /** |
||
| 586 | * Find the previous siblings of the given $node |
||
| 587 | * |
||
| 588 | * @param object $node |
||
| 589 | * @param bool $includeSelf - include the node itself |
||
| 590 | * |
||
| 591 | * @return array |
||
| 592 | */ |
||
| 593 | public function getPrevSiblings($node, $includeSelf = false) |
||
| 594 | { |
||
| 595 | return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult(); |
||
| 596 | } |
||
| 597 | |||
| 598 | /** |
||
| 599 | * Move the node down in the same level |
||
| 600 | * |
||
| 601 | * @param object $node |
||
| 602 | * @param int|bool $number integer - number of positions to shift |
||
| 603 | * boolean - if "true" - shift till last position |
||
| 604 | * |
||
| 605 | * @throws \RuntimeException - if something fails in transaction |
||
| 606 | * |
||
| 607 | * @return boolean - true if shifted |
||
| 608 | */ |
||
| 609 | public function moveDown($node, $number = 1) |
||
| 610 | { |
||
| 611 | $result = false; |
||
| 612 | $meta = $this->getClassMetadata(); |
||
| 613 | if ($node instanceof $meta->name) { |
||
| 614 | $nextSiblings = $this->getNextSiblings($node); |
||
| 615 | if ($numSiblings = count($nextSiblings)) { |
||
| 616 | $result = true; |
||
| 617 | if ($number === true) { |
||
| 618 | $number = $numSiblings; |
||
| 619 | } elseif ($number > $numSiblings) { |
||
| 620 | $number = $numSiblings; |
||
| 621 | } |
||
| 622 | $this->listener |
||
| 623 | ->getStrategy($this->getEntityManager(), $meta->name) |
||
| 624 | ->updateNode($this->getEntityManager(), $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING); |
||
| 625 | } |
||
| 626 | } else { |
||
| 627 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 628 | } |
||
| 629 | |||
| 630 | return $result; |
||
| 631 | } |
||
| 632 | |||
| 633 | /** |
||
| 634 | * Move the node up in the same level |
||
| 635 | * |
||
| 636 | * @param object $node |
||
| 637 | * @param int|bool $number integer - number of positions to shift |
||
| 638 | * boolean - true shift till first position |
||
| 639 | * |
||
| 640 | * @throws \RuntimeException - if something fails in transaction |
||
| 641 | * |
||
| 642 | * @return boolean - true if shifted |
||
| 643 | */ |
||
| 644 | public function moveUp($node, $number = 1) |
||
| 645 | { |
||
| 646 | $result = false; |
||
| 647 | $meta = $this->getClassMetadata(); |
||
| 648 | if ($node instanceof $meta->name) { |
||
| 649 | $prevSiblings = array_reverse($this->getPrevSiblings($node)); |
||
| 650 | if ($numSiblings = count($prevSiblings)) { |
||
| 651 | $result = true; |
||
| 652 | if ($number === true) { |
||
| 653 | $number = $numSiblings; |
||
| 654 | } elseif ($number > $numSiblings) { |
||
| 655 | $number = $numSiblings; |
||
| 656 | } |
||
| 657 | $this->listener |
||
| 658 | ->getStrategy($this->getEntityManager(), $meta->name) |
||
| 659 | ->updateNode($this->getEntityManager(), $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING); |
||
| 660 | } |
||
| 661 | } else { |
||
| 662 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 663 | } |
||
| 664 | |||
| 665 | return $result; |
||
| 666 | } |
||
| 667 | |||
| 668 | /** |
||
| 669 | * UNSAFE: be sure to backup before running this method when necessary |
||
| 670 | * |
||
| 671 | * Removes given $node from the tree and reparents its descendants |
||
| 672 | */ |
||
| 673 | public function removeFromTree(object $node): void |
||
| 674 | { |
||
| 675 | $meta = $this->getClassMetadata(); |
||
| 676 | $em = $this->getEntityManager(); |
||
| 677 | |||
| 678 | if ($node instanceof $meta->name) { |
||
| 679 | $wrapped = new EntityWrapper($node, $em); |
||
| 680 | $config = $this->listener->getConfiguration($em, $meta->name); |
||
| 681 | $right = $wrapped->getPropertyValue($config['right']); |
||
| 682 | $left = $wrapped->getPropertyValue($config['left']); |
||
| 683 | $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; |
||
| 684 | |||
| 685 | if (!is_numeric($left) || !is_numeric($right)) { |
||
| 686 | $this->removeSingle($wrapped); |
||
| 687 | return; |
||
| 688 | } |
||
| 689 | |||
| 690 | if ($right == $left + 1) { |
||
| 691 | $this->removeSingle($wrapped); |
||
| 692 | $this->listener |
||
| 693 | ->getStrategy($em, $meta->name) |
||
| 694 | ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId); |
||
| 695 | |||
| 696 | return; // node was a leaf |
||
| 697 | } |
||
| 698 | // process updates in transaction |
||
| 699 | $em->getConnection()->beginTransaction(); |
||
| 700 | try { |
||
| 701 | $parent = $wrapped->getPropertyValue($config['parent']); |
||
| 702 | $parentId = null; |
||
| 703 | if ($parent) { |
||
| 704 | $wrappedParent = new EntityWrapper($parent, $em); |
||
| 705 | $parentId = $wrappedParent->getIdentifier(); |
||
| 706 | } |
||
| 707 | $pk = $meta->getSingleIdentifierFieldName(); |
||
| 708 | $nodeId = $wrapped->getIdentifier(); |
||
| 709 | $shift = -1; |
||
| 710 | |||
| 711 | // in case if root node is removed, children become roots |
||
| 712 | if (isset($config['root']) && !$parent) { |
||
| 713 | $qb = $this->getQueryBuilder(); |
||
| 714 | $qb->select('node.'.$pk, 'node.'.$config['left'], 'node.'.$config['right']) |
||
| 715 | ->from($config['useObjectClass'], 'node'); |
||
| 716 | |||
| 717 | $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); |
||
| 718 | $qb->setParameter('pid', $nodeId); |
||
| 719 | $nodes = $qb->getQuery()->getArrayResult(); |
||
| 720 | |||
| 721 | foreach ($nodes as $newRoot) { |
||
| 722 | $left = $newRoot[$config['left']]; |
||
| 723 | $right = $newRoot[$config['right']]; |
||
| 724 | $rootId = $newRoot[$pk]; |
||
| 725 | $shift = -($left - 1); |
||
| 726 | |||
| 727 | $qb = $this->getQueryBuilder(); |
||
| 728 | $qb->update($config['useObjectClass'], 'node'); |
||
| 729 | $qb->set('node.'.$config['root'], ':rid'); |
||
| 730 | $qb->setParameter('rid', $rootId); |
||
| 731 | $qb->where($qb->expr()->eq('node.'.$config['root'], ':rpid')); |
||
| 732 | $qb->setParameter('rpid', $nodeId); |
||
| 733 | $qb->andWhere($qb->expr()->gte('node.'.$config['left'], $left)); |
||
| 734 | $qb->andWhere($qb->expr()->lte('node.'.$config['right'], $right)); |
||
| 735 | $qb->getQuery()->getSingleScalarResult(); |
||
| 736 | |||
| 737 | $qb = $this->getQueryBuilder(); |
||
| 738 | $qb->update($config['useObjectClass'], 'node'); |
||
| 739 | $qb->set('node.'.$config['parent'], ':pid'); |
||
| 740 | $qb->setParameter('pid', $parentId); |
||
| 741 | $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); |
||
| 742 | $qb->setParameter('rpid', $nodeId); |
||
| 743 | $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); |
||
| 744 | $qb->setParameter('rid', $rootId); |
||
| 745 | $qb->getQuery()->getSingleScalarResult(); |
||
| 746 | |||
| 747 | $this->listener |
||
| 748 | ->getStrategy($em, $meta->name) |
||
| 749 | ->shiftRangeRL($em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1); |
||
| 750 | $this->listener |
||
| 751 | ->getStrategy($em, $meta->name) |
||
| 752 | ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId); |
||
| 753 | } |
||
| 754 | } else { |
||
| 755 | $qb = $this->getQueryBuilder(); |
||
| 756 | $qb->update($config['useObjectClass'], 'node'); |
||
| 757 | $qb->set('node.'.$config['parent'], ':pid'); |
||
| 758 | $qb->setParameter('pid', $parentId); |
||
| 759 | $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); |
||
| 760 | $qb->setParameter('rpid', $nodeId); |
||
| 761 | if (isset($config['root'])) { |
||
| 762 | $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); |
||
| 763 | $qb->setParameter('rid', $rootId); |
||
| 764 | } |
||
| 765 | $qb->getQuery()->getSingleScalarResult(); |
||
| 766 | |||
| 767 | $this->listener |
||
| 768 | ->getStrategy($em, $meta->name) |
||
| 769 | ->shiftRangeRL($em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, - 1); |
||
| 770 | |||
| 771 | $this->listener |
||
| 772 | ->getStrategy($em, $meta->name) |
||
| 773 | ->shiftRL($em, $config['useObjectClass'], $right, -2, $rootId); |
||
| 774 | } |
||
| 775 | $this->removeSingle($wrapped); |
||
| 776 | $em->getConnection()->commit(); |
||
| 777 | } catch (\Exception $e) { |
||
| 778 | $em->close(); |
||
| 779 | $em->getConnection()->rollback(); |
||
| 780 | throw new RuntimeException('Transaction failed', null, $e); |
||
| 781 | } |
||
| 782 | } else { |
||
| 783 | throw new InvalidArgumentException("Node is not related to this repository"); |
||
| 784 | } |
||
| 785 | } |
||
| 786 | |||
| 787 | /** |
||
| 788 | * Reorders $node's sibling nodes and child nodes, |
||
| 789 | * according to the $sortByField and $direction specified |
||
| 790 | * |
||
| 791 | * @param object|null $node - node from which to start reordering the tree; null will reorder everything |
||
| 792 | * @param string $sortByField - field name to sort by |
||
| 793 | * @param string $direction - sort direction : "ASC" or "DESC" |
||
| 794 | * @param boolean $verify - true to verify tree first |
||
| 795 | * |
||
| 796 | * @return bool|null |
||
| 797 | */ |
||
| 798 | public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true) |
||
| 819 | } |
||
| 820 | } |
||
| 821 | |||
| 822 | /** |
||
| 823 | * Reorders all nodes in the tree according to the $sortByField and $direction specified. |
||
| 824 | * |
||
| 825 | * @param string $sortByField - field name to sort by |
||
| 826 | * @param string $direction - sort direction : "ASC" or "DESC" |
||
| 827 | * @param boolean $verify - true to verify tree first |
||
| 828 | */ |
||
| 829 | public function reorderAll($sortByField = null, $direction = 'ASC', $verify = true) |
||
| 830 | { |
||
| 831 | $this->reorder(null, $sortByField, $direction, $verify); |
||
| 832 | } |
||
| 833 | |||
| 834 | /** |
||
| 835 | * Verifies that current tree is valid. |
||
| 836 | * If any error is detected it will return an array |
||
| 837 | * with a list of errors found on tree |
||
| 838 | * |
||
| 839 | * @return array|bool - true on success,error list on failure |
||
| 840 | */ |
||
| 841 | public function verify() |
||
| 842 | { |
||
| 843 | if (!$this->childCount()) { |
||
| 844 | return true; // tree is empty |
||
| 845 | } |
||
| 846 | |||
| 847 | $errors = array(); |
||
| 848 | $meta = $this->getClassMetadata(); |
||
| 849 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 850 | if (isset($config['root'])) { |
||
| 851 | $trees = $this->getRootNodes(); |
||
| 852 | foreach ($trees as $tree) { |
||
| 853 | $this->verifyTree($errors, $tree); |
||
| 854 | } |
||
| 855 | } else { |
||
| 856 | $this->verifyTree($errors); |
||
| 857 | } |
||
| 858 | |||
| 859 | return $errors ?: true; |
||
| 860 | } |
||
| 861 | |||
| 862 | /** |
||
| 863 | * NOTE: flush your entity manager after |
||
| 864 | * |
||
| 865 | * Tries to recover the tree |
||
| 866 | * |
||
| 867 | * @return void |
||
| 868 | */ |
||
| 869 | public function recover() |
||
| 870 | { |
||
| 871 | if ($this->verify() === true) { |
||
| 872 | return; |
||
| 873 | } |
||
| 874 | |||
| 875 | $meta = $this->getClassMetadata(); |
||
| 876 | $em = $this->getEntityManager(); |
||
| 877 | $config = $this->listener->getConfiguration($em, $meta->name); |
||
| 878 | $self = $this; |
||
| 879 | |||
| 880 | $doRecover = function ($root, &$count, $level = 0) use ($meta, $config, $self, $em, &$doRecover) { |
||
| 881 | $lft = $count++; |
||
| 882 | foreach ($self->getChildren($root, true) as $child) { |
||
| 883 | $doRecover($child, $count, $level + 1); |
||
| 884 | } |
||
| 885 | $rgt = $count++; |
||
| 886 | $meta->getReflectionProperty($config['left'])->setValue($root, $lft); |
||
| 887 | $meta->getReflectionProperty($config['right'])->setValue($root, $rgt); |
||
| 888 | if (isset($config['level'])) { |
||
| 889 | $meta->getReflectionProperty($config['level'])->setValue($root, $level); |
||
| 890 | } |
||
| 891 | $em->persist($root); |
||
| 892 | }; |
||
| 893 | |||
| 894 | if (isset($config['root'])) { |
||
| 895 | foreach ($this->getRootNodes() as $root) { |
||
| 896 | $count = 1; // reset on every root node |
||
| 897 | $doRecover($root, $count); |
||
| 898 | } |
||
| 899 | } else { |
||
| 900 | $count = 1; |
||
| 901 | foreach ($this->getChildren(null, true) as $root) { |
||
| 902 | $doRecover($root, $count); |
||
| 903 | } |
||
| 904 | } |
||
| 905 | } |
||
| 906 | |||
| 907 | /** |
||
| 908 | * Added in Chamilo. |
||
| 909 | */ |
||
| 910 | public function recoverNode($node, $sortByField = null) |
||
| 911 | { |
||
| 912 | $meta = $this->getClassMetadata(); |
||
| 913 | $em = $this->getEntityManager(); |
||
| 914 | $config = $this->listener->getConfiguration($em, $meta->name); |
||
| 915 | $doRecover = function ($root, &$count, $level = 0) use ($meta, $config, $node, $em, $sortByField, &$doRecover) { |
||
| 916 | $lft = $count++; |
||
| 917 | foreach ($this->getChildren($root, true, $sortByField) as $child) { |
||
| 918 | $doRecover($child, $count, $level + 1); |
||
| 919 | } |
||
| 920 | $rgt = $count++; |
||
| 921 | $meta->getReflectionProperty($config['left'])->setValue($root, $lft); |
||
| 922 | $meta->getReflectionProperty($config['right'])->setValue($root, $rgt); |
||
| 923 | if (isset($config['level'])) { |
||
| 924 | $meta->getReflectionProperty($config['level'])->setValue($root, $level); |
||
| 925 | } |
||
| 926 | $em->persist($root); |
||
| 927 | }; |
||
| 928 | |||
| 929 | $count = 1; |
||
| 930 | $doRecover($node, $count); |
||
| 931 | } |
||
| 932 | |||
| 933 | public function verifyNode($node) |
||
| 934 | { |
||
| 935 | if (!$node->childCount()) { |
||
| 936 | return true; // tree is empty |
||
| 937 | } |
||
| 938 | |||
| 939 | $errors = array(); |
||
| 940 | $this->verifyTree($errors, $node); |
||
| 941 | |||
| 942 | return $errors ?: true; |
||
| 943 | } |
||
| 944 | |||
| 945 | /** |
||
| 946 | * {@inheritDoc} |
||
| 947 | */ |
||
| 948 | public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = array(), $includeNode = false) |
||
| 949 | { |
||
| 950 | $meta = $this->getClassMetadata(); |
||
| 951 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 952 | |||
| 953 | return $this->childrenQueryBuilder( |
||
| 954 | $node, |
||
| 955 | $direct, |
||
| 956 | isset($config['root']) ? array($config['root'], $config['left']) : $config['left'], |
||
| 957 | 'ASC', |
||
| 958 | $includeNode |
||
| 959 | ); |
||
| 960 | } |
||
| 961 | |||
| 962 | /** |
||
| 963 | * {@inheritDoc} |
||
| 964 | */ |
||
| 965 | public function getNodesHierarchyQuery($node = null, $direct = false, array $options = array(), $includeNode = false) |
||
| 966 | { |
||
| 967 | return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); |
||
| 968 | } |
||
| 969 | |||
| 970 | /** |
||
| 971 | * {@inheritdoc} |
||
| 972 | */ |
||
| 973 | public function getNodesHierarchy($node = null, $direct = false, array $options = array(), $includeNode = false) |
||
| 974 | { |
||
| 975 | return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); |
||
| 976 | } |
||
| 977 | |||
| 978 | /** |
||
| 979 | * {@inheritdoc} |
||
| 980 | */ |
||
| 981 | protected function validate() |
||
| 982 | { |
||
| 983 | return $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName() === Strategy::NESTED; |
||
| 984 | } |
||
| 985 | |||
| 986 | /** |
||
| 987 | * Collect errors on given tree if |
||
| 988 | * where are any |
||
| 989 | * |
||
| 990 | * @param array $errors |
||
| 991 | * @param object $root |
||
| 992 | */ |
||
| 993 | private function verifyTree(&$errors, $root = null) |
||
| 1118 | } |
||
| 1119 | } |
||
| 1120 | } |
||
| 1121 | } |
||
| 1122 | |||
| 1123 | /** |
||
| 1124 | * Removes single node without touching children |
||
| 1125 | * |
||
| 1126 | * @internal |
||
| 1127 | * |
||
| 1128 | * @param EntityWrapper $wrapped |
||
| 1129 | */ |
||
| 1130 | private function removeSingle(EntityWrapper $wrapped) |
||
| 1131 | { |
||
| 1132 | $meta = $this->getClassMetadata(); |
||
| 1133 | $config = $this->listener->getConfiguration($this->getEntityManager(), $meta->name); |
||
| 1134 | |||
| 1135 | $pk = $meta->getSingleIdentifierFieldName(); |
||
| 1136 | $nodeId = $wrapped->getIdentifier(); |
||
| 1137 | // prevent from deleting whole branch |
||
| 1138 | $qb = $this->getQueryBuilder(); |
||
| 1139 | $qb->update($config['useObjectClass'], 'node') |
||
| 1140 | ->set('node.'.$config['left'], 0) |
||
| 1156 | } |
||
| 1157 | } |
||
| 1158 |