1<?php 2// Copyright (C) 2015-2018 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19 20/** 21 * Data structures (i.e. PHP classes) to build and use relation graphs 22 * 23 * @copyright Copyright (C) 2015-2018 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 * 26 */ 27 28require_once(APPROOT.'core/simplegraph.class.inc.php'); 29 30/** 31 * An object Node inside a RelationGraph 32 */ 33class RelationObjectNode extends GraphNode 34{ 35 public function __construct($oGraph, $oObject) 36 { 37 parent::__construct($oGraph, self::MakeId($oObject)); 38 $this->SetProperty('object', $oObject); 39 $this->SetProperty('label', get_class($oObject).'::'.$oObject->GetKey().' ('.$oObject->Get('friendlyname').')'); 40 } 41 42 /** 43 * Make a normalized ID to ensure the uniqueness of such a node 44 * 45 * @param string $oObject 46 * 47 * @return string 48 */ 49 public static function MakeId($oObject) 50 { 51 return get_class($oObject).'::'.$oObject->GetKey(); 52 } 53 54 /** 55 * Formatting for GraphViz 56 * 57 * @param bool $bNoLabel 58 * 59 * @return string 60 */ 61 public function GetDotAttributes($bNoLabel = false) 62 { 63 $sDot = parent::GetDotAttributes(); 64 if ($this->GetProperty('developped', false)) 65 { 66 $sDot .= ',fontcolor=black'; 67 } 68 else 69 { 70 $sDot .= ',fontcolor=lightgrey'; 71 } 72 if ($this->GetProperty('source', false) || $this->GetProperty('sink', false)) 73 { 74 $sDot .= ',shape=rectangle'; 75 } 76 if ($this->GetProperty('is_reached', false)) 77 { 78 $sDot .= ',fillcolor="#ffdddd"'; 79 } 80 else 81 { 82 $sDot .= ',fillcolor=white'; 83 } 84 return $sDot; 85 } 86 87 /** 88 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node or a 'not allowed' node 89 * 90 * @param string $sProperty 91 * @param $value 92 */ 93 public function ReachDown($sProperty, $value) 94 { 95 if (is_null($this->GetProperty($sProperty)) && ($this->GetProperty($sProperty.'_allowed') !== false)) 96 { 97 $this->SetProperty($sProperty, $value); 98 foreach ($this->GetOutgoingEdges() as $oOutgoingEdge) 99 { 100 // Recurse 101 $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value); 102 } 103 } 104 } 105} 106 107/** 108 * An redundancy Node inside a RelationGraph 109 */ 110class RelationRedundancyNode extends GraphNode 111{ 112 public function __construct($oGraph, $sId, $iMinUp, $fThreshold) 113 { 114 parent::__construct($oGraph, $sId); 115 $this->SetProperty('min_up', $iMinUp); 116 $this->SetProperty('threshold', $fThreshold); 117 } 118 119 /** 120 * Make a normalized ID to ensure the uniqueness of such a node 121 * 122 * @param string $sRelCode 123 * @param string $sNeighbourId 124 * @param $oSourceObject 125 * @param \DBObject $oSinkObject 126 * 127 * @return string 128 */ 129 public static function MakeId($sRelCode, $sNeighbourId, $oSourceObject, $oSinkObject) 130 { 131 return 'redundancy-'.$sRelCode.'-'.$sNeighbourId.'-'.get_class($oSinkObject).'::'.$oSinkObject->GetKey(); 132 } 133 134 /** 135 * Formatting for GraphViz 136 * 137 * @param bool $bNoLabel 138 * 139 * @return string 140 */ 141 public function GetDotAttributes($bNoLabel = false) 142 { 143 $sDisplayThreshold = sprintf('%.1f', $this->GetProperty('threshold')); 144 $sDot = 'shape=doublecircle,fillcolor=indianred,fontcolor=papayawhip,label="'.$sDisplayThreshold.'"'; 145 return $sDot; 146 } 147 148 /** 149 * Recursively mark the objects nodes as reached, unless we get stopped by a redundancy node 150 * 151 * @param string $sProperty 152 * @param $value 153 */ 154 public function ReachDown($sProperty, $value) 155 { 156 $this->SetProperty($sProperty.'_count', $this->GetProperty($sProperty.'_count', 0) + 1); 157 if ($this->GetProperty($sProperty.'_count') > $this->GetProperty('threshold')) 158 { 159 // Looping... though there should be only ONE SINGLE outgoing edge 160 foreach ($this->GetOutgoingEdges() as $oOutgoingEdge) 161 { 162 // Recurse 163 $oOutgoingEdge->GetSinkNode()->ReachDown($sProperty, $value); 164 } 165 } 166 } 167} 168 169 170/** 171 * Helper to name the edges in a unique way 172 */ 173class RelationEdge extends GraphEdge 174{ 175 /** 176 * RelationEdge constructor. 177 * 178 * @param \SimpleGraph $oGraph 179 * @param \GraphNode $oSourceNode 180 * @param \GraphNode $oSinkNode 181 * @param bool $bMustBeUnique 182 * 183 * @throws \SimpleGraphException 184 */ 185 public function __construct(SimpleGraph $oGraph, GraphNode $oSourceNode, GraphNode $oSinkNode, $bMustBeUnique = false) 186 { 187 $sId = $oSourceNode->GetId().'-to-'.$oSinkNode->GetId(); 188 parent::__construct($oGraph, $sId, $oSourceNode, $oSinkNode, $bMustBeUnique); 189 } 190} 191 192/** 193 * A graph representing the relations between objects 194 * The graph is made of two types of nodes. Here is a list of the meaningful node properties 195 * 1) RelationObjectNode 196 * source: boolean, that node was added as a source node 197 * sink: boolean, that node was added as a sink node 198 * reached: boolean, that node has been marked as reached (impacted by the source nodes) 199 * developped: boolean, that node has been visited to search for related objects 200 * 1) RelationRedundancyNode 201 * reached_count: int, the number of source nodes having reached=true 202 * threshold: float, if reached_count > threshold, the sink nodes become reachable 203 */ 204class RelationGraph extends SimpleGraph 205{ 206 protected $aSourceNodes; // Index of source nodes (for a quicker access) 207 protected $aSinkNodes; // Index of sink nodes (for a quicker access) 208 protected $aRedundancySettings; // Cache of user settings 209 protected $aContextSearches; // Context ("knowing that") stored as a hash array 'class' => DBObjectSearch 210 211 public function __construct() 212 { 213 parent::__construct(); 214 $this->aSourceNodes = array(); 215 $this->aSinkNodes = array(); 216 $this->aRedundancySettings = array(); 217 $this->aContextSearches = array(); 218 } 219 220 /** 221 * Add an object that will be the starting point for building the relations downstream 222 * 223 * @param \DBObject $oObject 224 */ 225 public function AddSourceObject(DBObject $oObject) 226 { 227 $oSourceNode = new RelationObjectNode($this, $oObject); 228 $oSourceNode->SetProperty('source', true); 229 $this->aSourceNodes[$oSourceNode->GetId()] = $oSourceNode; 230 } 231 232 /** 233 * Add an object that will be the starting point for building the relations uptream 234 * 235 * @param \DBObject $oObject 236 */ 237 public function AddSinkObject(DBObject$oObject) 238 { 239 $oSinkNode = new RelationObjectNode($this, $oObject); 240 $oSinkNode->SetProperty('sink', true); 241 $this->aSinkNodes[$oSinkNode->GetId()] = $oSinkNode; 242 } 243 244 /** 245 * Add a 'context' OQL query, specifying extra objects to be marked as 'is_reached' 246 * even though they are not part of the sources. 247 * 248 * @param string $key 249 * @param string $sOQL The OQL query defining the context objects 250 * 251 * @throws \Exception 252 */ 253 public function AddContextQuery($key, $sOQL) 254 { 255 if ($sOQL === '') { return;} 256 257 $oSearch = static::MakeSearch($sOQL); 258 $aAliases = $oSearch->GetSelectedClasses(); 259 if (count($aAliases) < 2 ) 260 { 261 IssueLog::Error("Invalid context query '$sOQL'. A context query must contain at least two columns."); 262 throw new Exception("Invalid context query '$sOQL'. A context query must contain at least two columns. Columns: ".implode(', ', $aAliases).'. '); 263 } 264 $aAliasNames = array_keys($aAliases); 265 $oCondition = new BinaryExpression(new FieldExpression('id', $aAliasNames[0]), '=', new VariableExpression('id')); 266 $oSearch->AddConditionExpression($oCondition); 267 268 $sClass = $oSearch->GetClass(); 269 if (!array_key_exists($sClass, $this->aContextSearches)) 270 { 271 $this->aContextSearches[$sClass] = array(); 272 } 273 $this->aContextSearches[$sClass][] = array('key' => $key, 'search' => $oSearch); 274 } 275 276 /** 277 * Determines if the given DBObject is part of a 'context' 278 * 279 * @param DBObject $oObj 280 * 281 * @return boolean 282 * @throws \CoreException 283 */ 284 public function IsPartOfContext(DBObject $oObj, &$aRootCauses) 285 { 286 $bRet = false; 287 $sFinalClass = get_class($oObj); 288 $aParentClasses = MetaModel::EnumParentClasses($sFinalClass, ENUM_PARENT_CLASSES_ALL); 289 290 foreach($aParentClasses as $sClass) 291 { 292 if (array_key_exists($sClass, $this->aContextSearches)) 293 { 294 foreach($this->aContextSearches[$sClass] as $aContextQuery) 295 { 296 $aAliases = $aContextQuery['search']->GetSelectedClasses(); 297 $aAliasNames = array_keys($aAliases); 298 $sRootCauseAlias = $aAliasNames[1]; // 1st column (=0) = object, second column = root cause 299 $oSet = new DBObjectSet($aContextQuery['search'], array(), array('id' => $oObj->GetKey())); 300 $oSet->OptimizeColumnLoad(array($aAliasNames[0] => array(), $aAliasNames[1] => array())); // Do not load any column... better do a reload than many joins 301 while($aRow = $oSet->FetchAssoc()) 302 { 303 if (!is_null($aRow[$sRootCauseAlias])) 304 { 305 if (!array_key_exists($aContextQuery['key'], $aRootCauses)) 306 { 307 $aRootCauses[$aContextQuery['key']] = array(); 308 } 309 $aRootCauses[$aContextQuery['key']][] = $aRow[$sRootCauseAlias]; 310 $bRet = true; 311 } 312 } 313 } 314 } 315 } 316 return $bRet; 317 } 318 319 /** 320 * Build the graph downstream, and mark the nodes that can be reached from the source node 321 * 322 * @param string $sRelCode 323 * @param int $iMaxDepth 324 * @param bool $bEnableRedundancy 325 * @param array $aUnreachableObjects 326 * 327 * @throws \CoreException 328 * @throws \Exception 329 */ 330 public function ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy, $aUnreachableObjects = array()) 331 { 332 //echo "<h5>Sources only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n"; 333 // Build the graph out of the sources 334 foreach ($this->aSourceNodes as $oSourceNode) 335 { 336 $this->AddRelatedObjects($sRelCode, true, $oSourceNode, $iMaxDepth, $bEnableRedundancy); 337 //echo "<h5>After processing of {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n"; 338 } 339 340 // Mark the unreachable nodes 341 foreach ($aUnreachableObjects as $oObj) 342 { 343 $sNodeId = RelationObjectNode::MakeId($oObj); 344 $oNode = $this->GetNode($sNodeId); 345 if($oNode) 346 { 347 $oNode->SetProperty('is_reached_allowed', false); 348 } 349 } 350 351 // Determine the reached nodes 352 foreach ($this->aSourceNodes as $oSourceNode) 353 { 354 $oSourceNode->ReachDown('is_reached', true); 355 //echo "<h5>After reaching from {$oSourceNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n"; 356 } 357 358 // Mark also the "context" nodes as reached and record the "root causes" for each node 359 $oIterator = new RelationTypeIterator($this, 'Node'); 360 foreach($oIterator as $oNode) 361 { 362 $oObj = $oNode->GetProperty('object'); 363 $aRootCauses = array(); 364 if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses)) 365 { 366 $oNode->SetProperty('context_root_causes', $aRootCauses); 367 $oNode->ReachDown('is_reached', true); 368 } 369 } 370 } 371 372 /** 373 * Build the graph upstream 374 * 375 * @param string $sRelCode 376 * @param int $iMaxDepth 377 * @param bool $bEnableRedundancy 378 * 379 * @throws \CoreException 380 * @throws \Exception 381 */ 382 public function ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy) 383 { 384 //echo "<h5>Sinks only...</h5>\n".$this->DumpAsHtmlImage()."<br/>\n"; 385 // Build the graph out of the sinks 386 foreach ($this->aSinkNodes as $oSinkNode) 387 { 388 $this->AddRelatedObjects($sRelCode, false, $oSinkNode, $iMaxDepth, $bEnableRedundancy); 389 //echo "<h5>After processing of {$oSinkNode->GetId()}</h5>\n".$this->DumpAsHtmlImage()."<br/>\n"; 390 } 391 392 // Mark also the "context" nodes as reached and record the "root causes" for each node 393 $oIterator = new RelationTypeIterator($this, 'Node'); 394 foreach($oIterator as $oNode) 395 { 396 $oObj = $oNode->GetProperty('object'); 397 $aRootCauses = array(); 398 if (!is_null($oObj) && $this->IsPartOfContext($oObj, $aRootCauses)) 399 { 400 $oNode->SetProperty('context_root_causes', $aRootCauses); 401 $oNode->ReachDown('is_reached', true); 402 } 403 } 404 } 405 406 407 /** 408 * Recursively find related objects, and add them into the graph 409 * 410 * @param string $sRelCode The code of the relation to use for the computation 411 * @param boolean $bDown The direction: downstream or upstream 412 * @param \GraphElement $oObjectNode The node from which to compute the neighbours 413 * @param int $iMaxDepth 414 * @param boolean $bEnableRedundancy 415 * 416 * @throws \Exception 417 */ 418 protected function AddRelatedObjects($sRelCode, $bDown, $oObjectNode, $iMaxDepth, $bEnableRedundancy) 419 { 420 if ($iMaxDepth > 0) 421 { 422 if ($oObjectNode instanceof RelationRedundancyNode) 423 { 424 // Note: this happens when recursing on an existing part of the graph 425 // Skip that redundancy node 426 $aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges(); 427 foreach ($aRelatedEdges as $oRelatedEdge) 428 { 429 $oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode(); 430 // Recurse (same depth) 431 $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth, $bEnableRedundancy); 432 } 433 } 434 elseif ($oObjectNode->GetProperty('developped', false)) 435 { 436 // No need to explore the underlying graph at all. We can stop here since the node has already been developped. 437 // Otherwise in case of "loops" in the graph we would recurse up to the max depth limit 438 // without producing any difference in the resulting graph... but potentially taking a LOOOONG time. 439 return; 440 441 // Former code was 442 //$aRelatedEdges = $bDown ? $oObjectNode->GetOutgoingEdges() : $oObjectNode->GetIncomingEdges(); 443 //foreach ($aRelatedEdges as $oRelatedEdge) 444 //{ 445 // $oRelatedNode = $bDown ? $oRelatedEdge->GetSinkNode() : $oRelatedEdge->GetSourceNode(); 446 // // Recurse (decrement the depth) 447 // $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy); 448 //} 449 } 450 else 451 { 452 $oObjectNode->SetProperty('developped', true); 453 454 $oObject = $oObjectNode->GetProperty('object'); 455 $iPreviousTimeLimit = ini_get('max_execution_time'); 456 $iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop'); 457 foreach (MetaModel::EnumRelationQueries(get_class($oObject), $sRelCode, $bDown) as $sDummy => $aQueryInfo) 458 { 459 $sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp']; 460 try 461 { 462 $oFlt = static::MakeSearch($sQuery); 463 $oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery()); 464 $oRelatedObj = $oObjSet->Fetch(); 465 } 466 catch (Exception $e) 467 { 468 $sDirection = $bDown ? 'downstream' : 'upstream'; 469 throw new Exception("Wrong query ($sDirection) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); 470 } 471 if ($oRelatedObj) 472 { 473 do 474 { 475 set_time_limit($iLoopTimeLimit); 476 477 $sObjectRef = RelationObjectNode::MakeId($oRelatedObj); 478 $oRelatedNode = $this->GetNode($sObjectRef); 479 if (is_null($oRelatedNode)) 480 { 481 $oRelatedNode = new RelationObjectNode($this, $oRelatedObj); 482 } 483 $oSourceNode = $bDown ? $oObjectNode : $oRelatedNode; 484 $oSinkNode = $bDown ? $oRelatedNode : $oObjectNode; 485 if ($bEnableRedundancy) 486 { 487 $oRedundancyNode = $this->ComputeRedundancy($sRelCode, $aQueryInfo, $oSourceNode, $oSinkNode); 488 } 489 else 490 { 491 $oRedundancyNode = null; 492 } 493 if (!$oRedundancyNode) 494 { 495 // Direct link (otherwise handled by ComputeRedundancy) 496 new RelationEdge($this, $oSourceNode, $oSinkNode); 497 } 498 // Recurse 499 $this->AddRelatedObjects($sRelCode, $bDown, $oRelatedNode, $iMaxDepth - 1, $bEnableRedundancy); 500 } 501 while ($oRelatedObj = $oObjSet->Fetch()); 502 } 503 } 504 set_time_limit($iPreviousTimeLimit); 505 } 506 } 507 } 508 509 /** 510 * Determine if there is a redundancy (or use the existing one) and add the corresponding nodes/edges 511 * 512 * @param string $sRelCode 513 * @param array $aQueryInfo 514 * @param GraphElement $oFromNode 515 * @param GraphElement $oToNode 516 * 517 * @return \GraphNode|NULL|\RelationRedundancyNode 518 * @throws \Exception 519 */ 520 protected function ComputeRedundancy($sRelCode, $aQueryInfo, $oFromNode, $oToNode) 521 { 522 $oRedundancyNode = null; 523 $oObject = $oToNode->GetProperty('object'); 524 if ($this->IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode)) 525 { 526 $sUniqueNeighbourId = $aQueryInfo['sDefinedInClass'].'-'.$aQueryInfo['sNeighbour']; 527 $sId = RelationRedundancyNode::MakeId($sRelCode, $sUniqueNeighbourId, $oFromNode->GetProperty('object'), $oToNode->GetProperty('object')); 528 529 $oRedundancyNode = $this->GetNode($sId); 530 if (is_null($oRedundancyNode)) 531 { 532 // Get the upper neighbours 533 $sQuery = $aQueryInfo['sQueryUp']; 534 if (!$sQuery) 535 { 536 throw new Exception("Redundancy cannot be enabled on the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: its direction is \"{$aQueryInfo['sDirection']}\""); 537 } 538 try 539 { 540 $oFlt = static::MakeSearch($sQuery); 541 $oObjSet = new DBObjectSet($oFlt, array(), $oObject->ToArgsForQuery()); 542 $iCount = $oObjSet->Count(); 543 } 544 catch (Exception $e) 545 { 546 throw new Exception("Wrong query (upstream) for the relation $sRelCode/{$aQueryInfo['sDefinedInClass']}/{$aQueryInfo['sNeighbour']}: ".$e->getMessage()); 547 } 548 549 $iMinUp = $this->GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iCount); 550 $fThreshold = max(0, $iCount - $iMinUp); 551 $oRedundancyNode = new RelationRedundancyNode($this, $sId, $iMinUp, $fThreshold); 552 new RelationEdge($this, $oRedundancyNode, $oToNode); 553 554 while ($oUpperObj = $oObjSet->Fetch()) 555 { 556 $sObjectRef = RelationObjectNode::MakeId($oUpperObj); 557 $oUpperNode = $this->GetNode($sObjectRef); 558 if (is_null($oUpperNode)) 559 { 560 $oUpperNode = new RelationObjectNode($this, $oUpperObj); 561 } 562 new RelationEdge($this, $oUpperNode, $oRedundancyNode); 563 } 564 } 565 } 566 return $oRedundancyNode; 567 } 568 569 /** 570 * Helper to determine the redundancy setting on a given relation 571 * 572 * @param string $sRelCode 573 * @param array $aQueryInfo 574 * @param GraphElement $oToNode 575 * 576 * @return bool 577 */ 578 protected function IsRedundancyEnabled($sRelCode, $aQueryInfo, $oToNode) 579 { 580 $bRet = false; 581 $oToObject = $oToNode->GetProperty('object'); 582 $oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject)); 583 if ($oRedundancyAttDef) 584 { 585 $sValue = $oToObject->Get($oRedundancyAttDef->GetCode()); 586 $bRet = $oRedundancyAttDef->IsEnabled($sValue); 587 } 588 return $bRet; 589 } 590 591 /** 592 * Helper to determine the redundancy threshold, given the count of objects upstream 593 * 594 * @param string $sRelCode 595 * @param array $aQueryInfo 596 * @param GraphElement $oToNode 597 * @param int $iUpstreamObjects 598 * 599 * @return int 600 */ 601 protected function GetRedundancyMinUp($sRelCode, $aQueryInfo, $oToNode, $iUpstreamObjects) 602 { 603 $iMinUp = 0; 604 605 $oToObject = $oToNode->GetProperty('object'); 606 $oRedundancyAttDef = $this->FindRedundancyAttribute($sRelCode, $aQueryInfo, get_class($oToObject)); 607 if ($oRedundancyAttDef) 608 { 609 $sValue = $oToObject->Get($oRedundancyAttDef->GetCode()); 610 if ($oRedundancyAttDef->GetMinUpType($sValue) == 'count') 611 { 612 $iMinUp = $oRedundancyAttDef->GetMinUpValue($sValue); 613 } 614 else 615 { 616 $iMinUp = $iUpstreamObjects * $oRedundancyAttDef->GetMinUpValue($sValue) / 100; 617 } 618 } 619 return $iMinUp; 620 } 621 622 /** 623 * Helper to search for the redundancy attribute 624 * 625 * @param string $sRelCode 626 * @param array $aQueryInfo 627 * @param string $sClass 628 * 629 * @return \AttributeDefinition|\AttributeRedundancySettings|null 630 */ 631 protected function FindRedundancyAttribute($sRelCode, $aQueryInfo, $sClass) 632 { 633 $oRet = null; 634 foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) 635 { 636 if ($oAttDef instanceof AttributeRedundancySettings) 637 { 638 if ($oAttDef->Get('relation_code') == $sRelCode) 639 { 640 if ($oAttDef->Get('from_class') == $aQueryInfo['sFromClass']) 641 { 642 if ($oAttDef->Get('neighbour_id') == $aQueryInfo['sNeighbour']) 643 { 644 $oRet = $oAttDef; 645 break; 646 } 647 } 648 } 649 } 650 } 651 return $oRet; 652 } 653 654 /** 655 * Get the objects referenced by the graph as a hash array: 'class' => array of objects 656 * @return array Ambigous <multitype:multitype: , unknown> 657 */ 658 public function GetObjectsByClass() 659 { 660 $aResults = array(); 661 $oIterator = new RelationTypeIterator($this, 'Node'); 662 foreach($oIterator as $oNode) 663 { 664 $oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object 665 if ($oObj) 666 { 667 $sObjClass = get_class($oObj); 668 if (!array_key_exists($sObjClass, $aResults)) 669 { 670 $aResults[$sObjClass] = array(); 671 } 672 $aResults[$sObjClass][] = $oObj; 673 } 674 } 675 return $aResults; 676 } 677 678 /** 679 * @param string $sOQL 680 * 681 * @return \DBSearch 682 * @throws \CoreException 683 * @throws \OQLException 684 */ 685 protected static function MakeSearch($sOQL) 686 { 687 $oSearch = DBSearch::FromOQL($sOQL); 688 if (MetaModel::IsObsoletable($oSearch->GetClass())) 689 { 690 // Exclude obsolete objects anytime 691 $oSearch->AddCondition('obsolescence_flag', 0); 692 } 693 // Exclude archived objects anytime 694 $oSearch->SetArchiveMode(false); 695 return $oSearch; 696 } 697} 698