require_once('dbobjectiterator.php'); /** * The value for an attribute representing a set of links between the host object and "remote" objects * * @package iTopORM * @copyright Copyright (C) 2010-2017 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ class ormLinkSet implements iDBObjectSetIterator, Iterator, SeekableIterator { protected $sHostClass; // subclass of DBObject protected $sAttCode; // xxxxxx_list protected $sClass; // class of the links /** * @var DBObjectSet */ protected $oOriginalSet; /** * @var DBObject[] array of iObjectId => DBObject */ protected $aOriginalObjects = null; /** * @var bool */ protected $bHasDelta = false; /** * Object from the original set, minus the removed objects * @var DBObject[] array of iObjectId => DBObject */ protected $aPreserved; /** * @var DBObject[] New items */ protected $aAdded = array(); /** * @var DBObject[] Modified items (could also be found in aPreserved) */ protected $aModified = array(); /** * @var int[] Removed items */ protected $aRemoved = array(); /** * @var int Position in the collection */ protected $iCursor = 0; /** * ormLinkSet constructor. * @param $sHostClass * @param $sAttCode * @param DBObjectSet|null $oOriginalSet * @throws Exception */ public function __construct($sHostClass, $sAttCode, DBObjectSet $oOriginalSet = null) { $this->sHostClass = $sHostClass; $this->sAttCode = $sAttCode; $this->oOriginalSet = $oOriginalSet ? clone $oOriginalSet : null; $oAttDef = MetaModel::GetAttributeDef($sHostClass, $sAttCode); if (!$oAttDef instanceof AttributeLinkedSet) { throw new Exception("ormLinkSet: $sAttCode is not a link set"); } $this->sClass = $oAttDef->GetLinkedClass(); if ($oOriginalSet && ($oOriginalSet->GetClass() != $this->sClass)) { throw new Exception("ormLinkSet: wrong class for the original set, found {$oOriginalSet->GetClass()} while expecting {$oAttDef->GetLinkedClass()}"); } } public function GetFilter() { return clone $this->oOriginalSet->GetFilter(); } /** * Specify the subset of attributes to load (for each class of objects) before performing the SQL query for retrieving the rows from the DB * * @param hash $aAttToLoad Format: alias => array of attribute_codes * * @return void */ public function OptimizeColumnLoad($aAttToLoad) { $this->oOriginalSet->OptimizeColumnLoad($aAttToLoad); } /** * @param DBObject $oLink */ public function AddItem(DBObject $oLink) { assert($oLink instanceof $this->sClass); // No impact on the iteration algorithm $this->aAdded[] = $oLink; $this->bHasDelta = true; } /** * @param DBObject $oObject * @param string $sClassAlias * @deprecated */ public function AddObject(DBObject $oObject, $sClassAlias = '') { trigger_error('iTop: ormLinkSet::AddObject() is deprecated use ormLinkSet::AddItem() instead.', E_USER_DEPRECATED); $this->AddItem($oObject); } /** * @param $iObjectId */ public function RemoveItem($iObjectId) { if (array_key_exists($iObjectId, $this->aPreserved)) { unset($this->aPreserved[$iObjectId]); $this->aRemoved[$iObjectId] = $iObjectId; $this->bHasDelta = true; } } /** * @param DBObject $oLink */ public function ModifyItem(DBObject $oLink) { assert($oLink instanceof $this->sClass); $iObjectId = $oLink->GetKey(); $this->aModified[$iObjectId] = $oLink; $this->bHasDelta = true; } protected function LoadOriginalIds() { if ($this->aOriginalObjects === null) { if ($this->oOriginalSet) { $this->aOriginalObjects = $this->oOriginalSet->ToArray(); $this->aPreserved = $this->aOriginalObjects; // Copy (not effective until aPreserved gets modified) foreach ($this->aRemoved as $iObjectId) { if (array_key_exists($iObjectId, $this->aPreserved)) { unset($this->aPreserved[$iObjectId]); } } } else { // Nothing to load $this->aOriginalObjects = array(); $this->aPreserved = array(); } } } /** * Note: After calling this method, the set cursor will be at the end of the set. You might to rewind it. * * @param bool $bWithId * @return array * @deprecated */ public function ToArray($bWithId = true) { trigger_error('iTop: ormLinkSet::ToArray() is deprecated use foreach instead.', E_USER_DEPRECATED); $aRet = array(); foreach($this as $oItem) { if ($bWithId) { $aRet[$oItem->GetKey()] = $oItem; } else { $aRet[] = $oItem; } } return $aRet; } /** * The class of the objects of the collection (at least a common ancestor) * * @return string */ public function GetClass() { return $this->sClass; } /** * The total number of objects in the collection * * @return int */ public function Count() { $this->LoadOriginalIds(); $iRet = count($this->aPreserved) + count($this->aAdded); return $iRet; } /** * Position the cursor to the given 0-based position * * @param $iPosition * @throws Exception * @internal param int $iRow */ public function Seek($iPosition) { $this->LoadOriginalIds(); $iCount = $this->Count(); if ($iPosition >= $iCount) { throw new Exception("Invalid position $iPosition: the link set is made of $iCount items."); } $this->rewind(); for($iPos = 0 ; $iPos < $iPosition ; $iPos++) { $this->next(); } } /** * Fetch the object at the current position in the collection and move the cursor to the next position. * * @return DBObject|null The fetched object or null when at the end */ public function Fetch() { $this->LoadOriginalIds(); $ret = $this->current(); if ($ret === false) { $ret = null; } $this->next(); return $ret; } /** * Return the current element * @link http://php.net/manual/en/iterator.current.php * @return mixed Can return any type. */ public function current() { $this->LoadOriginalIds(); $iPreservedCount = count($this->aPreserved); if ($this->iCursor < $iPreservedCount) { $oRet = current($this->aPreserved); } else { $oRet = current($this->aAdded); } return $oRet; } /** * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * @return void Any returned value is ignored. */ public function next() { $this->LoadOriginalIds(); $iPreservedCount = count($this->aPreserved); if ($this->iCursor < $iPreservedCount) { next($this->aPreserved); } else { next($this->aAdded); } // Increment AFTER moving the internal cursors because when starting aAdded, we must leave it intact $this->iCursor++; } /** * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * @return mixed scalar on success, or null on failure. */ public function key() { return $this->iCursor; } /** * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * @return boolean The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. */ public function valid() { $this->LoadOriginalIds(); $iCount = $this->Count(); $bRet = ($this->iCursor < $iCount); return $bRet; } /** * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * @return void Any returned value is ignored. */ public function rewind() { $this->LoadOriginalIds(); $this->iCursor = 0; reset($this->aPreserved); reset($this->aAdded); } public function HasDelta() { return $this->bHasDelta; } /** * This method has been designed specifically for AttributeLinkedSet:Equals and as such it assumes that the passed argument is a clone of this. * @param ormLinkSet $oFellow * @return bool|null * @throws Exception */ public function Equals(ormLinkSet $oFellow) { $bRet = null; if ($this === $oFellow) { $bRet = true; } else { if ( ($this->oOriginalSet !== $oFellow->oOriginalSet) && ($this->oOriginalSet->GetFilter()->ToOQL() != $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) { throw new Exception('ormLinkSet::Equals assumes that compared link sets have the same original scope'); } if ($this->HasDelta()) { throw new Exception('ormLinkSet::Equals assumes that left link set had no delta'); } $bRet = !$oFellow->HasDelta(); } return $bRet; } public function UpdateFromCompleteList(iDBObjectSetIterator $oFellow) { if ($oFellow === $this) { throw new Exception('ormLinkSet::UpdateFromCompleteList assumes that the passed link set is at least a clone of the current one'); } $bUpdateFromDelta = false; if ($oFellow instanceof ormLinkSet) { if ( ($this->oOriginalSet === $oFellow->oOriginalSet) || ($this->oOriginalSet->GetFilter()->ToOQL() == $oFellow->oOriginalSet->GetFilter()->ToOQL()) ) { $bUpdateFromDelta = true; } } if ($bUpdateFromDelta) { // Same original set -> simply update the delta $this->iCursor = 0; $this->aAdded = $oFellow->aAdded; $this->aRemoved = $oFellow->aRemoved; $this->aModified = $oFellow->aModified; $this->aPreserved = $oFellow->aPreserved; $this->bHasDelta = $oFellow->bHasDelta; } else { // For backward compatibility reasons, let's rebuild a delta... // Reset the delta $this->iCursor = 0; $this->aAdded = array(); $this->aRemoved = array(); $this->aModified = array(); $this->aPreserved = $this->aOriginalObjects; $this->bHasDelta = false; /** @var AttributeLinkedSet $oAttDef */ $oAttDef = MetaModel::GetAttributeDef($this->sHostClass, $this->sAttCode); $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); $sAdditionalKey = null; if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) { $sAdditionalKey = $oAttDef->GetExtKeyToRemote(); } // Compare both collections by iterating the whole sets, order them, a build a fingerprint based on meaningful data (what make the difference) $oComparator = new DBObjectSetComparator($this, $oFellow, array($sExtKeyToMe), $sAdditionalKey); $aChanges = $oComparator->GetDifferences(); foreach ($aChanges['added'] as $oLink) { $this->AddItem($oLink); } foreach ($aChanges['modified'] as $oLink) { $this->ModifyItem($oLink); } foreach ($aChanges['removed'] as $oLink) { $this->RemoveItem($oLink->GetKey()); } } } /** * @param DBObject $oHostObject */ public function DBWrite(DBObject $oHostObject) { /** @var AttributeLinkedSet $oAttDef */ $oAttDef = MetaModel::GetAttributeDef(get_class($oHostObject), $this->sAttCode); $sExtKeyToMe = $oAttDef->GetExtKeyToMe(); $sExtKeyToRemote = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote() : 'n/a'; $aCheckLinks = array(); $aCheckRemote = array(); foreach ($this->aAdded as $oLink) { if ($oLink->IsNew()) { if ($oAttDef->IsIndirect() && !$oAttDef->DuplicatesAllowed()) { //todo: faire un test qui passe dans cette branche ! $aCheckRemote[] = $oLink->Get($sExtKeyToRemote); } } else { //todo: faire un test qui passe dans cette branche ! $aCheckLinks[] = $oLink->GetKey(); } } foreach ($this->aRemoved as $iLinkId) { $aCheckLinks[] = $iLinkId; } foreach ($this->aModified as $iLinkId => $oLink) { $aCheckLinks[] = $oLink->GetKey(); } // Critical section : serialize any write access to these links // $oMtx = new iTopMutex('Write-'.$this->sClass); $oMtx->Lock(); // Check for the existing links // if (count($aCheckLinks) > 0) { $oSearch = new DBObjectSearch($this->sClass); $oSearch->AddCondition('id', $aCheckLinks, 'IN'); $oSet = new DBObjectSet($oSearch); /** @var DBObject[] $aExistingLinks */ $aExistingLinks = $oSet->ToArray(); } // Check for the existing remote objects // if (count($aCheckRemote) > 0) { $oSearch = new DBObjectSearch($this->sClass); $oSearch->AddCondition($sExtKeyToMe, $oHostObject->GetKey(), '='); $oSearch->AddCondition($sExtKeyToRemote, $aCheckRemote, 'IN'); $oSet = new DBObjectSet($oSearch); /** @var Int[] $aExistingRemote */ $aExistingRemote = $oSet->GetColumnAsArray($sExtKeyToRemote); } // Write the links according to the existing links // foreach ($this->aAdded as $oLink) { // Make sure that the objects in the set point to "this" $oLink->Set($sExtKeyToMe, $oHostObject->GetKey()); if ($oLink->IsNew()) { if (count($aCheckRemote) > 0) { if (in_array($oLink->Get($sExtKeyToRemote), $aExistingRemote)) { // Do not create a duplicate continue; } } } else { if (!array_key_exists($oLink->GetKey(), $aExistingLinks)) { $oLink->DBClone(); } } $oLink->DBWrite(); } foreach ($this->aRemoved as $iLinkId) { if (array_key_exists($iLinkId, $aExistingLinks)) { $oLink = $aExistingLinks[$iLinkId]; if ($oAttDef->IsIndirect()) { $oLink->DBDelete(); } else { $oExtKeyToRemote = MetaModel::GetAttributeDef($this->sClass, $sExtKeyToMe); if ($oExtKeyToRemote->IsNullAllowed()) { if ($oLink->Get($sExtKeyToMe) == $oHostObject->GetKey()) { // Detach the link object from this $oLink->Set($sExtKeyToMe, 0); $oLink->DBUpdate(); } } else { $oLink->DBDelete(); } } } } // Note: process modifications at the end: if a link to remove has also been listed as modified, then it will be gracefully ignored foreach ($this->aModified as $iLinkId => $oLink) { if (array_key_exists($oLink->GetKey(), $aExistingLinks)) { $oLink->DBUpdate(); } else { $oLink->DBClone(); } } // End of the critical section // $oMtx->Unlock(); } }