1<?php 2// Copyright (c) 2010-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// Dev hack for disabling the some query build optimizations (Folding/Merging) 21define('ENABLE_OPT', true); 22 23class DBObjectSearch extends DBSearch 24{ 25 private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters) 26 private $m_aSelectedClasses; // selected for the output (alias => class name) 27 private $m_oSearchCondition; 28 private $m_aParams; 29 private $m_aPointingTo; 30 private $m_aReferencedBy; 31 32 // By default, some information may be hidden to the current user 33 // But it may happen that we need to disable that feature 34 protected $m_bAllowAllData = false; 35 protected $m_bDataFiltered = false; 36 37 public function __construct($sClass, $sClassAlias = null) 38 { 39 parent::__construct(); 40 41 if (is_null($sClassAlias)) $sClassAlias = $sClass; 42 if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true)); 43 if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"'); 44 45 $this->m_aSelectedClasses = array($sClassAlias => $sClass); 46 $this->m_aClasses = array($sClassAlias => $sClass); 47 $this->m_oSearchCondition = new TrueExpression; 48 $this->m_aParams = array(); 49 $this->m_aPointingTo = array(); 50 $this->m_aReferencedBy = array(); 51 } 52 53 public function AllowAllData($bAllowAllData = true) {$this->m_bAllowAllData = $bAllowAllData;} 54 public function IsAllDataAllowed() {return $this->m_bAllowAllData;} 55 protected function IsDataFiltered() {return $this->m_bDataFiltered; } 56 protected function SetDataFiltered() {$this->m_bDataFiltered = true;} 57 58 // Create a search definition that leads to 0 result, still a valid search object 59 static public function FromEmptySet($sClass) 60 { 61 $oResultFilter = new DBObjectSearch($sClass); 62 $oResultFilter->m_oSearchCondition = new FalseExpression; 63 return $oResultFilter; 64 } 65 66 67 public function GetJoinedClasses() {return $this->m_aClasses;} 68 69 public function GetClassName($sAlias) 70 { 71 if (array_key_exists($sAlias, $this->m_aSelectedClasses)) 72 { 73 return $this->m_aSelectedClasses[$sAlias]; 74 } 75 else 76 { 77 throw new CoreException("Invalid class alias '$sAlias'"); 78 } 79 } 80 81 public function GetClass() 82 { 83 return reset($this->m_aSelectedClasses); 84 } 85 public function GetClassAlias() 86 { 87 reset($this->m_aSelectedClasses); 88 return key($this->m_aSelectedClasses); 89 } 90 91 public function GetFirstJoinedClass() 92 { 93 return reset($this->m_aClasses); 94 } 95 public function GetFirstJoinedClassAlias() 96 { 97 reset($this->m_aClasses); 98 return key($this->m_aClasses); 99 } 100 101 /** 102 * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) 103 * Defaults to the first selected class (most of the time it is also the first joined class 104 * 105 * @param $sNewClass 106 * @param null $sAlias 107 * 108 * @throws \CoreException 109 */ 110 public function ChangeClass($sNewClass, $sAlias = null) 111 { 112 if (is_null($sAlias)) 113 { 114 $sAlias = $this->GetClassAlias(); 115 } 116 else 117 { 118 if (!array_key_exists($sAlias, $this->m_aSelectedClasses)) 119 { 120 // discard silently - necessary when recursing on the related nodes (see code below) 121 return; 122 } 123 } 124 $sCurrClass = $this->GetClassName($sAlias); 125 if ($sNewClass == $sCurrClass) 126 { 127 // Skip silently 128 return; 129 } 130 if (!MetaModel::IsParentClass($sCurrClass, $sNewClass)) 131 { 132 throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted."); 133 } 134 135 // Change for this node 136 // 137 $this->m_aSelectedClasses[$sAlias] = $sNewClass; 138 $this->m_aClasses[$sAlias] = $sNewClass; 139 140 // Change for all the related node (yes, this was necessary with some queries - strange effects otherwise) 141 // 142 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 143 { 144 foreach($aPointingTo as $iOperatorCode => $aFilter) 145 { 146 foreach($aFilter as $oExtFilter) 147 { 148 $oExtFilter->ChangeClass($sNewClass, $sAlias); 149 } 150 } 151 } 152 foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) 153 { 154 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 155 { 156 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 157 { 158 foreach ($aFilters as $oForeignFilter) 159 { 160 $oForeignFilter->ChangeClass($sNewClass, $sAlias); 161 } 162 } 163 } 164 } 165 } 166 167 public function GetSelectedClasses() 168 { 169 return $this->m_aSelectedClasses; 170 } 171 172 /** 173 * @param array $aSelectedClasses array of aliases 174 * @throws CoreException 175 */ 176 public function SetSelectedClasses($aSelectedClasses) 177 { 178 $this->m_aSelectedClasses = array(); 179 foreach ($aSelectedClasses as $sAlias) 180 { 181 if (!array_key_exists($sAlias, $this->m_aClasses)) 182 { 183 throw new CoreException("SetSelectedClasses: Invalid class alias $sAlias"); 184 } 185 $this->m_aSelectedClasses[$sAlias] = $this->m_aClasses[$sAlias]; 186 } 187 } 188 189 /** 190 * Change any alias of the query tree 191 * 192 * @param $sOldName 193 * @param $sNewName 194 * 195 * @return bool True if the alias has been found and changed 196 * @throws \Exception 197 */ 198 public function RenameAlias($sOldName, $sNewName) 199 { 200 $bFound = false; 201 if (array_key_exists($sOldName, $this->m_aClasses)) 202 { 203 $bFound = true; 204 } 205 if (array_key_exists($sNewName, $this->m_aClasses)) 206 { 207 throw new Exception("RenameAlias: alias '$sNewName' already used"); 208 } 209 210 $aClasses = array(); 211 foreach ($this->m_aClasses as $sAlias => $sClass) 212 { 213 if ($sAlias === $sOldName) 214 { 215 $aClasses[$sNewName] = $sClass; 216 } 217 else 218 { 219 $aClasses[$sAlias] = $sClass; 220 } 221 } 222 $this->m_aClasses = $aClasses; 223 224 $aSelectedClasses = array(); 225 foreach ($this->m_aSelectedClasses as $sAlias => $sClass) 226 { 227 if ($sAlias === $sOldName) 228 { 229 $aSelectedClasses[$sNewName] = $sClass; 230 } 231 else 232 { 233 $aSelectedClasses[$sAlias] = $sClass; 234 } 235 } 236 $this->m_aSelectedClasses = $aSelectedClasses; 237 238 $this->m_oSearchCondition->RenameAlias($sOldName, $sNewName); 239 240 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 241 { 242 foreach($aPointingTo as $iOperatorCode => $aFilter) 243 { 244 foreach($aFilter as $oExtFilter) 245 { 246 $bFound = $oExtFilter->RenameAlias($sOldName, $sNewName) || $bFound; 247 } 248 } 249 } 250 foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) 251 { 252 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 253 { 254 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 255 { 256 foreach ($aFilters as $oForeignFilter) 257 { 258 $bFound = $oForeignFilter->RenameAlias($sOldName, $sNewName) || $bFound; 259 } 260 } 261 } 262 } 263 return $bFound; 264 } 265 266 public function SetModifierProperty($sPluginClass, $sProperty, $value) 267 { 268 $this->m_aModifierProperties[$sPluginClass][$sProperty] = $value; 269 } 270 271 public function GetModifierProperties($sPluginClass) 272 { 273 if (array_key_exists($sPluginClass, $this->m_aModifierProperties)) 274 { 275 return $this->m_aModifierProperties[$sPluginClass]; 276 } 277 else 278 { 279 return array(); 280 } 281 } 282 283 public function IsAny() 284 { 285 if (!$this->m_oSearchCondition->IsTrue()) return false; 286 if (count($this->m_aPointingTo) > 0) return false; 287 if (count($this->m_aReferencedBy) > 0) return false; 288 return true; 289 } 290 291 protected function TransferConditionExpression($oFilter, $aTranslation) 292 { 293 // Prevent collisions in the parameter names by renaming them if needed 294 foreach($this->m_aParams as $sParam => $value) 295 { 296 if (array_key_exists($sParam, $oFilter->m_aParams) && ($value != $oFilter->m_aParams[$sParam])) 297 { 298 // Generate a new and unique name for the collinding parameter 299 $index = 1; 300 while(array_key_exists($sParam.$index, $oFilter->m_aParams)) 301 { 302 $index++; 303 } 304 $secondValue = $oFilter->m_aParams[$sParam]; 305 $oFilter->RenameParam($sParam, $sParam.$index); 306 unset($oFilter->m_aParams[$sParam]); 307 $oFilter->m_aParams[$sParam.$index] = $secondValue; 308 } 309 } 310 $oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */); 311 $this->AddConditionExpression($oTranslated); 312 $this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams); 313 } 314 315 protected function RenameParam($sOldName, $sNewName) 316 { 317 $this->m_oSearchCondition->RenameParam($sOldName, $sNewName); 318 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 319 { 320 foreach($aPointingTo as $iOperatorCode => $aFilter) 321 { 322 foreach($aFilter as $oExtFilter) 323 { 324 $oExtFilter->RenameParam($sOldName, $sNewName); 325 } 326 } 327 } 328 foreach($this->m_aReferencedBy as $sForeignClass => $aReferences) 329 { 330 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 331 { 332 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 333 { 334 foreach ($aFilters as $oForeignFilter) 335 { 336 $oForeignFilter->RenameParam($sOldName, $sNewName); 337 } 338 } 339 } 340 } 341 } 342 343 public function ResetCondition() 344 { 345 $this->m_oSearchCondition = new TrueExpression(); 346 // ? is that usefull/enough, do I need to rebuild the list after the subqueries ? 347 } 348 349 public function MergeConditionExpression($oExpression) 350 { 351 $this->m_oSearchCondition = $this->m_oSearchCondition->LogOr($oExpression); 352 } 353 354 public function AddConditionExpression($oExpression) 355 { 356 $this->m_oSearchCondition = $this->m_oSearchCondition->LogAnd($oExpression); 357 } 358 359 public function AddNameCondition($sName) 360 { 361 $oValueExpr = new ScalarExpression($sName); 362 $oNameExpr = new FieldExpression('friendlyname', $this->GetClassAlias()); 363 $oNewCondition = new BinaryExpression($oNameExpr, '=', $oValueExpr); 364 $this->AddConditionExpression($oNewCondition); 365 } 366 367 /** 368 * @param string $sFilterCode 369 * @param mixed $value 370 * @param string $sOpCode operator to use : 'IN', 'NOT IN', 'Contains',' Begins with', 'Finishes with', ... 371 * @param bool $bParseSearchString 372 * 373 * @throws \CoreException 374 * 375 * @see AddConditionForInOperatorUsingParam for IN/NOT IN queries with lots of params 376 */ 377 public function AddCondition($sFilterCode, $value, $sOpCode = null, $bParseSearchString = false) 378 { 379 MyHelpers::CheckKeyInArray('filter code in class: '.$this->GetClass(), $sFilterCode, MetaModel::GetClassFilterDefs($this->GetClass())); 380 381 $oField = new FieldExpression($sFilterCode, $this->GetClassAlias()); 382 if (empty($sOpCode)) 383 { 384 if ($sFilterCode == 'id') 385 { 386 $sOpCode = '='; 387 } 388 else 389 { 390 $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); 391 $oNewCondition = $oAttDef->GetSmartConditionExpression($value, $oField, $this->m_aParams); 392 $this->AddConditionExpression($oNewCondition); 393 return; 394 } 395 } 396 // Parse search strings if needed and if the filter code corresponds to a valid attcode 397 if($bParseSearchString && MetaModel::IsValidAttCode($this->GetClass(), $sFilterCode)) 398 { 399 $oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sFilterCode); 400 $value = $oAttDef->ParseSearchString($value); 401 } 402 403 // Preserve backward compatibility - quick n'dirty way to change that API semantic 404 // 405 switch($sOpCode) 406 { 407 case 'SameDay': 408 case 'SameMonth': 409 case 'SameYear': 410 case 'Today': 411 case '>|': 412 case '<|': 413 case '=|': 414 throw new CoreException('Deprecated operator, please consider using OQL (SQL) expressions like "(TO_DAYS(NOW()) - TO_DAYS(x)) AS AgeDays"', array('operator' => $sOpCode)); 415 break; 416 417 case 'IN': 418 if (!is_array($value)) $value = array($value); 419 if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); 420 $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; 421 $sOQLCondition = $oField->Render()." IN $sListExpr"; 422 break; 423 424 case 'NOTIN': 425 if (!is_array($value)) $value = array($value); 426 if (count($value) === 0) throw new Exception('AddCondition '.$sOpCode.': Value cannot be an empty array.'); 427 $sListExpr = '('.implode(', ', CMDBSource::Quote($value)).')'; 428 $sOQLCondition = $oField->Render()." NOT IN $sListExpr"; 429 break; 430 431 case 'Contains': 432 $this->m_aParams[$sFilterCode] = "%$value%"; 433 $sOperator = 'LIKE'; 434 break; 435 436 case 'Begins with': 437 $this->m_aParams[$sFilterCode] = "$value%"; 438 $sOperator = 'LIKE'; 439 break; 440 441 case 'Finishes with': 442 $this->m_aParams[$sFilterCode] = "%$value"; 443 $sOperator = 'LIKE'; 444 break; 445 446 default: 447 if ($value === null) 448 { 449 switch ($sOpCode) 450 { 451 case '=': 452 $sOpCode = '*Expression*'; 453 $oExpression = new FunctionExpression('ISNULL', array($oField)); 454 break; 455 case '!=': 456 $sOpCode = '*Expression*'; 457 $oExpression = new FunctionExpression('ISNULL', array($oField)); 458 $oExpression = new BinaryExpression($oExpression, '=', new ScalarExpression(0)); 459 break; 460 default: 461 throw new Exception("AddCondition on null value: unsupported operator '$sOpCode''"); 462 } 463 } 464 else 465 { 466 $this->m_aParams[$sFilterCode] = $value; 467 $sOperator = $sOpCode; 468 } 469 } 470 471 switch($sOpCode) 472 { 473 case '*Expression*': 474 $oNewCondition = $oExpression; 475 break; 476 case "IN": 477 case "NOTIN": 478 // this will parse all of the values... Can take forever if there are lots of them ! 479 // In this case using a parameter is far better : WHERE ... IN (:my_param) 480 $oNewCondition = Expression::FromOQL($sOQLCondition); 481 break; 482 483 case 'MATCHES': 484 $oRightExpr = new ScalarExpression($value); 485 $oNewCondition = new MatchExpression($oField, $oRightExpr); 486 break; 487 488 case 'Contains': 489 case 'Begins with': 490 case 'Finishes with': 491 default: 492 $oRightExpr = new VariableExpression($sFilterCode); 493 $oNewCondition = new BinaryExpression($oField, $sOperator, $oRightExpr); 494 } 495 496 $this->AddConditionExpression($oNewCondition); 497 } 498 499 /** 500 * @param string $sFilterCode attribute code to use 501 * @param array $aValues 502 * @param bool $bPositiveMatch if true will add a IN filter, else a NOT IN 503 * 504 * @throws \CoreException 505 * 506 * @since 2.5 N°1418 507 */ 508 public function AddConditionForInOperatorUsingParam($sFilterCode, $aValues, $bPositiveMatch = true) 509 { 510 $oFieldExpression = new FieldExpression($sFilterCode, $this->GetClassAlias()); 511 512 $sOperator = $bPositiveMatch ? 'IN' : 'NOT IN'; 513 514 $sInParamName = $this->GenerateUniqueParamName(); 515 $oParamExpression = new VariableExpression($sInParamName); 516 $this->GetInternalParamsByRef()[$sInParamName] = $aValues; 517 518 $oListExpression = new ListExpression(array($oParamExpression)); 519 520 $oInCondition = new BinaryExpression($oFieldExpression, $sOperator, $oListExpression); 521 $this->AddConditionExpression($oInCondition); 522 } 523 524 /** 525 * Specify a condition on external keys or link sets 526 * @param string $sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively 527 * Example: infra_list->ci_id->location_id->country 528 * @param $value 529 * @return void 530 * @throws \CoreException 531 * @throws \CoreWarning 532 */ 533 public function AddConditionAdvanced($sAttSpec, $value) 534 { 535 $sClass = $this->GetClass(); 536 537 $iPos = strpos($sAttSpec, '->'); 538 if ($iPos !== false) 539 { 540 $sAttCode = substr($sAttSpec, 0, $iPos); 541 $sSubSpec = substr($sAttSpec, $iPos + 2); 542 543 if (!MetaModel::IsValidAttCode($sClass, $sAttCode)) 544 { 545 throw new Exception("Invalid attribute code '$sClass/$sAttCode' in condition specification '$sAttSpec'"); 546 } 547 548 $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); 549 if ($oAttDef->IsLinkSet()) 550 { 551 $sTargetClass = $oAttDef->GetLinkedClass(); 552 $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); 553 554 $oNewFilter = new DBObjectSearch($sTargetClass); 555 $oNewFilter->AddConditionAdvanced($sSubSpec, $value); 556 557 $this->AddCondition_ReferencedBy($oNewFilter, $sExtKeyToMe); 558 } 559 elseif ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE)) 560 { 561 $sTargetClass = $oAttDef->GetTargetClass(EXTKEY_ABSOLUTE); 562 563 $oNewFilter = new DBObjectSearch($sTargetClass); 564 $oNewFilter->AddConditionAdvanced($sSubSpec, $value); 565 566 $this->AddCondition_PointingTo($oNewFilter, $sAttCode); 567 } 568 else 569 { 570 throw new Exception("Attribute specification '$sAttSpec', '$sAttCode' should be either a link set or an external key"); 571 } 572 } 573 else 574 { 575 // $sAttSpec is an attribute code 576 // 577 if (is_array($value)) 578 { 579 $oField = new FieldExpression($sAttSpec, $this->GetClass()); 580 $oListExpr = ListExpression::FromScalars($value); 581 $oInValues = new BinaryExpression($oField, 'IN', $oListExpr); 582 583 $this->AddConditionExpression($oInValues); 584 } 585 else 586 { 587 $this->AddCondition($sAttSpec, $value); 588 } 589 } 590 } 591 592 public function AddCondition_FullText($sNeedle) 593 { 594 // Transform the full text condition into additional condition expression 595 $aFullTextFields = array(); 596 foreach (MetaModel::ListAttributeDefs($this->GetClass()) as $sAttCode => $oAttDef) 597 { 598 if (!$oAttDef->IsScalar()) continue; 599 if ($oAttDef->IsExternalKey()) continue; 600 $aFullTextFields[] = new FieldExpression($sAttCode, $this->GetClassAlias()); 601 } 602 $oTextFields = new CharConcatWSExpression(' ', $aFullTextFields); 603 604 $sQueryParam = 'needle'; 605 $oFlexNeedle = new CharConcatExpression(array(new ScalarExpression('%'), new VariableExpression($sQueryParam), new ScalarExpression('%'))); 606 607 $oNewCond = new BinaryExpression($oTextFields, 'LIKE', $oFlexNeedle); 608 $this->AddConditionExpression($oNewCond); 609 $this->m_aParams[$sQueryParam] = $sNeedle; 610 } 611 612 protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true) 613 { 614 if ($bTranslateMainAlias) 615 { 616 $sOrigAlias = $this->GetFirstJoinedClassAlias(); 617 if (array_key_exists($sOrigAlias, $aClassAliases)) 618 { 619 $sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetFirstJoinedClass()); 620 if (isset($this->m_aSelectedClasses[$sOrigAlias])) 621 { 622 $this->m_aSelectedClasses[$sNewAlias] = $this->GetFirstJoinedClass(); 623 unset($this->m_aSelectedClasses[$sOrigAlias]); 624 } 625 626 // TEMPORARY ALGORITHM (m_aClasses is not correctly updated, it is not possible to add a subtree onto a subnode) 627 // Replace the element at the same position (unset + set is not enough because the hash array is ordered) 628 $aPrevList = $this->m_aClasses; 629 $this->m_aClasses = array(); 630 foreach ($aPrevList as $sSomeAlias => $sSomeClass) 631 { 632 if ($sSomeAlias == $sOrigAlias) 633 { 634 $this->m_aClasses[$sNewAlias] = $sSomeClass; // note: GetFirstJoinedClass now returns '' !!! 635 } 636 else 637 { 638 $this->m_aClasses[$sSomeAlias] = $sSomeClass; 639 } 640 } 641 642 // Translate the condition expression with the new alias 643 $aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias; 644 } 645 646 // add the alias into the filter aliases list 647 $aClassAliases[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); 648 } 649 650 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 651 { 652 foreach($aPointingTo as $iOperatorCode => $aFilter) 653 { 654 foreach($aFilter as $oFilter) 655 { 656 $oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); 657 } 658 } 659 } 660 661 foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) 662 { 663 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 664 { 665 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 666 { 667 foreach ($aFilters as $oForeignFilter) 668 { 669 $oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); 670 } 671 } 672 } 673 } 674 } 675 676 677 // Browse the tree nodes recursively 678 // 679 protected function GetNode($sAlias) 680 { 681 if ($this->GetFirstJoinedClassAlias() == $sAlias) 682 { 683 return $this; 684 } 685 else 686 { 687 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 688 { 689 foreach($aPointingTo as $iOperatorCode => $aFilter) 690 { 691 foreach($aFilter as $oFilter) 692 { 693 $ret = $oFilter->GetNode($sAlias); 694 if (is_object($ret)) 695 { 696 return $ret; 697 } 698 } 699 } 700 } 701 foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) 702 { 703 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 704 { 705 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 706 { 707 foreach ($aFilters as $oForeignFilter) 708 { 709 $ret = $oForeignFilter->GetNode($sAlias); 710 if (is_object($ret)) 711 { 712 return $ret; 713 } 714 } 715 } 716 } 717 } 718 } 719 // Not found 720 return null; 721 } 722 723 /** 724 * Helper to 725 * - convert a translation table (format optimized for the translation in an expression tree) into simple hash 726 * - compile over an eventually existing map 727 * 728 * @param array $aRealiasingMap Map to update 729 * @param array $aAliasTranslation Translation table resulting from calls to MergeWith_InNamespace 730 * @return void of <old-alias> => <new-alias> 731 */ 732 protected function UpdateRealiasingMap(&$aRealiasingMap, $aAliasTranslation) 733 { 734 if ($aRealiasingMap !== null) 735 { 736 foreach ($aAliasTranslation as $sPrevAlias => $aRules) 737 { 738 if (isset($aRules['*'])) 739 { 740 $sNewAlias = $aRules['*']; 741 $sOriginalAlias = array_search($sPrevAlias, $aRealiasingMap); 742 if ($sOriginalAlias !== false) 743 { 744 $aRealiasingMap[$sOriginalAlias] = $sNewAlias; 745 } 746 else 747 { 748 $aRealiasingMap[$sPrevAlias] = $sNewAlias; 749 } 750 } 751 } 752 } 753 } 754 755 /** 756 * Completes the list of alias=>class by browsing the whole structure recursively 757 * This a workaround to handle some cases in which the list of classes is not correctly updated. 758 * This code should disappear as soon as DBObjectSearch get split between a container search class and a Node class 759 * 760 * @param array $aClasses List to be completed 761 */ 762 protected function RecomputeClassList(&$aClasses) 763 { 764 $aClasses[$this->GetFirstJoinedClassAlias()] = $this->GetFirstJoinedClass(); 765 766 // Recurse in the query tree 767 foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 768 { 769 foreach($aPointingTo as $iOperatorCode => $aFilter) 770 { 771 foreach($aFilter as $oFilter) 772 { 773 $oFilter->RecomputeClassList($aClasses); 774 } 775 } 776 } 777 778 foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) 779 { 780 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 781 { 782 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 783 { 784 foreach ($aFilters as $oForeignFilter) 785 { 786 $oForeignFilter->RecomputeClassList($aClasses); 787 } 788 } 789 } 790 } 791 } 792 793 /** 794 * @param DBObjectSearch $oFilter 795 * @param $sExtKeyAttCode 796 * @param int $iOperatorCode 797 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed 798 * @throws CoreException 799 * @throws CoreWarning 800 */ 801 public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) 802 { 803 if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode)) 804 { 805 throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}'"); 806 } 807 $oAttExtKey = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode); 808 if(!MetaModel::IsSameFamilyBranch($oFilter->GetClass(), $oAttExtKey->GetTargetClass())) 809 { 810 throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); 811 } 812 if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey)) 813 { 814 throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey"); 815 } 816 // Note: though it seems to be a good practice to clone the given source filter 817 // (as it was done and fixed an issue in Intersect()) 818 // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) 819 // root cause: FromOQL relies on the fact that the passed filter can be modified later 820 // NO: $oFilter = $oFilter->DeepClone(); 821 // See also: Trac #639, and self::AddCondition_ReferencedBy() 822 $aAliasTranslation = array(); 823 $res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); 824 $this->TransferConditionExpression($oFilter, $aAliasTranslation); 825 $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); 826 827 if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) 828 { 829 if (isset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode])) 830 { 831 foreach ($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) 832 { 833 if ($this->GetClass() == $oRemoteFilter->GetClass()) 834 { 835 // Optimization - fold sibling query 836 $aAliasTranslation = array(); 837 $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); 838 unset($oFilter->m_aReferencedBy[$this->GetClass()][$sExtKeyAttCode][$iOperatorCode]); 839 $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); 840 $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); 841 break; 842 } 843 } 844 } 845 } 846 $this->RecomputeClassList($this->m_aClasses); 847 return $res; 848 } 849 850 protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) 851 { 852 // Find the node on which the new tree must be attached (most of the time it is "this") 853 $oReceivingFilter = $this->GetNode($this->GetClassAlias()); 854 855 $bMerged = false; 856 if (ENABLE_OPT && isset($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode])) 857 { 858 foreach ($oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] as $oExisting) 859 { 860 if ($oExisting->GetClass() == $oFilter->GetClass()) 861 { 862 $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); 863 $bMerged = true; 864 break; 865 } 866 } 867 } 868 if (!$bMerged) 869 { 870 $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); 871 $oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter; 872 } 873 } 874 875 /** 876 * @param DBObjectSearch $oFilter 877 * @param $sForeignExtKeyAttCode 878 * @param int $iOperatorCode 879 * @param null $aRealiasingMap array of <old-alias> => <new-alias>, for each alias that has changed 880 * @return void 881 * @throws \CoreException 882 */ 883 public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) 884 { 885 $sForeignClass = $oFilter->GetClass(); 886 if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode)) 887 { 888 throw new CoreException("The attribute code '$sForeignExtKeyAttCode' is not an external key of the class '{$sForeignClass}'"); 889 } 890 $oAttExtKey = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); 891 if(!MetaModel::IsSameFamilyBranch($this->GetClass(), $oAttExtKey->GetTargetClass())) 892 { 893 // à refaire en spécifique dans FromOQL 894 throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}"); 895 } 896 897 // Note: though it seems to be a good practice to clone the given source filter 898 // (as it was done and fixed an issue in Intersect()) 899 // this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge) 900 // root cause: FromOQL relies on the fact that the passed filter can be modified later 901 // NO: $oFilter = $oFilter->DeepClone(); 902 // See also: Trac #639, and self::AddCondition_PointingTo() 903 $aAliasTranslation = array(); 904 $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode); 905 $this->TransferConditionExpression($oFilter, $aAliasTranslation); 906 $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); 907 908 if (ENABLE_OPT && ($oFilter->GetClass() == $oFilter->GetFirstJoinedClass())) 909 { 910 if (isset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode])) 911 { 912 foreach ($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode] as $oRemoteFilter) 913 { 914 if ($this->GetClass() == $oRemoteFilter->GetClass()) 915 { 916 // Optimization - fold sibling query 917 $aAliasTranslation = array(); 918 $this->MergeWith_InNamespace($oRemoteFilter, $this->m_aClasses, $aAliasTranslation); 919 unset($oFilter->m_aPointingTo[$sForeignExtKeyAttCode][$iOperatorCode]); 920 $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false); 921 $this->UpdateRealiasingMap($aRealiasingMap, $aAliasTranslation); 922 break; 923 } 924 } 925 } 926 } 927 $this->RecomputeClassList($this->m_aClasses); 928 } 929 930 protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode) 931 { 932 $sForeignClass = $oFilter->GetClass(); 933 934 // Find the node on which the new tree must be attached (most of the time it is "this") 935 $oReceivingFilter = $this->GetNode($this->GetClassAlias()); 936 937 $bMerged = false; 938 if (ENABLE_OPT && isset($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode])) 939 { 940 foreach ($oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode] as $oExisting) 941 { 942 if ($oExisting->GetClass() == $oFilter->GetClass()) 943 { 944 $oExisting->MergeWith_InNamespace($oFilter, $oExisting->m_aClasses, $aAliasTranslation); 945 $bMerged = true; 946 break; 947 } 948 } 949 } 950 if (!$bMerged) 951 { 952 $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); 953 $oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode][] = $oFilter; 954 } 955 } 956 957 public function Intersect(DBSearch $oFilter) 958 { 959 if ($oFilter instanceof DBUnionSearch) 960 { 961 // Develop! 962 $aFilters = $oFilter->GetSearches(); 963 } 964 else 965 { 966 $aFilters = array($oFilter); 967 } 968 969 $aSearches = array(); 970 foreach ($aFilters as $oRightFilter) 971 { 972 // Limitation: the queried class must be the first declared class 973 if ($this->GetFirstJoinedClassAlias() != $this->GetClassAlias()) 974 { 975 throw new CoreException("Limitation: cannot merge two queries if the queried class ({$this->GetClass()} AS {$this->GetClassAlias()}) is not the first joined class ({$this->GetFirstJoinedClass()} AS {$this->GetFirstJoinedClassAlias()})"); 976 } 977 if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias()) 978 { 979 throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})"); 980 } 981 982 $oLeftFilter = $this->DeepClone(); 983 $oRightFilter = $oRightFilter->DeepClone(); 984 985 $bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed()); 986 if ($bAllowAllData) 987 { 988 $oLeftFilter->AllowAllData(); 989 } 990 991 if ($oLeftFilter->GetClass() != $oRightFilter->GetClass()) 992 { 993 if (MetaModel::IsParentClass($oLeftFilter->GetClass(), $oRightFilter->GetClass())) 994 { 995 // Specialize $oLeftFilter 996 $oLeftFilter->ChangeClass($oRightFilter->GetClass()); 997 } 998 elseif (MetaModel::IsParentClass($oRightFilter->GetClass(), $oLeftFilter->GetClass())) 999 { 1000 // Specialize $oRightFilter 1001 $oRightFilter->ChangeClass($oLeftFilter->GetClass()); 1002 } 1003 else 1004 { 1005 throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'"); 1006 } 1007 } 1008 1009 $aAliasTranslation = array(); 1010 $oLeftFilter->MergeWith_InNamespace($oRightFilter, $oLeftFilter->m_aClasses, $aAliasTranslation); 1011 $oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation); 1012 $aSearches[] = $oLeftFilter; 1013 } 1014 if (count($aSearches) == 1) 1015 { 1016 // return a DBObjectSearch 1017 return $aSearches[0]; 1018 } 1019 else 1020 { 1021 return new DBUnionSearch($aSearches); 1022 } 1023 } 1024 1025 protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation) 1026 { 1027 if ($this->GetClass() != $oFilter->GetClass()) 1028 { 1029 throw new CoreException("Attempting to merge a filter of class '{$this->GetClass()}' with a filter of class '{$oFilter->GetClass()}'"); 1030 } 1031 1032 // Translate search condition into our aliasing scheme 1033 $aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); 1034 1035 foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) 1036 { 1037 foreach($aPointingTo as $iOperatorCode => $aFilter) 1038 { 1039 foreach($aFilter as $oExtFilter) 1040 { 1041 $this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); 1042 } 1043 } 1044 } 1045 foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences) 1046 { 1047 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 1048 { 1049 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 1050 { 1051 foreach ($aFilters as $oForeignFilter) 1052 { 1053 $this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); 1054 } 1055 } 1056 } 1057 } 1058 } 1059 1060 public function GetCriteria() {return $this->m_oSearchCondition;} 1061 public function GetCriteria_FullText() {throw new Exception("Removed GetCriteria_FullText");} 1062 public function GetCriteria_PointingTo($sKeyAttCode = "") 1063 { 1064 if (empty($sKeyAttCode)) 1065 { 1066 return $this->m_aPointingTo; 1067 } 1068 if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array(); 1069 return $this->m_aPointingTo[$sKeyAttCode]; 1070 } 1071 protected function GetCriteria_ReferencedBy() 1072 { 1073 return $this->m_aReferencedBy; 1074 } 1075 1076 public function SetInternalParams($aParams) 1077 { 1078 return $this->m_aParams = $aParams; 1079 } 1080 1081 /** 1082 * @return array <strong>warning</strong> : array returned by value 1083 * @see self::GetInternalParamsByRef to get the attribute by reference 1084 */ 1085 public function GetInternalParams() 1086 { 1087 return $this->m_aParams; 1088 } 1089 1090 /** 1091 * @return array 1092 * @see http://php.net/manual/en/language.references.return.php 1093 * @since 2.5.1 N°1582 1094 */ 1095 public function &GetInternalParamsByRef() 1096 { 1097 return $this->m_aParams; 1098 } 1099 1100 /** 1101 * @param string $sKey 1102 * @param mixed $value 1103 * @param bool $bDoNotOverride 1104 * 1105 * @throws \CoreUnexpectedValue if $bDoNotOverride and $sKey already exists 1106 */ 1107 public function AddInternalParam($sKey, $value, $bDoNotOverride = false) 1108 { 1109 if ($bDoNotOverride) 1110 { 1111 if (array_key_exists($sKey, $this->m_aParams)) 1112 { 1113 throw new CoreUnexpectedValue("The key $sKey already exists with value : ".$this->m_aParams[$sKey]); 1114 } 1115 } 1116 1117 $this->m_aParams[$sKey] = $value; 1118 } 1119 1120 public function GetQueryParams($bExcludeMagicParams = true) 1121 { 1122 $aParams = array(); 1123 $this->m_oSearchCondition->Render($aParams, true); 1124 1125 if ($bExcludeMagicParams) 1126 { 1127 $aRet = array(); 1128 1129 // Make the list of acceptable arguments... could be factorized with run_query, into oSearch->GetQueryParams($bExclude magic params) 1130 $aNakedMagicArguments = array(); 1131 foreach (MetaModel::PrepareQueryArguments(array()) as $sArgName => $value) 1132 { 1133 $iPos = strpos($sArgName, '->object()'); 1134 if ($iPos === false) 1135 { 1136 $aNakedMagicArguments[$sArgName] = $value; 1137 } 1138 else 1139 { 1140 $aNakedMagicArguments[substr($sArgName, 0, $iPos)] = true; 1141 } 1142 } 1143 foreach ($aParams as $sParam => $foo) 1144 { 1145 $iPos = strpos($sParam, '->'); 1146 if ($iPos === false) 1147 { 1148 $sRefName = $sParam; 1149 } 1150 else 1151 { 1152 $sRefName = substr($sParam, 0, $iPos); 1153 } 1154 if (!array_key_exists($sRefName, $aNakedMagicArguments)) 1155 { 1156 $aRet[$sParam] = $foo; 1157 } 1158 } 1159 } 1160 1161 return $aRet; 1162 } 1163 1164 public function ListConstantFields() 1165 { 1166 return $this->m_oSearchCondition->ListConstantFields(); 1167 } 1168 1169 /** 1170 * Turn the parameters (:xxx) into scalar values in order to easily 1171 * serialize a search 1172 * @param $aArgs 1173*/ 1174 public function ApplyParameters($aArgs) 1175 { 1176 $this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs)); 1177 } 1178 1179 public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) 1180 { 1181 // Currently unused, but could be useful later 1182 $bRetrofitParams = false; 1183 1184 if ($bDevelopParams) 1185 { 1186 if (is_null($aContextParams)) 1187 { 1188 $aParams = array_merge($this->m_aParams); 1189 } 1190 else 1191 { 1192 $aParams = array_merge($aContextParams, $this->m_aParams); 1193 } 1194 $aParams = MetaModel::PrepareQueryArguments($aParams); 1195 } 1196 else 1197 { 1198 // Leave it as is, the rendering will be made with parameters in clear 1199 $aParams = null; 1200 } 1201 1202 $aSelectedAliases = array(); 1203 foreach ($this->m_aSelectedClasses as $sAlias => $sClass) 1204 { 1205 $aSelectedAliases[] = '`' . $sAlias . '`'; 1206 } 1207 $sSelectedClasses = implode(', ', $aSelectedAliases); 1208 $sRes = 'SELECT '.$sSelectedClasses.' FROM'; 1209 1210 $sRes .= ' ' . $this->GetFirstJoinedClass() . ' AS `' . $this->GetFirstJoinedClassAlias() . '`'; 1211 $sRes .= $this->ToOQL_Joins(); 1212 $sRes .= " WHERE ".$this->m_oSearchCondition->Render($aParams, $bRetrofitParams); 1213 1214 if ($bWithAllowAllFlag && $this->m_bAllowAllData) 1215 { 1216 $sRes .= " ALLOW ALL DATA"; 1217 } 1218 return $sRes; 1219 } 1220 1221 protected function OperatorCodeToOQL($iOperatorCode) 1222 { 1223 switch($iOperatorCode) 1224 { 1225 case TREE_OPERATOR_EQUALS: 1226 $sOperator = ' = '; 1227 break; 1228 1229 case TREE_OPERATOR_BELOW: 1230 $sOperator = ' BELOW '; 1231 break; 1232 1233 case TREE_OPERATOR_BELOW_STRICT: 1234 $sOperator = ' BELOW STRICT '; 1235 break; 1236 1237 case TREE_OPERATOR_NOT_BELOW: 1238 $sOperator = ' NOT BELOW '; 1239 break; 1240 1241 case TREE_OPERATOR_NOT_BELOW_STRICT: 1242 $sOperator = ' NOT BELOW STRICT '; 1243 break; 1244 1245 case TREE_OPERATOR_ABOVE: 1246 $sOperator = ' ABOVE '; 1247 break; 1248 1249 case TREE_OPERATOR_ABOVE_STRICT: 1250 $sOperator = ' ABOVE STRICT '; 1251 break; 1252 1253 case TREE_OPERATOR_NOT_ABOVE: 1254 $sOperator = ' NOT ABOVE '; 1255 break; 1256 1257 case TREE_OPERATOR_NOT_ABOVE_STRICT: 1258 $sOperator = ' NOT ABOVE STRICT '; 1259 break; 1260 1261 } 1262 return $sOperator; 1263 } 1264 1265 protected function ToOQL_Joins() 1266 { 1267 $sRes = ''; 1268 foreach($this->m_aPointingTo as $sExtKey => $aPointingTo) 1269 { 1270 foreach($aPointingTo as $iOperatorCode => $aFilter) 1271 { 1272 $sOperator = $this->OperatorCodeToOQL($iOperatorCode); 1273 foreach($aFilter as $oFilter) 1274 { 1275 $sRes .= ' JOIN ' . $oFilter->GetFirstJoinedClass() . ' AS `' . $oFilter->GetFirstJoinedClassAlias() . '` ON `' . $this->GetFirstJoinedClassAlias() . '`.' . $sExtKey . $sOperator . '`' . $oFilter->GetFirstJoinedClassAlias() . '`.id'; 1276 $sRes .= $oFilter->ToOQL_Joins(); 1277 } 1278 } 1279 } 1280 foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) 1281 { 1282 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 1283 { 1284 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 1285 { 1286 $sOperator = $this->OperatorCodeToOQL($iOperatorCode); 1287 foreach ($aFilters as $oForeignFilter) 1288 { 1289 $sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . $sOperator . '`' . $this->GetFirstJoinedClassAlias() . '`.id'; 1290 $sRes .= $oForeignFilter->ToOQL_Joins(); 1291 } 1292 } 1293 } 1294 } 1295 return $sRes; 1296 } 1297 1298 protected function OQLExpressionToCondition($sQuery, $oExpression, $aClassAliases) 1299 { 1300 if ($oExpression instanceof BinaryOqlExpression) 1301 { 1302 $sOperator = $oExpression->GetOperator(); 1303 $oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases); 1304 $oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases); 1305 return new BinaryExpression($oLeft, $sOperator, $oRight); 1306 } 1307 elseif ($oExpression instanceof MatchOqlExpression) 1308 { 1309 $oLeft = $this->OQLExpressionToCondition($sQuery, $oExpression->GetLeftExpr(), $aClassAliases); 1310 $oRight = $this->OQLExpressionToCondition($sQuery, $oExpression->GetRightExpr(), $aClassAliases); 1311 1312 return new MatchExpression($oLeft, $oRight); 1313 } 1314 elseif ($oExpression instanceof FieldOqlExpression) 1315 { 1316 $sClassAlias = $oExpression->GetParent(); 1317 $sFltCode = $oExpression->GetName(); 1318 if (empty($sClassAlias)) 1319 { 1320 // Need to find the right alias 1321 // Build an array of field => array of aliases 1322 $aFieldClasses = array(); 1323 foreach($aClassAliases as $sAlias => $sReal) 1324 { 1325 foreach(MetaModel::GetFiltersList($sReal) as $sAnFltCode) 1326 { 1327 $aFieldClasses[$sAnFltCode][] = $sAlias; 1328 } 1329 } 1330 $sClassAlias = $aFieldClasses[$sFltCode][0]; 1331 } 1332 return new FieldExpression($sFltCode, $sClassAlias); 1333 } 1334 elseif ($oExpression instanceof VariableOqlExpression) 1335 { 1336 return new VariableExpression($oExpression->GetName()); 1337 } 1338 elseif ($oExpression instanceof TrueOqlExpression) 1339 { 1340 return new TrueExpression; 1341 } 1342 elseif ($oExpression instanceof ScalarOqlExpression) 1343 { 1344 return new ScalarExpression($oExpression->GetValue()); 1345 } 1346 elseif ($oExpression instanceof ListOqlExpression) 1347 { 1348 $aItems = array(); 1349 foreach ($oExpression->GetItems() as $oItemExpression) 1350 { 1351 $aItems[] = $this->OQLExpressionToCondition($sQuery, $oItemExpression, $aClassAliases); 1352 } 1353 return new ListExpression($aItems); 1354 } 1355 elseif ($oExpression instanceof FunctionOqlExpression) 1356 { 1357 $aArgs = array(); 1358 foreach ($oExpression->GetArgs() as $oArgExpression) 1359 { 1360 $aArgs[] = $this->OQLExpressionToCondition($sQuery, $oArgExpression, $aClassAliases); 1361 } 1362 return new FunctionExpression($oExpression->GetVerb(), $aArgs); 1363 } 1364 elseif ($oExpression instanceof IntervalOqlExpression) 1365 { 1366 return new IntervalExpression($oExpression->GetValue(), $oExpression->GetUnit()); 1367 } 1368 else 1369 { 1370 throw new CoreException('Unknown expression type', array('class'=>get_class($oExpression), 'query'=>$sQuery)); 1371 } 1372 } 1373 1374 public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery) 1375 { 1376 $oModelReflection = new ModelReflectionRuntime(); 1377 $sClass = $oOqlQuery->GetClass($oModelReflection); 1378 $sClassAlias = $oOqlQuery->GetClassAlias(); 1379 1380 $aAliases = array($sClassAlias => $sClass); 1381 1382 // Note: the condition must be built here, it may be altered later on when optimizing some joins 1383 $oConditionTree = $oOqlQuery->GetCondition(); 1384 if ($oConditionTree instanceof Expression) 1385 { 1386 $aRawAliases = array($sClassAlias => $sClass); 1387 $aJoinSpecs = $oOqlQuery->GetJoins(); 1388 if (is_array($aJoinSpecs)) 1389 { 1390 foreach ($aJoinSpecs as $oJoinSpec) 1391 { 1392 $aRawAliases[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass(); 1393 } 1394 } 1395 $this->m_oSearchCondition = $this->OQLExpressionToCondition($sQuery, $oConditionTree, $aRawAliases); 1396 } 1397 1398 // Maintain an array of filters, because the flat list is in fact referring to a tree 1399 // And this will be an easy way to dispatch the conditions 1400 // $this will be referenced by the other filters, or the other way around... 1401 $aJoinItems = array($sClassAlias => $this); 1402 1403 $aJoinSpecs = $oOqlQuery->GetJoins(); 1404 if (is_array($aJoinSpecs)) 1405 { 1406 $aAliasTranslation = array(); 1407 foreach ($aJoinSpecs as $oJoinSpec) 1408 { 1409 $sJoinClass = $oJoinSpec->GetClass(); 1410 $sJoinClassAlias = $oJoinSpec->GetClassAlias(); 1411 if (isset($aAliasTranslation[$sJoinClassAlias]['*'])) 1412 { 1413 $sJoinClassAlias = $aAliasTranslation[$sJoinClassAlias]['*']; 1414 } 1415 1416 // Assumption: ext key on the left only !!! 1417 // normalization should take care of this 1418 $oLeftField = $oJoinSpec->GetLeftField(); 1419 $sFromClass = $oLeftField->GetParent(); 1420 if (isset($aAliasTranslation[$sFromClass]['*'])) 1421 { 1422 $sFromClass = $aAliasTranslation[$sFromClass]['*']; 1423 } 1424 $sExtKeyAttCode = $oLeftField->GetName(); 1425 1426 $oRightField = $oJoinSpec->GetRightField(); 1427 $sToClass = $oRightField->GetParent(); 1428 if (isset($aAliasTranslation[$sToClass]['*'])) 1429 { 1430 $sToClass = $aAliasTranslation[$sToClass]['*']; 1431 } 1432 1433 $aAliases[$sJoinClassAlias] = $sJoinClass; 1434 $aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias); 1435 1436 $sOperator = $oJoinSpec->GetOperator(); 1437 switch($sOperator) 1438 { 1439 case '=': 1440 default: 1441 $iOperatorCode = TREE_OPERATOR_EQUALS; 1442 break; 1443 case 'BELOW': 1444 $iOperatorCode = TREE_OPERATOR_BELOW; 1445 break; 1446 case 'BELOW_STRICT': 1447 $iOperatorCode = TREE_OPERATOR_BELOW_STRICT; 1448 break; 1449 case 'NOT_BELOW': 1450 $iOperatorCode = TREE_OPERATOR_NOT_BELOW; 1451 break; 1452 case 'NOT_BELOW_STRICT': 1453 $iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT; 1454 break; 1455 case 'ABOVE': 1456 $iOperatorCode = TREE_OPERATOR_ABOVE; 1457 break; 1458 case 'ABOVE_STRICT': 1459 $iOperatorCode = TREE_OPERATOR_ABOVE_STRICT; 1460 break; 1461 case 'NOT_ABOVE': 1462 $iOperatorCode = TREE_OPERATOR_NOT_ABOVE; 1463 break; 1464 case 'NOT_ABOVE_STRICT': 1465 $iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT; 1466 break; 1467 } 1468 1469 if ($sFromClass == $sJoinClassAlias) 1470 { 1471 $oReceiver = $aJoinItems[$sToClass]; 1472 $oNewComer = $aJoinItems[$sFromClass]; 1473 $oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); 1474 } 1475 else 1476 { 1477 $oReceiver = $aJoinItems[$sFromClass]; 1478 $oNewComer = $aJoinItems[$sToClass]; 1479 $oReceiver->AddCondition_PointingTo_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode); 1480 } 1481 } 1482 $this->m_oSearchCondition = $this->m_oSearchCondition->Translate($aAliasTranslation, false, false /* leave unresolved fields */); 1483 } 1484 1485 // Check and prepare the select information 1486 $this->m_aSelectedClasses = array(); 1487 foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails) 1488 { 1489 $sClassToSelect = $oClassDetails->GetValue(); 1490 $this->m_aSelectedClasses[$sClassToSelect] = $aAliases[$sClassToSelect]; 1491 } 1492 $this->m_aClasses = $aAliases; 1493 } 1494 1495 //////////////////////////////////////////////////////////////////////////// 1496 // 1497 // Construction of the SQL queries 1498 // 1499 //////////////////////////////////////////////////////////////////////////// 1500 1501 public function MakeDeleteQuery($aArgs = array()) 1502 { 1503 $aModifierProperties = MetaModel::MakeModifierProperties($this); 1504 $oBuild = new QueryBuilderContext($this, $aModifierProperties); 1505 $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => array()), array()); 1506 $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); 1507 $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); 1508 $oSQLQuery->OptimizeJoins(array()); 1509 $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); 1510 $sRet = $oSQLQuery->RenderDelete($aScalarArgs); 1511 return $sRet; 1512 } 1513 1514 public function MakeUpdateQuery($aValues, $aArgs = array()) 1515 { 1516 // $aValues is an array of $sAttCode => $value 1517 $aModifierProperties = MetaModel::MakeModifierProperties($this); 1518 $oBuild = new QueryBuilderContext($this, $aModifierProperties); 1519 $aRequested = array(); // Requested attributes are the updated attributes 1520 foreach ($aValues as $sAttCode => $value) 1521 { 1522 $aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode); 1523 } 1524 $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => $aRequested), $aValues); 1525 $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); 1526 $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); 1527 $oSQLQuery->OptimizeJoins(array()); 1528 $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); 1529 $sRet = $oSQLQuery->RenderUpdate($aScalarArgs); 1530 return $sRet; 1531 } 1532 1533 public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) 1534 { 1535 // Hide objects that are not visible to the current user 1536 // 1537 $oSearch = $this; 1538 if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered()) 1539 { 1540 $oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter')); 1541 if ($oVisibleObjects === false) 1542 { 1543 // Make sure this is a valid search object, saying NO for all 1544 $oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass()); 1545 } 1546 if (is_object($oVisibleObjects)) 1547 { 1548 $oVisibleObjects->AllowAllData(); 1549 $oSearch = $this->Intersect($oVisibleObjects); 1550 $oSearch->SetDataFiltered(); 1551 } 1552 } 1553 1554 // Compute query modifiers properties (can be set in the search itself, by the context, etc.) 1555 // 1556 $aModifierProperties = MetaModel::MakeModifierProperties($oSearch); 1557 1558 // Create a unique cache id 1559 // 1560 $aContextData = array(); 1561 $bCanCache = true; 1562 if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries) 1563 { 1564 if (isset($_SERVER['REQUEST_URI'])) 1565 { 1566 $aContextData['sRequestUri'] = $_SERVER['REQUEST_URI']; 1567 } 1568 else if (isset($_SERVER['SCRIPT_NAME'])) 1569 { 1570 $aContextData['sRequestUri'] = $_SERVER['SCRIPT_NAME']; 1571 } 1572 else 1573 { 1574 $aContextData['sRequestUri'] = ''; 1575 } 1576 1577 // Need to identify the query 1578 $sOqlQuery = $oSearch->ToOql(false, null, true); 1579 if ((strpos($sOqlQuery, '`id` IN (') !== false) || (strpos($sOqlQuery, '`id` NOT IN (') !== false)) 1580 { 1581 // Requests containing "id IN" are not worth caching 1582 $bCanCache = false; 1583 } 1584 1585 $aContextData['sOqlQuery'] = $sOqlQuery; 1586 1587 if (count($aModifierProperties)) 1588 { 1589 array_multisort($aModifierProperties); 1590 $sModifierProperties = json_encode($aModifierProperties); 1591 } 1592 else 1593 { 1594 $sModifierProperties = ''; 1595 } 1596 $aContextData['sModifierProperties'] = $sModifierProperties; 1597 1598 $sRawId = $sOqlQuery.$sModifierProperties; 1599 if (!is_null($aAttToLoad)) 1600 { 1601 $sRawId .= json_encode($aAttToLoad); 1602 } 1603 $aContextData['aAttToLoad'] = $aAttToLoad; 1604 if (!is_null($aGroupByExpr)) 1605 { 1606 foreach($aGroupByExpr as $sAlias => $oExpr) 1607 { 1608 $sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render(); 1609 } 1610 } 1611 if (!is_null($aSelectExpr)) 1612 { 1613 foreach($aSelectExpr as $sAlias => $oExpr) 1614 { 1615 $sRawId .= 'se:'.$sAlias.'!'.$oExpr->Render(); 1616 } 1617 } 1618 $aContextData['aGroupByExpr'] = $aGroupByExpr; 1619 $aContextData['aSelectExpr'] = $aSelectExpr; 1620 $sRawId .= $bGetCount; 1621 $aContextData['bGetCount'] = $bGetCount; 1622 if (is_array($aSelectedClasses)) 1623 { 1624 $sRawId .= implode(',', $aSelectedClasses); // Unions may alter the list of selected columns 1625 } 1626 $aContextData['aSelectedClasses'] = $aSelectedClasses; 1627 $bIsArchiveMode = $oSearch->GetArchiveMode(); 1628 $sRawId .= $bIsArchiveMode ? '--arch' : ''; 1629 $bShowObsoleteData = $oSearch->GetShowObsoleteData(); 1630 $sRawId .= $bShowObsoleteData ? '--obso' : ''; 1631 $aContextData['bIsArchiveMode'] = $bIsArchiveMode; 1632 $aContextData['bShowObsoleteData'] = $bShowObsoleteData; 1633 $sOqlId = md5($sRawId); 1634 } 1635 else 1636 { 1637 $sOqlQuery = "SELECTING... ".$oSearch->GetClass(); 1638 $sOqlId = "query id ? n/a"; 1639 } 1640 1641 1642 // Query caching 1643 // 1644 $sOqlAPCCacheId = null; 1645 if (self::$m_bQueryCacheEnabled) 1646 { 1647 // Warning: using directly the query string as the key to the hash array can FAIL if the string 1648 // is long and the differences are only near the end... so it's safer (but not bullet proof?) 1649 // to use a hash (like md5) of the string as the key ! 1650 // 1651 // Example of two queries that were found as similar by the hash array: 1652 // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2 1653 // and 1654 // SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2 1655 // the only difference is R instead or O at position 285 (TTR instead of TTO)... 1656 // 1657 if (array_key_exists($sOqlId, self::$m_aQueryStructCache)) 1658 { 1659 // hit! 1660 1661 $oSQLQuery = unserialize(serialize(self::$m_aQueryStructCache[$sOqlId])); 1662 // Note: cloning is not enough because the subtree is made of objects 1663 } 1664 elseif (self::$m_bUseAPCCache) 1665 { 1666 // Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter 1667 // 1668 $sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-query-cache-'.$sOqlId; 1669 $oKPI = new ExecutionKPI(); 1670 $result = apc_fetch($sOqlAPCCacheId); 1671 $oKPI->ComputeStats('Query APC (fetch)', $sOqlQuery); 1672 1673 if (is_object($result)) 1674 { 1675 $oSQLQuery = $result; 1676 self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery; 1677 } 1678 } 1679 } 1680 1681 if (!isset($oSQLQuery)) 1682 { 1683 $oKPI = new ExecutionKPI(); 1684 $oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); 1685 $oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery); 1686 1687 if (self::$m_bQueryCacheEnabled) 1688 { 1689 if ($bCanCache && self::$m_bUseAPCCache) 1690 { 1691 $oSQLQuery->m_aContextData = $aContextData; 1692 $oKPI = new ExecutionKPI(); 1693 apc_store($sOqlAPCCacheId, $oSQLQuery, self::$m_iQueryCacheTTL); 1694 $oKPI->ComputeStats('Query APC (store)', $sOqlQuery); 1695 } 1696 1697 self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery->DeepClone(); 1698 } 1699 } 1700 return $oSQLQuery; 1701 } 1702 1703 /** 1704 * @param array $aAttToLoad 1705 * @param bool $bGetCount 1706 * @param array $aModifierProperties 1707 * @param array $aGroupByExpr 1708 * @param array $aSelectedClasses 1709 * @param array $aSelectExpr 1710 * 1711 * @return null|SQLObjectQuery 1712 * @throws \CoreException 1713 */ 1714 protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) 1715 { 1716 $oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); 1717 1718 $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array()); 1719 $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); 1720 if (is_array($aGroupByExpr)) 1721 { 1722 $aCols = $oBuild->m_oQBExpressions->GetGroupBy(); 1723 $oSQLQuery->SetGroupBy($aCols); 1724 $oSQLQuery->SetSelect($aCols); 1725 } 1726 else 1727 { 1728 $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); 1729 } 1730 if ($aSelectExpr) 1731 { 1732 // Get the fields corresponding to the select expressions 1733 foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr) 1734 { 1735 if (key_exists($sAlias, $aSelectExpr)) 1736 { 1737 $oSQLQuery->AddSelect($sAlias, $oExpr); 1738 } 1739 } 1740 } 1741 1742 $aMandatoryTables = null; 1743 if (self::$m_bOptimizeQueries) 1744 { 1745 if ($bGetCount) 1746 { 1747 // Simplify the query if just getting the count 1748 $oSQLQuery->SetSelect(array()); 1749 } 1750 $oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables); 1751 $oSQLQuery->OptimizeJoins($aMandatoryTables); 1752 } 1753 // Filter tables as late as possible: do not interfere with the optimization process 1754 foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions) 1755 { 1756 if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables)) 1757 { 1758 foreach ($aConditions as $oCondition) 1759 { 1760 $oSQLQuery->AddCondition($oCondition); 1761 } 1762 } 1763 } 1764 1765 return $oSQLQuery; 1766 } 1767 1768 1769 /** 1770 * @param $oBuild 1771 * @param null $aAttToLoad 1772 * @param array $aValues 1773 * @return null|SQLObjectQuery 1774 * @throws \CoreException 1775 */ 1776 protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array()) 1777 { 1778 // Note: query class might be different than the class of the filter 1779 // -> this occurs when we are linking our class to an external class (referenced by, or pointing to) 1780 $sClass = $this->GetFirstJoinedClass(); 1781 $sClassAlias = $this->GetFirstJoinedClassAlias(); 1782 1783 $bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); 1784 1785 //self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); 1786 1787 //$sRootClass = MetaModel::GetRootClass($sClass); 1788 $sKeyField = MetaModel::DBGetKey($sClass); 1789 1790 if ($bIsOnQueriedClass) 1791 { 1792 // default to the whole list of attributes + the very std id/finalclass 1793 $oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias)); 1794 if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad)) 1795 { 1796 $sSelectedClass = $oBuild->GetSelectedClass($sClassAlias); 1797 $aAttList = MetaModel::ListAttributeDefs($sSelectedClass); 1798 } 1799 else 1800 { 1801 $aAttList = $aAttToLoad[$sClassAlias]; 1802 } 1803 foreach ($aAttList as $sAttCode => $oAttDef) 1804 { 1805 if (!$oAttDef->IsScalar()) continue; 1806 // keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue; 1807 1808 if ($oAttDef->IsBasedOnOQLExpression()) 1809 { 1810 $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias)); 1811 } 1812 else 1813 { 1814 foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) 1815 { 1816 $oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias)); 1817 } 1818 } 1819 } 1820 } 1821 //echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n"; 1822 $aExpectedAtts = array(); // array of (attcode => fieldexpression) 1823 //echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n"; 1824 $oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts); 1825 1826 // Compute a clear view of required joins (from the current class) 1827 // Build the list of external keys: 1828 // -> ext keys required by an explicit join 1829 // -> ext keys mentionned in a 'pointing to' condition 1830 // -> ext keys required for an external field 1831 // -> ext keys required for a friendly name 1832 // 1833 $aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef)) 1834 // 1835 // Optimization: could be partially computed once for all (cached) ? 1836 // 1837 1838 if ($bIsOnQueriedClass) 1839 { 1840 // Get all Ext keys for the queried class (??) 1841 foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode) 1842 { 1843 $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); 1844 $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); 1845 } 1846 } 1847 // Get all Ext keys used by the filter 1848 foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo) 1849 { 1850 if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) 1851 { 1852 $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); 1853 $aExtKeys[$sKeyTableClass][$sKeyAttCode] = array(); 1854 } 1855 } 1856 1857 $aFNJoinAlias = array(); // array of (subclass => alias) 1858 foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression) 1859 { 1860 if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue; 1861 $oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode); 1862 if ($oAttDef->IsBasedOnOQLExpression()) 1863 { 1864 // To optimize: detect a restriction on child classes in the condition expression 1865 // e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine') 1866 $oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode); 1867 1868 $aRequiredFields = array(); 1869 $oExpression->GetUnresolvedFields('', $aRequiredFields); 1870 $aTranslateFields = array(); 1871 foreach($aRequiredFields as $sSubClass => $aFields) 1872 { 1873 foreach($aFields as $sAttCode => $oField) 1874 { 1875 $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); 1876 if ($oAttDef->IsExternalKey()) 1877 { 1878 $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); 1879 $aExtKeys[$sClassOfAttribute][$sAttCode] = array(); 1880 } 1881 elseif ($oAttDef->IsExternalField()) 1882 { 1883 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 1884 $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); 1885 $aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef; 1886 } 1887 else 1888 { 1889 $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); 1890 } 1891 1892 if (MetaModel::IsParentClass($sClassOfAttribute, $sClass)) 1893 { 1894 // The attribute is part of the standard query 1895 // 1896 $sAliasForAttribute = $sClassAlias; 1897 } 1898 else 1899 { 1900 // The attribute will be available from an additional outer join 1901 // For each subclass (table) one single join is enough 1902 // 1903 if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias)) 1904 { 1905 $sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute); 1906 $aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute; 1907 } 1908 else 1909 { 1910 $sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute]; 1911 } 1912 } 1913 1914 $aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute); 1915 } 1916 } 1917 $oExpression = $oExpression->Translate($aTranslateFields, false); 1918 1919 $aTranslateNow = array(); 1920 $aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression; 1921 $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); 1922 } 1923 } 1924 1925 // Add the ext fields used in the select (eventually adds an external key) 1926 foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) 1927 { 1928 if ($oAttDef->IsExternalField()) 1929 { 1930 if (array_key_exists($sAttCode, $aExpectedAtts)) 1931 { 1932 // Add the external attribute 1933 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 1934 $sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode); 1935 $aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef; 1936 } 1937 } 1938 } 1939 1940 $bRootFirst = MetaModel::GetConfig()->Get('optimize_requests_for_join_count'); 1941 if ($bRootFirst) 1942 { 1943 // First query built from the root, adding all tables including the leaf 1944 // Before N.1065 we were joining from the leaf first, but this wasn't a good choice : 1945 // most of the time (obsolescence, friendlyname, ...) we want to get a root attribute ! 1946 // 1947 $oSelectBase = null; 1948 $aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true); 1949 $bIsClassStandaloneClass = (count($aClassHierarchy) == 1); 1950 foreach($aClassHierarchy as $sSomeClass) 1951 { 1952 if (!MetaModel::HasTable($sSomeClass)) 1953 { 1954 continue; 1955 } 1956 1957 self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()"); 1958 $oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues); 1959 if (is_null($oSelectBase)) 1960 { 1961 $oSelectBase = $oSelectParentTable; 1962 if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass))) 1963 { 1964 // As we're linking to root class first, we're adding a where clause on the finalClass attribute : 1965 // COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1) 1966 // If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried 1967 // So we still need to filter records to only those corresponding to the child classes ! 1968 // The coalesce is mandatory if we have a polymorphic query (left join) 1969 $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); 1970 $sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass); 1971 $oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias()); 1972 $oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); 1973 $oTrueExpression = new TrueExpression(); 1974 $aCoalesceAttr = array($oInExpression, $oTrueExpression); 1975 $oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr); 1976 1977 $oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction); 1978 } 1979 } 1980 else 1981 { 1982 $oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass)); 1983 } 1984 } 1985 } 1986 else 1987 { 1988 // First query built upon on the leaf (ie current) class 1989 // 1990 self::DbgTrace("Main (=leaf) class, call MakeSQLObjectQuerySingleTable()"); 1991 if (MetaModel::HasTable($sClass)) 1992 { 1993 $oSelectBase = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sClass, $aExtKeys, $aValues); 1994 } 1995 else 1996 { 1997 $oSelectBase = null; 1998 1999 // As the join will not filter on the expected classes, we have to specify it explicitely 2000 $sExpectedClasses = implode("', '", MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); 2001 $oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')"); 2002 $oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction); 2003 } 2004 2005 // Then we join the queries of the eventual parent classes (compound model) 2006 foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass) 2007 { 2008 if (!MetaModel::HasTable($sParentClass)) continue; 2009 2010 self::DbgTrace("Parent class: $sParentClass... let's call MakeSQLObjectQuerySingleTable()"); 2011 $oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sParentClass, $aExtKeys, $aValues); 2012 if (is_null($oSelectBase)) 2013 { 2014 $oSelectBase = $oSelectParentTable; 2015 } 2016 else 2017 { 2018 $oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sParentClass)); 2019 } 2020 } 2021 } 2022 2023 // Filter on objects referencing me 2024 // 2025 foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) 2026 { 2027 foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator) 2028 { 2029 foreach ($aFiltersByOperator as $iOperatorCode => $aFilters) 2030 { 2031 foreach ($aFilters as $oForeignFilter) 2032 { 2033 $oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode); 2034 2035 self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()"); 2036 //self::DbgTrace($oForeignFilter); 2037 //self::DbgTrace($oForeignFilter->ToOQL()); 2038 //self::DbgTrace($oSelectForeign); 2039 //self::DbgTrace($oSelectForeign->RenderSelect(array())); 2040 2041 $sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias(); 2042 $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias)); 2043 2044 if ($oForeignKeyAttDef instanceof AttributeObjectKey) 2045 { 2046 $sClassAttCode = $oForeignKeyAttDef->Get('class_attcode'); 2047 2048 // Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass') 2049 $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL)); 2050 $oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias); 2051 $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); 2052 $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); 2053 } 2054 2055 $oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); 2056 2057 $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); 2058 $sForeignKeyTable = $oJoinExpr->GetParent(); 2059 $sForeignKeyColumn = $oJoinExpr->GetName(); 2060 2061 if ($iOperatorCode == TREE_OPERATOR_EQUALS) 2062 { 2063 $oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable); 2064 } 2065 else 2066 { 2067 // Hierarchical key 2068 $KeyLeft = $oForeignKeyAttDef->GetSQLLeft(); 2069 $KeyRight = $oForeignKeyAttDef->GetSQLRight(); 2070 2071 $oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true); 2072 } 2073 } 2074 } 2075 } 2076 } 2077 2078 // Additional JOINS for Friendly names 2079 // 2080 foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias) 2081 { 2082 $oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias); 2083 $oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array()); 2084 $oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass)); 2085 } 2086 2087 // That's all... cross fingers and we'll get some working query 2088 2089 //MyHelpers::var_dump_html($oSelectBase, true); 2090 //MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true); 2091 if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml(); 2092 return $oSelectBase; 2093 } 2094 2095 protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues) 2096 { 2097 // $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields)) 2098 2099 // Prepare the query for a single table (compound objects) 2100 // Ignores the items (attributes/filters) that are not on the target table 2101 // Perform an (inner or left) join for every external key (and specify the expected fields) 2102 // 2103 // Returns an SQLQuery 2104 // 2105 $sTargetClass = $this->GetFirstJoinedClass(); 2106 $sTargetAlias = $this->GetFirstJoinedClassAlias(); 2107 $sTable = MetaModel::DBGetTable($sTableClass); 2108 $sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable); 2109 2110 $aTranslation = array(); 2111 $aExpectedAtts = array(); 2112 $oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts); 2113 2114 $bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses()); 2115 2116 self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY")); 2117 2118 // 1 - SELECT and UPDATE 2119 // 2120 // Note: no need for any values nor fields for foreign Classes (ie not the queried Class) 2121 // 2122 $aUpdateValues = array(); 2123 2124 2125 // 1/a - Get the key and friendly name 2126 // 2127 // We need one pkey to be the key, let's take the first one available 2128 $oSelectedIdField = null; 2129 $oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias); 2130 $aTranslation[$sTargetAlias]['id'] = $oIdField; 2131 2132 if ($bIsOnQueriedClass) 2133 { 2134 // Add this field to the list of queried fields (required for the COUNT to work fine) 2135 $oSelectedIdField = $oIdField; 2136 } 2137 2138 // 1/b - Get the other attributes 2139 // 2140 foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) 2141 { 2142 // Skip this attribute if not defined in this table 2143 if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; 2144 2145 // Skip this attribute if not made of SQL columns 2146 if (count($oAttDef->GetSQLExpressions()) == 0) continue; 2147 2148 // Update... 2149 // 2150 if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues)) 2151 { 2152 assert ($oAttDef->IsBasedOnDBColumns()); 2153 foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue) 2154 { 2155 $aUpdateValues[$sColumn] = $sValue; 2156 } 2157 } 2158 } 2159 2160 // 2 - The SQL query, for this table only 2161 // 2162 $oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField); 2163 2164 // 3 - Resolve expected expressions (translation table: alias.attcode => table.column) 2165 // 2166 foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef) 2167 { 2168 // Skip this attribute if not defined in this table 2169 if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue; 2170 2171 // Select... 2172 // 2173 if ($oAttDef->IsExternalField()) 2174 { 2175 // skip, this will be handled in the joined tables (done hereabove) 2176 } 2177 else 2178 { 2179 // standard field, or external key 2180 // add it to the output 2181 foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) 2182 { 2183 if (array_key_exists($sAttCode.$sColId, $aExpectedAtts)) 2184 { 2185 $oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias); 2186 foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier) 2187 { 2188 $oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase); 2189 } 2190 $aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp; 2191 } 2192 } 2193 } 2194 } 2195 2196 // 4 - The external keys -> joins... 2197 // 2198 $aAllPointingTo = $this->GetCriteria_PointingTo(); 2199 2200 if (array_key_exists($sTableClass, $aExtKeys)) 2201 { 2202 foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields) 2203 { 2204 $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); 2205 2206 $aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode); 2207 if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo)) 2208 { 2209 // The join was not explicitely defined in the filter, 2210 // we need to do it now 2211 $sKeyClass = $oKeyAttDef->GetTargetClass(); 2212 $sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass); 2213 $oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias); 2214 2215 $aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter; 2216 } 2217 } 2218 } 2219 2220 foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo) 2221 { 2222 foreach($aPointingTo as $iOperatorCode => $aFilter) 2223 { 2224 foreach($aFilter as $oExtFilter) 2225 { 2226 if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it 2227 // The aliases should not conflict because normalization occured while building the filter 2228 $oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode); 2229 $sKeyClass = $oExtFilter->GetFirstJoinedClass(); 2230 $sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias(); 2231 2232 // Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree 2233 2234 if ($iOperatorCode == TREE_OPERATOR_EQUALS) 2235 { 2236 if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass])) 2237 { 2238 // Specify expected attributes for the target class query 2239 // ... and use the current alias ! 2240 $aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...) 2241 foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt) 2242 { 2243 $oExtAttDef = $oAtt->GetExtAttDef(); 2244 if ($oExtAttDef->IsBasedOnOQLExpression()) 2245 { 2246 $aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias); 2247 } 2248 else 2249 { 2250 $sExtAttCode = $oAtt->GetExtAttCode(); 2251 // Translate mainclass.extfield => remoteclassalias.remotefieldcode 2252 $oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode); 2253 foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr) 2254 { 2255 $aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias); 2256 } 2257 } 2258 } 2259 2260 if ($oKeyAttDef instanceof AttributeObjectKey) 2261 { 2262 // Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass') 2263 $sClassAttCode = $oKeyAttDef->Get('class_attcode'); 2264 $oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode); 2265 foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr) 2266 { 2267 $aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias); 2268 } 2269 2270 $oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL)); 2271 $oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias); 2272 $oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); 2273 $oBuild->m_oQBExpressions->AddCondition($oClassRestriction); 2274 } 2275 2276 // Translate prior to recursing 2277 // 2278 $oBuild->m_oQBExpressions->Translate($aTranslateNow, false); 2279 2280 self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()"); 2281 $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias)); 2282 2283 $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); 2284 2285 $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); 2286 $sExternalKeyTable = $oJoinExpr->GetParent(); 2287 $sExternalKeyField = $oJoinExpr->GetName(); 2288 2289 $aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc()) 2290 $sLocalKeyField = current($aCols); // get the first column for an external key 2291 2292 self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField"); 2293 if ($oKeyAttDef->IsNullAllowed()) 2294 { 2295 $oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField); 2296 } 2297 else 2298 { 2299 $oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable); 2300 } 2301 } 2302 } 2303 elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass) 2304 { 2305 $oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias)); 2306 $oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad); 2307 $oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField(); 2308 $sExternalKeyTable = $oJoinExpr->GetParent(); 2309 $sExternalKeyField = $oJoinExpr->GetName(); 2310 $sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft() 2311 $sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight() 2312 2313 $LocalKeyLeft = $oKeyAttDef->GetSQLLeft(); 2314 $LocalKeyRight = $oKeyAttDef->GetSQLRight(); 2315 2316 $oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode); 2317 } 2318 } 2319 } 2320 } 2321 2322 // Translate the selected columns 2323 // 2324 $oBuild->m_oQBExpressions->Translate($aTranslation, false); 2325 2326 // Filter out archived records 2327 // 2328 if (MetaModel::IsArchivable($sTableClass)) 2329 { 2330 if (!$oBuild->GetRootFilter()->GetArchiveMode()) 2331 { 2332 $bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses()); 2333 if ($bIsOnJoinedClass) 2334 { 2335 if (MetaModel::IsParentClass($sTableClass, $sTargetClass)) 2336 { 2337 $oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0)); 2338 $oBuild->AddFilteredTable($sTableAlias, $oNotArchived); 2339 } 2340 } 2341 } 2342 } 2343 return $oSelectBase; 2344 } 2345 2346 /** 2347 * Get the expression for the class and its subclasses (if finalclass = 'subclass' ...) 2348 * Simplifies the final expression by grouping classes having the same expression 2349 * @param $sClass 2350 * @param $sAttCode 2351 * @return \FunctionExpression|mixed|null 2352 * @throws \CoreException 2353*/ 2354 static public function GetPolymorphicExpression($sClass, $sAttCode) 2355 { 2356 $oExpression = ExpressionCache::GetCachedExpression($sClass, $sAttCode); 2357 if (!empty($oExpression)) 2358 { 2359 return $oExpression; 2360 } 2361 2362 // 1st step - get all of the required expressions (instantiable classes) 2363 // and group them using their OQL representation 2364 // 2365 $aExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes) 2366 foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass) 2367 { 2368 if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue; 2369 2370 $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); 2371 $oSubClassExp = $oAttDef->GetOQLExpression($sSubClass); 2372 2373 // 3rd step - position the attributes in the hierarchy of classes 2374 // 2375 $oSubClassExp->Browse(function($oNode) use ($sSubClass) { 2376 if ($oNode instanceof FieldExpression) 2377 { 2378 $sAttCode = $oNode->GetName(); 2379 $oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode); 2380 if ($oAttDef->IsExternalField()) 2381 { 2382 $sKeyAttCode = $oAttDef->GetKeyAttCode(); 2383 $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode); 2384 } 2385 else 2386 { 2387 $sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode); 2388 } 2389 $sParent = MetaModel::GetAttributeOrigin($sClassOfAttribute, $oNode->GetName()); 2390 $oNode->SetParent($sParent); 2391 } 2392 }); 2393 2394 $sSignature = $oSubClassExp->Render(); 2395 if (!array_key_exists($sSignature, $aExpressions)) 2396 { 2397 $aExpressions[$sSignature] = array( 2398 'expression' => $oSubClassExp, 2399 'classes' => array(), 2400 ); 2401 } 2402 $aExpressions[$sSignature]['classes'][] = $sSubClass; 2403 } 2404 2405 // 2nd step - build the final name expression depending on the finalclass 2406 // 2407 if (count($aExpressions) == 1) 2408 { 2409 $aExpData = reset($aExpressions); 2410 $oExpression = $aExpData['expression']; 2411 } 2412 else 2413 { 2414 $oExpression = null; 2415 foreach ($aExpressions as $sSignature => $aExpData) 2416 { 2417 $oClassListExpr = ListExpression::FromScalars($aExpData['classes']); 2418 $oClassExpr = new FieldExpression('finalclass', $sClass); 2419 $oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr); 2420 2421 if (is_null($oExpression)) 2422 { 2423 $oExpression = $aExpData['expression']; 2424 } 2425 else 2426 { 2427 $oExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oExpression)); 2428 } 2429 } 2430 } 2431 return $oExpression; 2432 } 2433} 2434