ormlinkset.class.inc.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  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. $this->aModified[$iObjectId] = $oLink;
  141. $this->bHasDelta = true;
  142. }
  143. protected function LoadOriginalIds()
  144. {
  145. if ($this->aOriginalObjects === null)
  146. {
  147. if ($this->oOriginalSet)
  148. {
  149. $this->aOriginalObjects = $this->oOriginalSet->ToArray();
  150. $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified)
  151. foreach ($this->aRemoved as $iObjectId)
  152. {
  153. if (array_key_exists($iObjectId, $this->aPreserved))
  154. {
  155. unset($this->aPreserved[$iObjectId]);
  156. }
  157. }
  158. }
  159. else
  160. {
  161. // Nothing to load
  162. $this->aOriginalObjects = array();
  163. $this->aPreserved = array();
  164. }
  165. }
  166. }
  167. /**
  168. * @param bool $bWithId
  169. * @return array
  170. * @deprecated Since iTop 2.4, use foreach($this as $oItem){} instead
  171. */
  172. public function ToArray($bWithId = true)
  173. {
  174. $aRet = array();
  175. foreach($this as $oItem)
  176. {
  177. if ($bWithId)
  178. {
  179. $aRet[$oItem->GetKey()] = $oItem;
  180. }
  181. else
  182. {
  183. $aRet[] = $oItem;
  184. }
  185. }
  186. return $aRet;
  187. }
  188. /**
  189. * @param string $sAttCode
  190. * @param bool $bWithId
  191. * @return array
  192. */
  193. public function GetColumnAsArray($sAttCode, $bWithId = true)
  194. {
  195. $aRet = array();
  196. foreach($this as $oItem)
  197. {
  198. if ($bWithId)
  199. {
  200. $aRet[$oItem->GetKey()] = $oItem->Get($sAttCode);
  201. }
  202. else
  203. {
  204. $aRet[] = $oItem->Get($sAttCode);
  205. }
  206. }
  207. return $aRet;
  208. }
  209. /**
  210. * The class of the objects of the collection (at least a common ancestor)
  211. *
  212. * @return string
  213. */
  214. public function GetClass()
  215. {
  216. return $this->sClass;
  217. }
  218. /**
  219. * The total number of objects in the collection
  220. *
  221. * @return int
  222. */
  223. public function Count()
  224. {
  225. $this->LoadOriginalIds();
  226. $iRet = count($this->aPreserved) + count($this->aAdded);
  227. return $iRet;
  228. }
  229. /**
  230. * Position the cursor to the given 0-based position
  231. *
  232. * @param $iPosition
  233. * @throws Exception
  234. * @internal param int $iRow
  235. */
  236. public function Seek($iPosition)
  237. {
  238. $this->LoadOriginalIds();
  239. $iCount = $this->Count();
  240. if ($iPosition >= $iCount)
  241. {
  242. throw new Exception("Invalid position $iPosition: the link set is made of $iCount items.");
  243. }
  244. $this->rewind();
  245. for($iPos = 0 ; $iPos < $iPosition ; $iPos++)
  246. {
  247. $this->next();
  248. }
  249. }
  250. /**
  251. * Fetch the object at the current position in the collection and move the cursor to the next position.
  252. *
  253. * @return DBObject|null The fetched object or null when at the end
  254. */
  255. public function Fetch()
  256. {
  257. $this->LoadOriginalIds();
  258. $ret = $this->current();
  259. if ($ret === false)
  260. {
  261. $ret = null;
  262. }
  263. $this->next();
  264. return $ret;
  265. }
  266. /**
  267. * Return the current element
  268. * @link http://php.net/manual/en/iterator.current.php
  269. * @return mixed Can return any type.
  270. */
  271. public function current()
  272. {
  273. $this->LoadOriginalIds();
  274. $iPreservedCount = count($this->aPreserved);
  275. if ($this->iCursor < $iPreservedCount)
  276. {
  277. $oRet = current($this->aPreserved);
  278. }
  279. else
  280. {
  281. $oRet = current($this->aAdded);
  282. }
  283. return $oRet;
  284. }
  285. /**
  286. * Move forward to next element
  287. * @link http://php.net/manual/en/iterator.next.php
  288. * @return void Any returned value is ignored.
  289. */
  290. public function next()
  291. {
  292. $this->LoadOriginalIds();
  293. $iPreservedCount = count($this->aPreserved);
  294. if ($this->iCursor < $iPreservedCount)
  295. {
  296. next($this->aPreserved);
  297. }
  298. else
  299. {
  300. next($this->aAdded);
  301. }
  302. // Increment AFTER moving the internal cursors because when starting aAdded, we must leave it intact
  303. $this->iCursor++;
  304. }
  305. /**
  306. * Return the key of the current element
  307. * @link http://php.net/manual/en/iterator.key.php
  308. * @return mixed scalar on success, or null on failure.
  309. */
  310. public function key()
  311. {
  312. return $this->iCursor;
  313. }
  314. /**
  315. * Checks if current position is valid
  316. * @link http://php.net/manual/en/iterator.valid.php
  317. * @return boolean The return value will be casted to boolean and then evaluated.
  318. * Returns true on success or false on failure.
  319. */
  320. public function valid()
  321. {
  322. $this->LoadOriginalIds();
  323. $iCount = $this->Count();
  324. $bRet = ($this->iCursor < $iCount);
  325. return $bRet;
  326. }
  327. /**
  328. * Rewind the Iterator to the first element
  329. * @link http://php.net/manual/en/iterator.rewind.php
  330. * @return void Any returned value is ignored.
  331. */
  332. public function rewind()
  333. {
  334. $this->LoadOriginalIds();
  335. $this->iCursor = 0;
  336. reset($this->aPreserved);
  337. reset($this->aAdded);
  338. }
  339. public function HasDelta()
  340. {
  341. return $this->bHasDelta;
  342. }
  343. /**
  344. * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this.
  345. * @param ormLinkSet $oFellow
  346. * @return bool|null
  347. * @throws Exception
  348. */
  349. public function Equals(ormLinkSet $oFellow)
  350. {
  351. $bRet = null;
  352. if ($this === $oFellow)
  353. {
  354. $bRet = true;
  355. }
  356. else
  357. {
  358. if ( ($this->oOriginalSet !== $oFellow->oOriginalSet)
  359. && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
  360. {
  361. throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope');
  362. }
  363. if ($this->HasDelta())
  364. {
  365. throw new Exception('ormLinkSet::Equals assumes that left link set had no delta');
  366. }
  367. $bRet = !$oFellow->HasDelta();
  368. }
  369. return $bRet;
  370. }
  371. public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow)
  372. {
  373. if ($oFellow === $this)
  374. {
  375. throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one');
  376. }
  377. $bUpdateFromDelta = false;
  378. if ($oFellow instanceof ormLinkSet)
  379. {
  380. if ( ($this->oOriginalSet === $oFellow->oOriginalSet)
  381. || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) )
  382. {
  383. $bUpdateFromDelta = true;
  384. }
  385. }
  386. if ($bUpdateFromDelta)
  387. {
  388. // Same original set -> simply update the delta
  389. $this->iCursor = 0;
  390. $this->aAdded = $oFellow->aAdded;
  391. $this->aRemoved = $oFellow->aRemoved;
  392. $this->aModified = $oFellow->aModified;
  393. $this->aPreserved = $oFellow->aPreserved;
  394. $this->bHasDelta = $oFellow->bHasDelta;
  395. }
  396. else
  397. {
  398. // For backward compatibility reasons, let's rebuild a delta...
  399. // Reset the delta
  400. $this->iCursor = 0;
  401. $this->aAdded = array();
  402. $this->aRemoved = array();
  403. $this->aModified = array();
  404. $this->aPreserved = $this->aOriginalObjects;
  405. $this->bHasDelta = false;
  406. /** @var AttributeLinkedSet $oAttDef */
  407. $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode);
  408. $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
  409. $sAdditionalKey = null;
  410. if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
  411. {
  412. $sAdditionalKey = $oAttDef->GetExtKeyToRemote();
  413. }
  414. // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference)
  415. $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey);
  416. $aChanges = $oComparator->GetDifferences();
  417. foreach ($aChanges['added'] as $oLink)
  418. {
  419. $this->AddItem($oLink);
  420. }
  421. foreach ($aChanges['modified'] as $oLink)
  422. {
  423. $this->ModifyItem($oLink);
  424. }
  425. foreach ($aChanges['removed'] as $oLink)
  426. {
  427. $this->RemoveItem($oLink->GetKey());
  428. }
  429. }
  430. }
  431. /**
  432. * @param DBObject $oHostObject
  433. */
  434. public function DBWrite(DBObject $oHostObject)
  435. {
  436. /** @var AttributeLinkedSet $oAttDef */
  437. $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode);
  438. $sExtKeyToMe = $oAttDef->GetExtKeyToMe();
  439. $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a';
  440. $aCheckLinks = array();
  441. $aCheckRemote = array();
  442. foreach ($this->aAdded as $oLink)
  443. {
  444. if ($oLink->IsNew())
  445. {
  446. if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed())
  447. {
  448. //todo: faire un test qui passe dans cette branche !
  449. $aCheckRemote[] = $oLink->Get($sExtKeyToRemote);
  450. }
  451. }
  452. else
  453. {
  454. //todo: faire un test qui passe dans cette branche !
  455. $aCheckLinks[] = $oLink->GetKey();
  456. }
  457. }
  458. foreach ($this->aRemoved as $iLinkId)
  459. {
  460. $aCheckLinks[] = $iLinkId;
  461. }
  462. foreach ($this->aModified as $iLinkId => $oLink)
  463. {
  464. $aCheckLinks[] = $oLink->GetKey();
  465. }
  466. // Critical section : serialize any write access to these links
  467. //
  468. $oMtx = new iTopMutex('Write-'.$this->sClass);
  469. $oMtx->Lock();
  470. // Check for the existing links
  471. //
  472. if (count($aCheckLinks) > 0)
  473. {
  474. $oSearch = new DBObjectSearch($this->sClass);
  475. $oSearch->AddCondition('id', $aCheckLinks, 'IN');
  476. $oSet = new DBObjectSet($oSearch);
  477. /** @var DBObject[] $aExistingLinks */
  478. $aExistingLinks = $oSet->ToArray();
  479. }
  480. // Check for the existing remote objects
  481. //
  482. if (count($aCheckRemote) > 0)
  483. {
  484. $oSearch = new DBObjectSearch($this->sClass);
  485. $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '=');
  486. $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN');
  487. $oSet = new DBObjectSet($oSearch);
  488. /** @var Int[] $aExistingRemote */
  489. $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote);
  490. }
  491. // Write the links according to the existing links
  492. //
  493. foreach ($this->aAdded as $oLink)
  494. {
  495. // Make sure that the objects in the set point to "this"
  496. $oLink->Set($sExtKeyToMe, $oHostObject->GetKey());
  497. if ($oLink->IsNew())
  498. {
  499. if (count($aCheckRemote) > 0)
  500. {
  501. if (in_array($oLink->Get($sExtKeyToRemote), $aExistingRemote))
  502. {
  503. // Do not create a duplicate
  504. continue;
  505. }
  506. }
  507. }
  508. else
  509. {
  510. if (!array_key_exists($oLink->GetKey(), $aExistingLinks))
  511. {
  512. $oLink->DBClone();
  513. }
  514. }
  515. $oLink->DBWrite();
  516. }
  517. foreach ($this->aRemoved as $iLinkId)
  518. {
  519. if (array_key_exists($iLinkId, $aExistingLinks))
  520. {
  521. $oLink = $aExistingLinks[$iLinkId];
  522. if ($oAttDef->IsIndirect())
  523. {
  524. $oLink->DBDelete();
  525. }
  526. else
  527. {
  528. $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe);
  529. if ($oExtKeyToRemote->IsNullAllowed())
  530. {
  531. if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey())
  532. {
  533. // Detach the link object from this
  534. $oLink->Set($sExtKeyToMe, 0);
  535. $oLink->DBUpdate();
  536. }
  537. }
  538. else
  539. {
  540. $oLink->DBDelete();
  541. }
  542. }
  543. }
  544. }
  545. // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored
  546. foreach ($this->aModified as $iLinkId => $oLink)
  547. {
  548. if (array_key_exists($oLink->GetKey(), $aExistingLinks))
  549. {
  550. $oLink->DBUpdate();
  551. }
  552. else
  553. {
  554. $oLink->DBClone();
  555. }
  556. }
  557. // End of the critical section
  558. //
  559. $oMtx->Unlock();
  560. }
  561. }