dbobjectset.class.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. <?php
  2. // Copyright (C) 2010-2014 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. * Object set management
  20. *
  21. * @copyright Copyright (C) 2010-2012 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. /**
  25. * A set of persistent objects, could be heterogeneous as long as the objects in the set have a common ancestor class
  26. *
  27. * @package iTopORM
  28. */
  29. class DBObjectSet
  30. {
  31. protected $m_aAddedIds; // Ids of objects added (discrete lists)
  32. protected $m_aAddedObjects;
  33. protected $m_aArgs;
  34. protected $m_aAttToLoad;
  35. protected $m_aOrderBy;
  36. public $m_bLoaded;
  37. protected $m_iNumTotalDBRows;
  38. protected $m_iNumLoadedDBRows;
  39. protected $m_iCurrRow;
  40. protected $m_oFilter;
  41. protected $m_oSQLResult;
  42. /**
  43. * Create a new set based on a Search definition.
  44. *
  45. * @param DBObjectSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported)
  46. * @param hash $aOrderBy
  47. * @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value
  48. * @param hash $aExtendedDataSpec
  49. * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count)
  50. * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count)
  51. */
  52. public function __construct(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0)
  53. {
  54. $this->m_oFilter = $oFilter->DeepClone();
  55. $this->m_aAddedIds = array();
  56. $this->m_aOrderBy = $aOrderBy;
  57. $this->m_aArgs = $aArgs;
  58. $this->m_aAttToLoad = null;
  59. $this->m_aExtendedDataSpec = $aExtendedDataSpec;
  60. $this->m_iLimitCount = $iLimitCount;
  61. $this->m_iLimitStart = $iLimitStart;
  62. $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet
  63. $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default
  64. $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
  65. $this->m_aAddedObjects = array(); // array of (row => array of (classalias) => object/null) storing the objects added "in memory"
  66. $this->m_iCurrRow = 0;
  67. $this->m_oSQLResult = null;
  68. }
  69. public function __destruct()
  70. {
  71. if (is_object($this->m_oSQLResult))
  72. {
  73. $this->m_oSQLResult->free();
  74. }
  75. }
  76. public function __toString()
  77. {
  78. $sRet = '';
  79. $this->Rewind();
  80. $sRet .= "Set (".$this->m_oFilter->ToOQL().")<br/>\n";
  81. $sRet .= "Query: <pre style=\"font-size: smaller; display:inline;\">".MetaModel::MakeSelectQuery($this->m_oFilter, array()).")</pre>\n";
  82. $sRet .= $this->Count()." records<br/>\n";
  83. if ($this->Count() > 0)
  84. {
  85. $sRet .= "<ul class=\"treeview\">\n";
  86. while ($oObj = $this->Fetch())
  87. {
  88. $sRet .= "<li>".$oObj->__toString()."</li>\n";
  89. }
  90. $sRet .= "</ul>\n";
  91. }
  92. return $sRet;
  93. }
  94. public function __clone()
  95. {
  96. $this->m_oFilter = $this->m_oFilter->DeepClone();
  97. $this->m_iNumTotalDBRows = null; // Total number of rows for the query without LIMIT. null if unknown yet
  98. $this->m_iNumLoadedDBRows = 0; // Total number of rows LOADED in $this->m_oSQLResult via a SQL query. 0 by default
  99. $this->m_bLoaded = false; // true when the filter has been used OR the set is built step by step (AddObject...)
  100. $this->m_iCurrRow = 0;
  101. $this->m_oSQLResult = null;
  102. }
  103. /**
  104. * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB
  105. *
  106. * @param hash $aAttToLoad Format: alias => array of attribute_codes
  107. *
  108. * @return void
  109. */
  110. public function OptimizeColumnLoad($aAttToLoad)
  111. {
  112. if (is_null($aAttToLoad))
  113. {
  114. $this->m_aAttToLoad = null;
  115. }
  116. else
  117. {
  118. // Complete the attribute list with the attribute codes
  119. $aAttToLoadWithAttDef = array();
  120. foreach($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
  121. {
  122. $aAttToLoadWithAttDef[$sClassAlias] = array();
  123. if (array_key_exists($sClassAlias, $aAttToLoad))
  124. {
  125. $aAttList = $aAttToLoad[$sClassAlias];
  126. foreach($aAttList as $sAttToLoad)
  127. {
  128. $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad);
  129. $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad] = $oAttDef;
  130. if ($oAttDef->IsExternalKey())
  131. {
  132. // Add the external key friendly name anytime
  133. $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, $sAttToLoad.'_friendlyname');
  134. $aAttToLoadWithAttDef[$sClassAlias][$sAttToLoad.'_friendlyname'] = $oFriendlyNameAttDef;
  135. }
  136. }
  137. }
  138. // Add the friendly name anytime
  139. $oFriendlyNameAttDef = MetaModel::GetAttributeDef($sClass, 'friendlyname');
  140. $aAttToLoadWithAttDef[$sClassAlias]['friendlyname'] = $oFriendlyNameAttDef;
  141. // Make sure that the final class is requested anytime, whatever the specification (needed for object construction!)
  142. if (!MetaModel::IsStandaloneClass($sClass) && !array_key_exists('finalclass', $aAttToLoadWithAttDef[$sClassAlias]))
  143. {
  144. $aAttToLoadWithAttDef[$sClassAlias]['finalclass'] = MetaModel::GetAttributeDef($sClass, 'finalclass');
  145. }
  146. }
  147. $this->m_aAttToLoad = $aAttToLoadWithAttDef;
  148. }
  149. }
  150. /**
  151. * Create a set (in-memory) containing just the given object
  152. *
  153. * @param DBobject $oObject
  154. *
  155. * @return DBObjectSet The singleton set
  156. */
  157. static public function FromObject($oObject)
  158. {
  159. $oRetSet = self::FromScratch(get_class($oObject));
  160. $oRetSet->AddObject($oObject);
  161. return $oRetSet;
  162. }
  163. /**
  164. * Create an empty set (in-memory), for the given class (and its subclasses) of objects
  165. *
  166. * @param string $sClass The class (or an ancestor) for the objects to be added in this set
  167. *
  168. * @return DBObject The empty set
  169. */
  170. static public function FromScratch($sClass)
  171. {
  172. $oFilter = new DBObjectSearch($sClass);
  173. $oFilter->AddConditionExpression(new FalseExpression());
  174. $oRetSet = new self($oFilter);
  175. $oRetSet->m_bLoaded = true; // no DB load
  176. $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB
  177. return $oRetSet;
  178. }
  179. /**
  180. * Create a set (in-memory) with just one column (i.e. one object per row) and filled with the given array of objects
  181. *
  182. * @param string $sClass The class of the objects (must be a common ancestor to all objects in the set)
  183. * @param array $aObjects The list of objects to add into the set
  184. *
  185. * @return DBObjectSet
  186. */
  187. static public function FromArray($sClass, $aObjects)
  188. {
  189. $oRetSet = self::FromScratch($sClass);
  190. $oRetSet->AddObjectArray($aObjects, $sClass);
  191. return $oRetSet;
  192. }
  193. /**
  194. * Create a set in-memory with several classes of objects per row (with one alias per "column")
  195. *
  196. * Limitation:
  197. * The filter/OQL query representing such a set can not be rebuilt (only the first column will be taken into account)
  198. *
  199. * @param hash $aClasses Format: array of (alias => class)
  200. * @param hash $aObjects Format: array of (array of (classalias => object))
  201. *
  202. * @return DBObjectSet
  203. */
  204. static public function FromArrayAssoc($aClasses, $aObjects)
  205. {
  206. // In a perfect world, we should create a complete tree of DBObjectSearch,
  207. // but as we lack most of the information related to the objects,
  208. // let's create one search definition corresponding only to the first column
  209. $sClass = reset($aClasses);
  210. $sAlias = key($aClasses);
  211. $oFilter = new CMDBSearchFilter($sClass, $sAlias);
  212. $oRetSet = new self($oFilter);
  213. $oRetSet->m_bLoaded = true; // no DB load
  214. $oRetSet->m_iNumTotalDBRows = 0; // Nothing from the DB
  215. foreach($aObjects as $rowIndex => $aObjectsByClassAlias)
  216. {
  217. $oRetSet->AddObjectExtended($aObjectsByClassAlias);
  218. }
  219. return $oRetSet;
  220. }
  221. static public function FromLinkSet($oObject, $sLinkSetAttCode, $sExtKeyToRemote)
  222. {
  223. $oLinkAttCode = MetaModel::GetAttributeDef(get_class($oObject), $sLinkSetAttCode);
  224. $oExtKeyAttDef = MetaModel::GetAttributeDef($oLinkAttCode->GetLinkedClass(), $sExtKeyToRemote);
  225. $sTargetClass = $oExtKeyAttDef->GetTargetClass();
  226. $oLinkSet = $oObject->Get($sLinkSetAttCode);
  227. $aTargets = array();
  228. while ($oLink = $oLinkSet->Fetch())
  229. {
  230. $aTargets[] = MetaModel::GetObject($sTargetClass, $oLink->Get($sExtKeyToRemote));
  231. }
  232. return self::FromArray($sTargetClass, $aTargets);
  233. }
  234. public function ToArray($bWithId = true)
  235. {
  236. $aRet = array();
  237. $this->Rewind();
  238. while ($oObject = $this->Fetch())
  239. {
  240. if ($bWithId)
  241. {
  242. $aRet[$oObject->GetKey()] = $oObject;
  243. }
  244. else
  245. {
  246. $aRet[] = $oObject;
  247. }
  248. }
  249. return $aRet;
  250. }
  251. public function ToArrayOfValues()
  252. {
  253. if (!$this->m_bLoaded) $this->Load();
  254. $this->Rewind();
  255. $aSelectedClasses = $this->m_oFilter->GetSelectedClasses();
  256. $aRet = array();
  257. $iRow = 0;
  258. while($aObjects = $this->FetchAssoc())
  259. {
  260. foreach($aObjects as $sClassAlias => $oObject)
  261. {
  262. if (is_null($oObject))
  263. {
  264. $aRet[$iRow][$sClassAlias.'.'.'id'] = null;
  265. }
  266. else
  267. {
  268. $aRet[$iRow][$sClassAlias.'.'.'id'] = $oObject->GetKey();
  269. }
  270. if (is_null($oObject))
  271. {
  272. $sClass = $aSelectedClasses[$sClassAlias];
  273. }
  274. else
  275. {
  276. $sClass = get_class($oObject);
  277. }
  278. foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
  279. {
  280. if ($oAttDef->IsScalar())
  281. {
  282. $sAttName = $sClassAlias.'.'.$sAttCode;
  283. if (is_null($oObject))
  284. {
  285. $aRet[$iRow][$sAttName] = null;
  286. }
  287. else
  288. {
  289. $aRet[$iRow][$sAttName] = $oObject->Get($sAttCode);
  290. }
  291. }
  292. }
  293. }
  294. $iRow++;
  295. }
  296. return $aRet;
  297. }
  298. public function GetColumnAsArray($sAttCode, $bWithId = true)
  299. {
  300. $aRet = array();
  301. $this->Rewind();
  302. while ($oObject = $this->Fetch())
  303. {
  304. if ($bWithId)
  305. {
  306. $aRet[$oObject->GetKey()] = $oObject->Get($sAttCode);
  307. }
  308. else
  309. {
  310. $aRet[] = $oObject->Get($sAttCode);
  311. }
  312. }
  313. return $aRet;
  314. }
  315. /**
  316. * Retrieve the DBObjectSearch corresponding to the objects present in this set
  317. *
  318. * Limitation:
  319. * This method will NOT work for sets with several columns (i.e. several objects per row)
  320. *
  321. * @return DBObjectSearch
  322. */
  323. public function GetFilter()
  324. {
  325. // Make sure that we carry on the parameters of the set with the filter
  326. $oFilter = $this->m_oFilter->DeepClone();
  327. // Note: the arguments found within a set can be object (but not in a filter)
  328. // That's why PrepareQueryArguments must be invoked there
  329. $oFilter->SetInternalParams(array_merge($oFilter->GetInternalParams(), MetaModel::PrepareQueryArguments($this->m_aArgs)));
  330. if (count($this->m_aAddedIds) == 0)
  331. {
  332. return $oFilter;
  333. }
  334. else
  335. {
  336. $oIdListExpr = ListExpression::FromScalars(array_keys($this->m_aAddedIds));
  337. $oIdExpr = new FieldExpression('id', $oFilter->GetClassAlias());
  338. $oIdInList = new BinaryExpression($oIdExpr, 'IN', $oIdListExpr);
  339. $oFilter->MergeConditionExpression($oIdInList);
  340. return $oFilter;
  341. }
  342. }
  343. /**
  344. * The (common ancestor) class of the objects in the first column of this set
  345. *
  346. * @return string The class of the objects in the first column
  347. */
  348. public function GetClass()
  349. {
  350. return $this->m_oFilter->GetClass();
  351. }
  352. /**
  353. * The alias for the class of the objects in the first column of this set
  354. *
  355. * @return string The alias of the class in the first column
  356. */
  357. public function GetClassAlias()
  358. {
  359. return $this->m_oFilter->GetClassAlias();
  360. }
  361. /**
  362. * The list of all classes (one per column) which are part of this set
  363. *
  364. * @return hash Format: alias => class
  365. */
  366. public function GetSelectedClasses()
  367. {
  368. return $this->m_oFilter->GetSelectedClasses();
  369. }
  370. /**
  371. * The root class (i.e. highest ancestor in the MeaModel class hierarchy) for the first column on this set
  372. *
  373. * @return string The root class for the objects in the first column of the set
  374. */
  375. public function GetRootClass()
  376. {
  377. return MetaModel::GetRootClass($this->GetClass());
  378. }
  379. /**
  380. * The arguments used for building this set
  381. *
  382. * @return hash Format: parameter_name => value
  383. */
  384. public function GetArgs()
  385. {
  386. return $this->m_aArgs;
  387. }
  388. /**
  389. * Sets the limits for loading the rows from the DB. Equivalent to MySQL's LIMIT start,count clause.
  390. * @param int $iLimitCount The number of rows to load
  391. * @param int $iLimitStart The index of the first row to load
  392. */
  393. public function SetLimit($iLimitCount, $iLimitStart = 0)
  394. {
  395. $this->m_iLimitCount = $iLimitCount;
  396. $this->m_iLimitStart = $iLimitStart;
  397. }
  398. /**
  399. * Sets the sort order for loading the rows from the DB. Changing the order by causes a Reload.
  400. *
  401. * @param hash $aOrderBy Format: field_code => boolean (true = ascending, false = descending)
  402. */
  403. public function SetOrderBy($aOrderBy)
  404. {
  405. if ($this->m_aOrderBy != $aOrderBy)
  406. {
  407. $this->m_aOrderBy = $aOrderBy;
  408. if ($this->m_bLoaded)
  409. {
  410. $this->m_bLoaded = false;
  411. $this->Load();
  412. }
  413. }
  414. }
  415. /**
  416. * Returns the 'count' limit for loading the rows from the DB
  417. *
  418. * @return int
  419. */
  420. public function GetLimitCount()
  421. {
  422. return $this->m_iLimitCount;
  423. }
  424. /**
  425. * Returns the 'start' limit for loading the rows from the DB
  426. *
  427. * @return int
  428. */
  429. public function GetLimitStart()
  430. {
  431. return $this->m_iLimitStart;
  432. }
  433. /**
  434. * Get the sort order used for loading this set from the database
  435. *
  436. * Limitation: the sort order has no effect on objects added in-memory
  437. *
  438. * @return hash Format: field_code => boolean (true = ascending, false = descending)
  439. */
  440. public function GetRealSortOrder()
  441. {
  442. // Get the class default sort order if not specified with the API
  443. //
  444. if (empty($this->m_aOrderBy))
  445. {
  446. return MetaModel::GetOrderByDefault($this->m_oFilter->GetClass());
  447. }
  448. else
  449. {
  450. return $this->m_aOrderBy;
  451. }
  452. }
  453. /**
  454. * Loads the set from the database. Actually performs the SQL query to retrieve the records from the DB.
  455. */
  456. public function Load()
  457. {
  458. if ($this->m_bLoaded) return;
  459. // Note: it is mandatory to set this value now, to protect against reentrance
  460. $this->m_bLoaded = true;
  461. if ($this->m_iLimitCount > 0)
  462. {
  463. $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec, $this->m_iLimitCount, $this->m_iLimitStart);
  464. }
  465. else
  466. {
  467. $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
  468. }
  469. if (is_object($this->m_oSQLResult))
  470. {
  471. // Free previous resultset if any
  472. $this->m_oSQLResult->free();
  473. $this->m_oSQLResult = null;
  474. }
  475. $this->m_iNumTotalDBRows = null;
  476. $this->m_oSQLResult = CMDBSource::Query($sSQL);
  477. if ($this->m_oSQLResult === false) return;
  478. if (($this->m_iLimitCount == 0) && ($this->m_iLimitStart == 0))
  479. {
  480. $this->m_iNumTotalDBRows = $this->m_oSQLResult->num_rows;
  481. }
  482. $this->m_iNumLoadedDBRows = $this->m_oSQLResult->num_rows;
  483. }
  484. /**
  485. * The total number of rows in this set. Independently of the SetLimit used for loading the set and taking into account the rows added in-memory.
  486. *
  487. * May actually perform the SQL query SELECT COUNT... if the set was not previously loaded, or loaded with a SetLimit
  488. *
  489. * @return int The total number of rows for this set.
  490. */
  491. public function Count()
  492. {
  493. if (is_null($this->m_iNumTotalDBRows))
  494. {
  495. $sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, array(), $this->m_aArgs, null, null, 0, 0, true);
  496. $resQuery = CMDBSource::Query($sSQL);
  497. if (!$resQuery) return 0;
  498. $aRow = CMDBSource::FetchArray($resQuery);
  499. CMDBSource::FreeResult($resQuery);
  500. $this->m_iNumTotalDBRows = $aRow['COUNT'];
  501. }
  502. return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ??
  503. }
  504. /**
  505. * Number of rows available in memory (loaded from DB + added in memory)
  506. *
  507. * @return number The number of rows available for Fetch'ing
  508. */
  509. protected function CountLoaded()
  510. {
  511. return $this->m_iNumLoadedDBRows + count($this->m_aAddedObjects);
  512. }
  513. /**
  514. * Fetch the object (with the given class alias) at the current position in the set and move the cursor to the next position.
  515. *
  516. * @param string $sRequestedClassAlias The class alias to fetch (if there are several objects/classes per row)
  517. * @return DBObject The fetched object or null when at the end
  518. */
  519. public function Fetch($sRequestedClassAlias = '')
  520. {
  521. if (!$this->m_bLoaded) $this->Load();
  522. if ($this->m_iCurrRow >= $this->CountLoaded())
  523. {
  524. return null;
  525. }
  526. if (strlen($sRequestedClassAlias) == 0)
  527. {
  528. $sRequestedClassAlias = $this->m_oFilter->GetClassAlias();
  529. }
  530. if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
  531. {
  532. // Pick the row from the database
  533. $aRow = CMDBSource::FetchArray($this->m_oSQLResult);
  534. foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
  535. {
  536. if ($sRequestedClassAlias == $sClassAlias)
  537. {
  538. if (is_null($aRow[$sClassAlias.'id']))
  539. {
  540. $oRetObj = null;
  541. }
  542. else
  543. {
  544. $oRetObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
  545. }
  546. break;
  547. }
  548. }
  549. }
  550. else
  551. {
  552. // Pick the row from the objects added *in memory*
  553. $oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias];
  554. }
  555. $this->m_iCurrRow++;
  556. return $oRetObj;
  557. }
  558. /**
  559. * Fetch the whole row of objects (if several classes have been specified in the query) and move the cursor to the next position
  560. *
  561. * @return hash A hash with the format 'classAlias' => $oObj representing the current row of the set. Returns null when at the end.
  562. */
  563. public function FetchAssoc()
  564. {
  565. if (!$this->m_bLoaded) $this->Load();
  566. if ($this->m_iCurrRow >= $this->CountLoaded())
  567. {
  568. return null;
  569. }
  570. if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
  571. {
  572. // Pick the row from the database
  573. $aRow = CMDBSource::FetchArray($this->m_oSQLResult);
  574. $aRetObjects = array();
  575. foreach ($this->m_oFilter->GetSelectedClasses() as $sClassAlias => $sClass)
  576. {
  577. if (is_null($aRow[$sClassAlias.'id']))
  578. {
  579. $oObj = null;
  580. }
  581. else
  582. {
  583. $oObj = MetaModel::GetObjectByRow($sClass, $aRow, $sClassAlias, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
  584. }
  585. $aRetObjects[$sClassAlias] = $oObj;
  586. }
  587. }
  588. else
  589. {
  590. // Pick the row from the objects added *in memory*
  591. $oRetObj = $this->m_aAddedObjects[$this->m_iCurrRow - $this->m_iNumLoadedDBRows][$sRequestedClassAlias];
  592. }
  593. $this->m_iCurrRow++;
  594. return $aRetObjects;
  595. }
  596. /**
  597. * Position the cursor (for iterating in the set) to the first position (equivalent to Seek(0))
  598. */
  599. public function Rewind()
  600. {
  601. if ($this->m_bLoaded)
  602. {
  603. $this->Seek(0);
  604. }
  605. }
  606. /**
  607. * Position the cursor (for iterating in the set) to the given position
  608. *
  609. * @param int $iRow
  610. */
  611. public function Seek($iRow)
  612. {
  613. if (!$this->m_bLoaded) $this->Load();
  614. $this->m_iCurrRow = min($iRow, $this->Count());
  615. if ($this->m_iCurrRow < $this->m_iNumLoadedDBRows)
  616. {
  617. $this->m_oSQLResult->data_seek($this->m_iCurrRow);
  618. }
  619. return $this->m_iCurrRow;
  620. }
  621. /**
  622. * Add an object to the current set (in-memory only, nothing is written to the database)
  623. *
  624. * Limitation:
  625. * Sets with several objects per row are NOT supported
  626. *
  627. * @param DBObject $oObject The object to add
  628. * @param string $sClassAlias The alias for the class of the object
  629. */
  630. public function AddObject($oObject, $sClassAlias = '')
  631. {
  632. if (!$this->m_bLoaded) $this->Load();
  633. if (strlen($sClassAlias) == 0)
  634. {
  635. $sClassAlias = $this->m_oFilter->GetClassAlias();
  636. }
  637. $iNextPos = count($this->m_aAddedObjects);
  638. $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject;
  639. if (!is_null($oObject))
  640. {
  641. $this->m_aAddedIds[$oObject->GetKey()] = true;
  642. }
  643. }
  644. /**
  645. * Add a hash containig objects into the current set.
  646. *
  647. * The expected format for the hash is: $aObjectArray[$idx][$sClassAlias] => $oObject
  648. * Limitation:
  649. * The aliases MUST match the ones used in the current set
  650. * Only the ID of the objects associated to the first alias (column) is remembered.. in case we have to rebuild a filter
  651. *
  652. * @param hash $aObjectArray
  653. */
  654. protected function AddObjectExtended($aObjectArray)
  655. {
  656. if (!$this->m_bLoaded) $this->Load();
  657. $iNextPos = count($this->m_aAddedObjects);
  658. $sFirstAlias = $this->m_oFilter->GetClassAlias();
  659. foreach ($aObjectArray as $sClassAlias => $oObject)
  660. {
  661. $this->m_aAddedObjects[$iNextPos][$sClassAlias] = $oObject;
  662. if (!is_null($oObject) && ($sFirstAlias == $sClassAlias))
  663. {
  664. $this->m_aAddedIds[$oObject->GetKey()] = true;
  665. }
  666. }
  667. }
  668. /**
  669. * Add an array of objects into the current set
  670. *
  671. * Limitation:
  672. * Sets with several classes per row are not supported (use AddObjectExtended instead)
  673. *
  674. * @param array $aObjects The array of objects to add
  675. * @param string $sClassAlias The Alias of the class for the added objects
  676. */
  677. public function AddObjectArray($aObjects, $sClassAlias = '')
  678. {
  679. if (!$this->m_bLoaded) $this->Load();
  680. // #@# todo - add a check on the object class ?
  681. foreach ($aObjects as $oObj)
  682. {
  683. $this->AddObject($oObj, $sClassAlias);
  684. }
  685. }
  686. /**
  687. * Append a given set to the current object. (This method used to be named Merge)
  688. *
  689. * Limitation:
  690. * The added objects are not checked for duplicates (i.e. one cann add several times the same object, or add an object already present in the set).
  691. *
  692. * @param DBObjectSet $oObjectSet The set to append
  693. * @throws CoreException
  694. */
  695. public function Append(DBObjectSet $oObjectSet)
  696. {
  697. if ($this->GetRootClass() != $oObjectSet->GetRootClass())
  698. {
  699. throw new CoreException("Could not merge two objects sets if they don't have the same root class");
  700. }
  701. if (!$this->m_bLoaded) $this->Load();
  702. $oObjectSet->Seek(0);
  703. while ($oObject = $oObjectSet->Fetch())
  704. {
  705. $this->AddObject($oObject);
  706. }
  707. }
  708. /**
  709. * Create a set containing the objects present in both the current set and another specified set
  710. *
  711. * Limitations:
  712. * Will NOT work if only a subset of the sets was loaded with SetLimit.
  713. * Works only with sets made of objects loaded from the database since the comparison is based on the objects identifiers
  714. *
  715. * @param DBObjectSet $oObjectSet The set to intersect with. The current position inside the set will be lost (= at the end)
  716. * @throws CoreException
  717. * @return DBObjectSet A new set of objects, containing the objects present in both sets (based on their identifier)
  718. */
  719. public function CreateIntersect(DBObjectSet $oObjectSet)
  720. {
  721. if ($this->GetRootClass() != $oObjectSet->GetRootClass())
  722. {
  723. throw new CoreException("Could not 'intersect' two objects sets if they don't have the same root class");
  724. }
  725. if (!$this->m_bLoaded) $this->Load();
  726. $aId2Row = array();
  727. $iCurrPos = $this->m_iCurrRow; // Save the cursor
  728. $idx = 0;
  729. while($oObj = $this->Fetch())
  730. {
  731. $aId2Row[$oObj->GetKey()] = $idx;
  732. $idx++;
  733. }
  734. $oNewSet = DBObjectSet::FromScratch($this->GetClass());
  735. $oObjectSet->Seek(0);
  736. while ($oObject = $oObjectSet->Fetch())
  737. {
  738. if (array_key_exists($oObject->GetKey(), $aId2Row))
  739. {
  740. $oNewSet->AddObject($oObject);
  741. }
  742. }
  743. $this->Seek($iCurrPos); // Restore the cursor
  744. return $oNewSet;
  745. }
  746. /**
  747. * Compare two sets of objects to determine if their content is identical or not.
  748. *
  749. * Limitation:
  750. * Works only on objects written to the DB, since we rely on their identifiers
  751. *
  752. * @param DBObjectSet $oObjectSet
  753. * @return boolean True if the sets are identical, false otherwise
  754. */
  755. public function HasSameContents(DBObjectSet $oObjectSet)
  756. {
  757. if ($this->GetRootClass() != $oObjectSet->GetRootClass())
  758. {
  759. return false;
  760. }
  761. if ($this->Count() != $oObjectSet->Count())
  762. {
  763. return false;
  764. }
  765. $aId2Row = array();
  766. $bRet = true;
  767. $iCurrPos = $this->m_iCurrRow; // Save the cursor
  768. $idx = 0;
  769. // Optimization: we retain the first $iMaxObjects objects in memory
  770. // to speed up the comparison of small sets (see below: $oObject->Equals($oSibling))
  771. $iMaxObjects = 20;
  772. $aCachedObjects = array();
  773. while($oObj = $this->Fetch())
  774. {
  775. $aId2Row[$oObj->GetKey()] = $idx;
  776. if ($idx <= $iMaxObjects)
  777. {
  778. $aCachedObjects[$idx] = $oObj;
  779. }
  780. $idx++;
  781. }
  782. $oObjectSet->Rewind();
  783. while ($oObject = $oObjectSet->Fetch())
  784. {
  785. $iObjectKey = $oObject->GetKey();
  786. if ($iObjectKey < 0)
  787. {
  788. $bRet = false;
  789. break;
  790. }
  791. if (!array_key_exists($iObjectKey, $aId2Row))
  792. {
  793. $bRet = false;
  794. break;
  795. }
  796. $iRow = $aId2Row[$iObjectKey];
  797. if (array_key_exists($iRow, $aCachedObjects))
  798. {
  799. // Cache hit
  800. $oSibling = $aCachedObjects[$iRow];
  801. }
  802. else
  803. {
  804. // Go fetch it from the DB, unless it's an object added in-memory
  805. $oSibling = $this->GetObjectAt($iRow);
  806. }
  807. if (!$oObject->Equals($oSibling))
  808. {
  809. $bRet = false;
  810. break;
  811. }
  812. }
  813. $this->Seek($iCurrPos); // Restore the cursor
  814. return $bRet;
  815. }
  816. protected function GetObjectAt($iIndex)
  817. {
  818. if (!$this->m_bLoaded) $this->Load();
  819. // Save the current position for iteration
  820. $iCurrPos = $this->m_iCurrRow;
  821. $this->Seek($iIndex);
  822. $oObject = $this->Fetch();
  823. // Restore the current position for iteration
  824. $this->Seek($this->m_iCurrRow);
  825. return $oObject;
  826. }
  827. /**
  828. * Build a new set (in memory) made of objects of the given set which are NOT present in the current set
  829. *
  830. * Limitations:
  831. * The objects inside the set must be written in the database since the comparison is based on their identifiers
  832. * Sets with several objects per row are NOT supported
  833. *
  834. * @param DBObjectSet $oObjectSet
  835. * @throws CoreException
  836. *
  837. * @return DBObjectSet The "delta" set.
  838. */
  839. public function CreateDelta(DBObjectSet $oObjectSet)
  840. {
  841. if ($this->GetRootClass() != $oObjectSet->GetRootClass())
  842. {
  843. throw new CoreException("Could not 'delta' two objects sets if they don't have the same root class");
  844. }
  845. if (!$this->m_bLoaded) $this->Load();
  846. $aId2Row = array();
  847. $iCurrPos = $this->m_iCurrRow; // Save the cursor
  848. $idx = 0;
  849. while($oObj = $this->Fetch())
  850. {
  851. $aId2Row[$oObj->GetKey()] = $idx;
  852. $idx++;
  853. }
  854. $oNewSet = DBObjectSet::FromScratch($this->GetClass());
  855. $oObjectSet->Seek(0);
  856. while ($oObject = $oObjectSet->Fetch())
  857. {
  858. if (!array_key_exists($oObject->GetKey(), $aId2Row))
  859. {
  860. $oNewSet->AddObject($oObject);
  861. }
  862. }
  863. $this->Seek($iCurrPos); // Restore the cursor
  864. return $oNewSet;
  865. }
  866. /**
  867. * Compute the "RelatedObjects" (for the given relation, as defined by MetaModel::GetRelatedObjects) for a whole set of DBObjects
  868. *
  869. * @param string $sRelCode The code of the relation to use for the computation
  870. * @param int $iMaxDepth Teh maximum recursion depth
  871. *
  872. * @return Array An array containg all the "related" objects
  873. */
  874. public function GetRelatedObjects($sRelCode, $iMaxDepth = 99)
  875. {
  876. $aRelatedObjs = array();
  877. $aVisited = array(); // optimization for consecutive calls of MetaModel::GetRelatedObjects
  878. $this->Seek(0);
  879. while ($oObject = $this->Fetch())
  880. {
  881. $aMore = $oObject->GetRelatedObjects($sRelCode, $iMaxDepth, $aVisited);
  882. foreach ($aMore as $sClass => $aRelated)
  883. {
  884. foreach ($aRelated as $iObj => $oObj)
  885. {
  886. if (!isset($aRelatedObjs[$sClass][$iObj]))
  887. {
  888. $aRelatedObjs[$sClass][$iObj] = $oObj;
  889. }
  890. }
  891. }
  892. }
  893. return $aRelatedObjs;
  894. }
  895. /**
  896. * Builds an object that contains the values that are common to all the objects
  897. * in the set. If for a given attribute, objects in the set have various values
  898. * then the resulting object will contain null for this value.
  899. * @param $aValues Hash Output: the distribution of the values, in the set, for each attribute
  900. * @return DBObject The object with the common values
  901. */
  902. public function ComputeCommonObject(&$aValues)
  903. {
  904. $sClass = $this->GetClass();
  905. $aList = MetaModel::ListAttributeDefs($sClass);
  906. $aValues = array();
  907. foreach($aList as $sAttCode => $oAttDef)
  908. {
  909. if ($oAttDef->IsScalar())
  910. {
  911. $aValues[$sAttCode] = array();
  912. }
  913. }
  914. $this->Rewind();
  915. while($oObj = $this->Fetch())
  916. {
  917. foreach($aList as $sAttCode => $oAttDef)
  918. {
  919. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  920. {
  921. $currValue = $oObj->Get($sAttCode);
  922. if (is_object($currValue)) continue; // Skip non scalar values...
  923. if(!array_key_exists($currValue, $aValues[$sAttCode]))
  924. {
  925. $aValues[$sAttCode][$currValue] = array('count' => 1, 'display' => $oObj->GetAsHTML($sAttCode));
  926. }
  927. else
  928. {
  929. $aValues[$sAttCode][$currValue]['count']++;
  930. }
  931. }
  932. }
  933. }
  934. foreach($aValues as $sAttCode => $aMultiValues)
  935. {
  936. if (count($aMultiValues) > 1)
  937. {
  938. uasort($aValues[$sAttCode], 'HashCountComparison');
  939. }
  940. }
  941. // Now create an object that has values for the homogenous values only
  942. $oCommonObj = new $sClass(); // @@ What if the class is abstract ?
  943. $aComments = array();
  944. $iFormId = cmdbAbstractObject::GetNextFormId(); // Identifier that prefixes all the form fields
  945. $sReadyScript = '';
  946. $aDependsOn = array();
  947. $sFormPrefix = '2_';
  948. foreach($aList as $sAttCode => $oAttDef)
  949. {
  950. if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
  951. {
  952. if ($oAttDef->GetEditClass() == 'One Way Password')
  953. {
  954. $oCommonObj->Set($sAttCode, null);
  955. }
  956. else
  957. {
  958. $iCount = count($aValues[$sAttCode]);
  959. if ($iCount == 1)
  960. {
  961. // Homogenous value
  962. reset($aValues[$sAttCode]);
  963. $aKeys = array_keys($aValues[$sAttCode]);
  964. $currValue = $aKeys[0]; // The only value is the first key
  965. $oCommonObj->Set($sAttCode, $currValue);
  966. }
  967. else
  968. {
  969. // Non-homogenous value
  970. $oCommonObj->Set($sAttCode, null);
  971. }
  972. }
  973. }
  974. }
  975. $this->Rewind();
  976. return $oCommonObj;
  977. }
  978. /**
  979. * List the constant fields (and their value) in the given query
  980. * @return Hash [Alias][AttCode] => value
  981. */
  982. public function ListConstantFields()
  983. {
  984. $aScalarArgs = $this->ExpandArgs();
  985. $aConst = $this->m_oFilter->ListConstantFields();
  986. foreach($aConst as $sClassAlias => $aVals)
  987. {
  988. foreach($aVals as $sCode => $oExpr)
  989. {
  990. $oScalarExpr = $oExpr->GetAsScalar($aScalarArgs);
  991. $aConst[$sClassAlias][$sCode] = $oScalarExpr->GetValue();
  992. }
  993. }
  994. return $aConst;
  995. }
  996. protected function ExpandArgs()
  997. {
  998. $aScalarArgs = $this->m_oFilter->GetInternalParams();
  999. foreach($this->m_aArgs as $sArgName => $value)
  1000. {
  1001. if (MetaModel::IsValidObject($value))
  1002. {
  1003. if (strpos($sArgName, '->object()') === false)
  1004. {
  1005. // Lazy syntax - develop the object contextual parameters
  1006. $aScalarArgs = array_merge($aScalarArgs, $value->ToArgsForQuery($sArgName));
  1007. }
  1008. else
  1009. {
  1010. // Leave as is
  1011. $aScalarArgs[$sArgName] = $value;
  1012. }
  1013. }
  1014. else
  1015. {
  1016. $aScalarArgs[$sArgName] = (string) $value;
  1017. }
  1018. }
  1019. $aScalarArgs['current_contact_id'] = UserRights::GetContactId();
  1020. return $aScalarArgs;
  1021. }
  1022. public function ApplyParameters()
  1023. {
  1024. $aScalarArgs = $this->ExpandArgs();
  1025. $this->m_oFilter->ApplyParameters($aScalarArgs);
  1026. }
  1027. }
  1028. /**
  1029. * Helper function to perform a custom sort of a hash array
  1030. */
  1031. function HashCountComparison($a, $b) // Sort descending on 'count'
  1032. {
  1033. if ($a['count'] == $b['count'])
  1034. {
  1035. return 0;
  1036. }
  1037. return ($a['count'] > $b['count']) ? -1 : 1;
  1038. }
  1039. ?>