ormlinkset.class.inc.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. <?php
  2. // Copyright (C) 2010-2017 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. require_once('dbobjectiterator.php');
  19. /**
  20. * The value for an attribute representing a set of links between the host object and "remote" objects
  21. *
  22. * @package iTopORM
  23. * @copyright Copyright (C) 2010-2017 Combodo SARL
  24. * @license http://opensource.org/licenses/AGPL-3.0
  25. */
  26. class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator
  27. {
  28. protected $sHostClass; // subclass of DBObject
  29. protected $sAttCode; // xxxxxx_list
  30. protected $sClass; // class of the links
  31. /**
  32. * @var DBObjectSet
  33. */
  34. protected $oOriginalSet;
  35. /**
  36. * @var DBObject[] array of iObjectId => DBObject
  37. */
  38. protected $aOriginalObjects = null;
  39. /**
  40. * @var bool
  41. */
  42. protected $bHasDelta = false;
  43. /**
  44. * Object from the original set, minus the removed objects
  45. * @var DBObject[] array of iObjectId => DBObject
  46. */
  47. protected $aPreserved;
  48. /**
  49. * @var DBObject[] New items
  50. */
  51. protected $aAdded = array();
  52. /**
  53. * @var DBObject[] Modified items (could also be found in aPreserved)
  54. */
  55. protected $aModified = array();
  56. /**
  57. * @var int[] Removed items
  58. */
  59. protected $aRemoved = array();
  60. /**
  61. * @var int Position in the collection
  62. */
  63. protected $iCursor = 0;
  64. /**
  65. * ormLinkSet constructor.
  66. * @param $sHostClass
  67. * @param $sAttCode
  68. * @param DBObjectSet|null $oOriginalSet
  69. * @throws Exception
  70. */
  71. public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null)
  72. {
  73. $this->sHostClass = $sHostClass;
  74. $this->sAttCode = $sAttCode;
  75. $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null;
  76. $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode);
  77. if (!$oAttDef instanceof AttributeLinkedSet)
  78. {
  79. throw new Exception("ormLinkSet: $sAttCode is not a link set");
  80. }
  81. $this->sClass = $oAttDef->GetLinkedClass();
  82. if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass))
  83. {
  84. throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}");
  85. }
  86. }
  87. public function GetFilter()
  88. {
  89. return clone $this->oOriginalSet->GetFilter();
  90. }
  91. /**
  92. * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB
  93. *
  94. * @param hash $aAttToLoad Format: alias => array of attribute_codes
  95. *
  96. * @return void
  97. */
  98. public function OptimizeColumnLoad($aAttToLoad)
  99. {
  100. $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad);
  101. }
  102. /**
  103. * @param DBObject $oLink
  104. */
  105. public function AddItem(DBObject $oLink)
  106. {
  107. assert($oLink instanceof $this->sClass);
  108. // No impact on the iteration algorithm
  109. $this->aAdded[] = $oLink;
  110. $this->bHasDelta = true;
  111. }
  112. /**
  113. * @param DBObject $oObject
  114. * @param string $sClassAlias
  115. * @deprecated Since iTop 2.4, use ormLinkset->AddItem() instead.
  116. */
  117. public function AddObject(DBObject $oObject, $sClassAlias = '')
  118. {
  119. $this->AddItem($oObject);
  120. }
  121. /**
  122. * @param $iObjectId
  123. */
  124. public function RemoveItem($iObjectId)
  125. {
  126. if (array_key_exists($iObjectId, $this->aPreserved))
  127. {
  128. unset($this->aPreserved[$iObjectId]);
  129. $this->aRemoved[$iObjectId] = $iObjectId;
  130. $this->bHasDelta = true;
  131. }
  132. }
  133. /**
  134. * @param DBObject $oLink
  135. */
  136. public function ModifyItem(DBObject $oLink)
  137. {
  138. assert($oLink instanceof $this->sClass);
  139. $iObjectId = $oLink->GetKey();
  140. if (array_key_exists($iObjectId, $this->aPreserved))
  141. {
  142. unset($this->aPreserved[$iObjectId]);
  143. $this->aModified[$iObjectId] = $oLink;
  144. $this->bHasDelta = true;
  145. }
  146. }
  147. protected function LoadOriginalIds()
  148. {
  149. if ($this->aOriginalObjects === null)
  150. {
  151. if ($this->oOriginalSet)
  152. {
  153. $this->aOriginalObjects = $this->oOriginalSet->ToArray();
  154. $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified)
  155. foreach ($this->aRemoved as $iObjectId)
  156. {
  157. if (array_key_exists($iObjectId, $this->aPreserved))
  158. {
  159. unset($this->aPreserved[$iObjectId]);
  160. }
  161. }
  162. foreach ($this->aModified as $iObjectId)
  163. {
  164. if (array_key_exists($iObjectId, $this->aPreserved))
  165. {
  166. unset($this->aPreserved[$iObjectId]);
  167. }
  168. }
  169. }
  170. else
  171. {
  172. // Nothing to load
  173. $this->aOriginalObjects = array();
  174. $this->aPreserved = array();
  175. }
  176. }
  177. }
  178. /**
  179. * @param bool $bWithId
  180. * @return array
  181. * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead
  182. */
  183. public function ToArray($bWithId = true)
  184. {
  185. $aRet = array();
  186. foreach($this as $oItem)
  187. {
  188. if ($bWithId)
  189. {
  190. $aRet[$oItem->GetKey()] = $oItem;
  191. }
  192. else
  193. {
  194. $aRet[] = $oItem;
  195. }
  196. }
  197. return $aRet;
  198. }
  199. /**
  200. * @param string $sAttCode
  201. * @param bool $bWithId
  202. * @return array
  203. */
  204. public function GetColumnAsArray($sAttCode, $bWithId = true)
  205. {
  206. $aRet = array();
  207. foreach($this as $oItem)
  208. {
  209. if ($bWithId)
  210. {
  211. $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode);
  212. }
  213. else
  214. {
  215. $aRet[] = $oItem->Get($sAttCode);
  216. }
  217. }
  218. return $aRet;
  219. }
  220. /**
  221. * The class of the objects of the collection (at least a common ancestor)
  222. *
  223. * @return string
  224. */
  225. public function GetClass()
  226. {
  227. return $this->sClass;
  228. }
  229. /**
  230. * The total number of objects in the collection
  231. *
  232. * @return int
  233. */
  234. public function Count()
  235. {
  236. $this->LoadOriginalIds();
  237. $iRet = count($this->aPreserved) + count($this->aAdded) + count($this->aModified);
  238. return $iRet;
  239. }
  240. /**
  241. * Position the cursor to the given 0-based position
  242. *
  243. * @param $iPosition
  244. * @throws Exception
  245. * @internal param int $iRow
  246. */
  247. public function Seek($iPosition)
  248. {
  249. $this->LoadOriginalIds();
  250. $iCount = $this->Count();
  251. if ($iPosition >= $iCount)
  252. {
  253. throw new Exception("Invalid position $iPosition: the link set is made of $iCount items.");
  254. }
  255. $this->rewind();
  256. for($iPos = 0 ; $iPos < $iPosition ; $iPos++)
  257. {
  258. $this->next();
  259. }
  260. }
  261. /**
  262. * Fetch the object at the current position in the collection and move the cursor to the next position.
  263. *
  264. * @return DBObject|null The fetched object or null when at the end
  265. */
  266. public function Fetch()
  267. {
  268. $this->LoadOriginalIds();
  269. $ret = $this->current();
  270. if ($ret === false)
  271. {
  272. $ret = null;
  273. }
  274. $this->next();
  275. return $ret;
  276. }
  277. /**
  278. * Return the current element
  279. * @link http://php.net/manual/en/iterator.current.php
  280. * @return mixed Can return any type.
  281. */
  282. public function current()
  283. {
  284. $this->LoadOriginalIds();
  285. $iPreservedCount = count($this->aPreserved);
  286. if ($this->iCursor < $iPreservedCount)
  287. {
  288. $oRet = current($this->aPreserved);
  289. }
  290. else
  291. {
  292. $iModifiedCount = count($this->aModified);
  293. if($this->iCursor < $iPreservedCount + $iModifiedCount)
  294. {
  295. $oRet = current($this->aModified);
  296. }
  297. else
  298. {
  299. $oRet = current($this->aAdded);
  300. }
  301. }
  302. return $oRet;
  303. }
  304. /**
  305. * Move forward to next element
  306. * @link http://php.net/manual/en/iterator.next.php
  307. * @return void Any returned value is ignored.
  308. */
  309. public function next()
  310. {
  311. $this->LoadOriginalIds();
  312. $iPreservedCount = count($this->aPreserved);
  313. if ($this->iCursor < $iPreservedCount)
  314. {
  315. next($this->aPreserved);
  316. }
  317. else
  318. {
  319. $iModifiedCount = count($this->aModified);
  320. if($this->iCursor < $iPreservedCount + $iModifiedCount)
  321. {
  322. next($this->aModified);
  323. }
  324. else
  325. {
  326. next($this->aAdded);
  327. }
  328. }
  329. // Increment AFTER moving the internal cursors because when starting aModified / aAdded, we must leave it intact
  330. $this->iCursor++;
  331. }
  332. /**
  333. * Return the key of the current element
  334. * @link http://php.net/manual/en/iterator.key.php
  335. * @return mixed scalar on success, or null on failure.
  336. */
  337. public function key()
  338. {
  339. return $this->iCursor;
  340. }
  341. /**
  342. * Checks if current position is valid
  343. * @link http://php.net/manual/en/iterator.valid.php
  344. * @return boolean The return value will be casted to boolean and then evaluated.
  345. * Returns true on success or false on failure.
  346. */
  347. public function valid()
  348. {
  349. $this->LoadOriginalIds();
  350. $iCount = $this->Count();
  351. $bRet = ($this->iCursor < $iCount);
  352. return $bRet;
  353. }
  354. /**
  355. * Rewind the Iterator to the first element
  356. * @link http://php.net/manual/en/iterator.rewind.php
  357. * @return void Any returned value is ignored.
  358. */
  359. public function rewind()
  360. {
  361. $this->LoadOriginalIds();
  362. $this->iCursor = 0;
  363. reset($this->aPreserved);
  364. reset($this->aAdded);
  365. reset($this->aModified);
  366. }
  367. public function HasDelta()
  368. {
  369. return $this->bHasDelta;
  370. }
  371. /**
  372. * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this.
  373. * @param ormLinkSet $oFellow
  374. * @return bool|null
  375. * @throws Exception
  376. */
  377. public function Equals(ormLinkSet $oFellow)
  378. {
  379. $bRet = null;
  380. if ($this === $oFellow)
  381. {
  382. $bRet = true;
  383. }
  384. else
  385. {
  386. if ( ($this->oOriginalSet !== $oFellow->oOriginalSet)
  387. && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
  388. {
  389. throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope');
  390. }
  391. if ($this->HasDelta())
  392. {
  393. throw new Exception('ormLinkSet::Equals assumes that left link set had no delta');
  394. }
  395. $bRet = !$oFellow->HasDelta();
  396. }
  397. return $bRet;
  398. }
  399. public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow)
  400. {
  401. if ($oFellow === $this)
  402. {
  403. throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one');
  404. }
  405. $bUpdateFromDelta = false;
  406. if ($oFellow instanceof ormLinkSet)
  407. {
  408. if ( ($this->oOriginalSet === $oFellow->oOriginalSet)
  409. || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
  410. {
  411. $bUpdateFromDelta = true;
  412. }
  413. }
  414. if ($bUpdateFromDelta)
  415. {
  416. // Same original set -> simply update the delta
  417. $this->iCursor = 0;
  418. $this->aAdded = $oFellow->aAdded;
  419. $this->aRemoved = $oFellow->aRemoved;
  420. $this->aModified = $oFellow->aModified;
  421. $this->aPreserved = $oFellow->aPreserved;
  422. $this->bHasDelta = $oFellow->bHasDelta;
  423. }
  424. else
  425. {
  426. // For backward compatibility reasons, let's rebuild a delta...
  427. // Reset the delta
  428. $this->iCursor = 0;
  429. $this->aAdded = array();
  430. $this->aRemoved = array();
  431. $this->aModified = array();
  432. $this->aPreserved = $this->aOriginalObjects;
  433. $this->bHasDelta = false;
  434. /** @var AttributeLinkedSet $oAttDef */
  435. $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode);
  436. $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
  437. $sAdditionalKey = null;
  438. if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
  439. {
  440. $sAdditionalKey = $oAttDef->GetExtKeyToRemote();
  441. }
  442. // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference)
  443. $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey);
  444. $aChanges = $oComparator->GetDifferences();
  445. foreach ($aChanges['added'] as $oLink)
  446. {
  447. $this->AddItem($oLink);
  448. }
  449. foreach ($aChanges['modified'] as $oLink)
  450. {
  451. $this->ModifyItem($oLink);
  452. }
  453. foreach ($aChanges['removed'] as $oLink)
  454. {
  455. $this->RemoveItem($oLink->GetKey());
  456. }
  457. }
  458. }
  459. /**
  460. * @param DBObject $oHostObject
  461. */
  462. public function DBWrite(DBObject $oHostObject)
  463. {
  464. /** @var AttributeLinkedSet $oAttDef */
  465. $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode);
  466. $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
  467. $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a';
  468. $aCheckLinks = array();
  469. $aCheckRemote = array();
  470. foreach ($this->aAdded as $oLink)
  471. {
  472. if ($oLink->IsNew())
  473. {
  474. if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
  475. {
  476. //todo: faire un test qui passe dans cette branche !
  477. $aCheckRemote[] = $oLink->Get($sExtKeyToRemote);
  478. }
  479. }
  480. else
  481. {
  482. //todo: faire un test qui passe dans cette branche !
  483. $aCheckLinks[] = $oLink->GetKey();
  484. }
  485. }
  486. foreach ($this->aRemoved as $iLinkId)
  487. {
  488. $aCheckLinks[] = $iLinkId;
  489. }
  490. foreach ($this->aModified as $iLinkId => $oLink)
  491. {
  492. $aCheckLinks[] = $oLink->GetKey();
  493. }
  494. // Critical section : serialize any write access to these links
  495. //
  496. $oMtx = new iTopMutex('Write-'.$this->sClass);
  497. $oMtx->Lock();
  498. // Check for the existing links
  499. //
  500. if (count($aCheckLinks) > 0)
  501. {
  502. $oSearch = new DBObjectSearch($this->sClass);
  503. $oSearch->AddCondition('id', $aCheckLinks, 'IN');
  504. $oSet = new DBObjectSet($oSearch);
  505. /** @var DBObject[] $aExistingLinks */
  506. $aExistingLinks = $oSet->ToArray();
  507. }
  508. // Check for the existing remote objects
  509. //
  510. if (count($aCheckRemote) > 0)
  511. {
  512. $oSearch = new DBObjectSearch($this->sClass);
  513. $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '=');
  514. $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN');
  515. $oSet = new DBObjectSet($oSearch);
  516. /** @var Int[] $aExistingRemote */
  517. $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote);
  518. }
  519. // Write the links according to the existing links
  520. //
  521. foreach ($this->aAdded as $oLink)
  522. {
  523. // Make sure that the objects in the set point to "this"
  524. $oLink->Set($sExtKeyToMe, $oHostObject->GetKey());
  525. if ($oLink->IsNew())
  526. {
  527. if (count($aCheckRemote) > 0)
  528. {
  529. if (in_array($oLink->Get($sExtKeyToRemote), $aExistingRemote))
  530. {
  531. // Do not create a duplicate
  532. continue;
  533. }
  534. }
  535. }
  536. else
  537. {
  538. if (!array_key_exists($oLink->GetKey(), $aExistingLinks))
  539. {
  540. $oLink->DBClone();
  541. }
  542. }
  543. $oLink->DBWrite();
  544. }
  545. foreach ($this->aRemoved as $iLinkId)
  546. {
  547. if (array_key_exists($iLinkId, $aExistingLinks))
  548. {
  549. $oLink = $aExistingLinks[$iLinkId];
  550. if ($oAttDef->IsIndirect())
  551. {
  552. $oLink->DBDelete();
  553. }
  554. else
  555. {
  556. $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe);
  557. if ($oExtKeyToRemote->IsNullAllowed())
  558. {
  559. if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey())
  560. {
  561. // Detach the link object from this
  562. $oLink->Set($sExtKeyToMe, 0);
  563. $oLink->DBUpdate();
  564. }
  565. }
  566. else
  567. {
  568. $oLink->DBDelete();
  569. }
  570. }
  571. }
  572. }
  573. // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored
  574. foreach ($this->aModified as $iLinkId => $oLink)
  575. {
  576. if (array_key_exists($oLink->GetKey(), $aExistingLinks))
  577. {
  578. $oLink->DBUpdate();
  579. }
  580. else
  581. {
  582. $oLink->DBClone();
  583. }
  584. }
  585. // End of the critical section
  586. //
  587. $oMtx->Unlock();
  588. }
  589. }