diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php
index f69a3a5f5..590495ded 100644
--- a/application/cmdbabstract.class.inc.php
+++ b/application/cmdbabstract.class.inc.php
@@ -190,6 +190,25 @@ abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
const MAX_UPDATE_LOOP_COUNT = 10;
+ /**
+ * @var array First level classname, second level id, value number of calls done
+ * @used-by static::RegisterObjectAwaitingEventDbLinksChanged()
+ * @used-by static::RemoveObjectAwaitingEventDbLinksChanged()
+ *
+ */
+ protected static array $aObjectsAwaitingEventDbLinksChanged = [];
+
+ /**
+ * @var bool Flag to allow/block the Event when DBLink are changed
+ * This is used to avoid sending too many events when doing mass-update
+ *
+ * When this flag is set to true, the object registration for links modification is done
+ * but the event is not fired.
+ *
+ * @since 3.1.0 N°5906
+ */
+ protected static bool $bBlockEventDBLinksChanged = false;
+
/**
* Constructor from a row of data (as a hash 'attcode' => value)
@@ -4425,17 +4444,23 @@ HTML;
*/
public function DBInsertNoReload()
{
- $res = parent::DBInsertNoReload();
+ try {
+ $res = parent::DBInsertNoReload();
- $this->SetWarningsAsSessionMessages('create');
+ $this->SetWarningsAsSessionMessages('create');
- // Invoke extensions after insertion (the object must exist, have an id, etc.)
- /** @var \iApplicationObjectExtension $oExtensionInstance */
- foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
- {
- $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
+ // Invoke extensions after insertion (the object must exist, have an id, etc.)
+ /** @var \iApplicationObjectExtension $oExtensionInstance */
+ foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) {
+ $oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
+ }
+ } finally {
+ if (static::IsCrudStackEmpty()) {
+ // Avoid signaling the current object that links were modified
+ static::RemoveObjectAwaitingEventDbLinksChanged(get_class($this), $this->GetKey());
+ static::FireEventDbLinksChangedForAllObjects();
+ }
}
-
return $res;
}
@@ -4464,48 +4489,53 @@ HTML;
public function DBUpdate()
{
- $res = parent::DBUpdate();
+ try {
+ $res = parent::DBUpdate();
- $this->SetWarningsAsSessionMessages('update');
+ $this->SetWarningsAsSessionMessages('update');
- // Protection against reentrance (e.g. cascading the update of ticket logs)
- // Note: This is based on the fix made on r 3190 in DBObject::DBUpdate()
- if (!MetaModel::StartReentranceProtection($this)) {
- $sClass = get_class($this);
- $sKey = $this->GetKey();
- IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Rejected (reentrance)", LogChannels::DM_CRUD);
-
- return $res;
- }
-
- try
- {
- // Invoke extensions after the update (could be before)
- /** @var \iApplicationObjectExtension $oExtensionInstance */
- foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
- {
- $oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
- }
- }
- finally
- {
- MetaModel::StopReentranceProtection($this);
- }
-
- $aChanges = $this->ListChanges();
- if (count($aChanges) != 0) {
- $this->iUpdateLoopCount++;
- if ($this->iUpdateLoopCount > self::MAX_UPDATE_LOOP_COUNT) {
+ // Protection against reentrance (e.g. cascading the update of ticket logs)
+ // Note: This is based on the fix made on r 3190 in DBObject::DBUpdate()
+ if (!MetaModel::StartReentranceProtection($this)) {
$sClass = get_class($this);
$sKey = $this->GetKey();
- $aPlugins = [];
+ IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Rejected (reentrance)", LogChannels::DM_CRUD);
+
+ return $res;
+ }
+
+ try {
+ // Invoke extensions after the update (could be before)
+ /** @var \iApplicationObjectExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) {
- $aPlugins[] = get_class($oExtensionInstance);
+ $oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
}
- $sPlugins = implode(', ', $aPlugins);
- IssueLog::Error("CRUD: DBUpdate $sClass::$sKey Update loop detected plugins: $sPlugins", LogChannels::DM_CRUD);
- } else {
- return $this->DBUpdate();
+ }
+ finally {
+ MetaModel::StopReentranceProtection($this);
+ }
+
+ $aChanges = $this->ListChanges();
+ if (count($aChanges) != 0) {
+ $this->iUpdateLoopCount++;
+ if ($this->iUpdateLoopCount > self::MAX_UPDATE_LOOP_COUNT) {
+ $sClass = get_class($this);
+ $sKey = $this->GetKey();
+ $aPlugins = [];
+ foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) {
+ $aPlugins[] = get_class($oExtensionInstance);
+ }
+ $sPlugins = implode(', ', $aPlugins);
+ IssueLog::Error("CRUD: DBUpdate $sClass::$sKey Update loop detected plugins: $sPlugins", LogChannels::DM_CRUD);
+ } else {
+ return $this->DBUpdate();
+ }
+ }
+ } finally {
+ if (static::IsCrudStackEmpty()) {
+ // Avoid signaling the current object that links were modified
+ static::RemoveObjectAwaitingEventDbLinksChanged(get_class($this), $this->GetKey());
+ static::FireEventDbLinksChangedForAllObjects();
}
}
@@ -4529,7 +4559,22 @@ HTML;
}
}
- protected function DBDeleteTracked_Internal(&$oDeletionPlan = null)
+ public function DBDelete(&$oDeletionPlan = null)
+ {
+ try {
+ parent::DBDelete($oDeletionPlan);
+ } finally {
+ if (static::IsCrudStackEmpty()) {
+ // Avoid signaling the current object that links were modified
+ static::RemoveObjectAwaitingEventDbLinksChanged(get_class($this), $this->GetKey());
+ static::FireEventDbLinksChangedForAllObjects();
+ }
+ }
+
+ return $oDeletionPlan;
+ }
+
+ protected function DBDeleteTracked_Internal(&$oDeletionPlan = null)
{
// Invoke extensions before the deletion (the deletion will do some cleanup and we might loose some information
/** @var \iApplicationObjectExtension $oExtensionInstance */
@@ -5197,6 +5242,9 @@ EOF
}
utils::RemoveTransaction($sTransactionId);
}
+
+ // Avoid too many events
+ static::SetEventDBLinksChangedBlocked(true);
$iPreviousTimeLimit = ini_get('max_execution_time');
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
foreach ($aSelectedObj as $iId) {
@@ -5226,6 +5274,10 @@ EOF
$oObj->DBUpdate();
}
}
+ // Send all the retained events for further computations
+ static::SetEventDBLinksChangedBlocked(false);
+ static::FireEventDbLinksChangedForAllObjects();
+
set_time_limit(intval($iPreviousTimeLimit));
$oTable = DataTableUIBlockFactory::MakeForForm('BulkModify', $aHeaders, $aRows);
$oTable->AddOption("bFullscreen", true);
@@ -5296,13 +5348,20 @@ EOF
{
$oDeletionPlan = new DeletionPlan();
- foreach($aObjects as $oObj)
- {
- if ($bPreview) {
- $oObj->CheckToDelete($oDeletionPlan);
- } else {
- $oObj->DBDelete($oDeletionPlan);
+ // Avoid too many events
+ static::SetEventDBLinksChangedBlocked(true);
+ try {
+ foreach ($aObjects as $oObj) {
+ if ($bPreview) {
+ $oObj->CheckToDelete($oDeletionPlan);
+ } else {
+ $oObj->DBDelete($oDeletionPlan);
+ }
}
+ } finally {
+ // Send all the retained events for further computations
+ static::SetEventDBLinksChangedBlocked(false);
+ static::FireEventDbLinksChangedForAllObjects();
}
if ($bPreview) {
@@ -5733,7 +5792,9 @@ JS
///
/**
- * @inheritDoc
+ * @return void
+ * @throws \CoreException
+ *
* @since 3.1.0
*/
final protected function FireEventCheckToWrite(): void
@@ -5742,11 +5803,14 @@ JS
}
/**
- * @inheritDoc
+ * @return void
+ * @throws \CoreException
+ *
* @since 3.1.0
*/
final protected function FireEventCreateDone(): void
{
+ $this->NotifyAttachedObjectsOnLinkClassModification();
$this->FireEvent(EVENT_DB_CREATE_DONE);
}
@@ -5755,11 +5819,16 @@ JS
///
/**
- * @inheritDoc
+ * @param array $aChanges
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
* @since 3.1.0
*/
final protected function FireEventUpdateDone(array $aChanges): void
{
+ $this->NotifyAttachedObjectsOnLinkClassModification();
$this->FireEvent(EVENT_DB_UPDATE_DONE, ['changes' => $aChanges]);
}
@@ -5768,7 +5837,10 @@ JS
///
/**
- * @inheritDoc
+ * @param \DeletionPlan $oDeletionPlan
+ *
+ * @return void
+ * @throws \CoreException
* @since 3.1.0
*/
final protected function FireEventCheckToDelete(DeletionPlan $oDeletionPlan): void
@@ -5777,14 +5849,184 @@ JS
}
/**
- * @inheritDoc
+ * @return void
+ * @throws \CoreException
+ *
* @since 3.1.0
*/
final protected function FireEventDeleteDone(): void
{
+ $this->NotifyAttachedObjectsOnLinkClassModification();
$this->FireEvent(EVENT_DB_DELETE_DONE);
}
+ /**
+ * If the passed object is an instance of a link class, then will register each remote object for modification using {@see static::RegisterObjectAwaitingEventDbLinksChanged()}
+ * If an external key was modified, register also the previous object that was linked previously.
+ *
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ * @throws \Exception
+ *
+ * @since 3.1.0 N°5906
+ */
+ final protected function NotifyAttachedObjectsOnLinkClassModification(): void
+ {
+ $sClass = get_class($this);
+ if (false === MetaModel::IsLinkClass($sClass)) {
+ return;
+ }
+ // previous values in case of link change
+ $aPreviousValues = $this->ListPreviousValuesForUpdatedAttributes();
+
+ $aLnkClassExternalKeys = MetaModel::GetAttributesList($sClass, [AttributeExternalKey::class]);
+ foreach ($aLnkClassExternalKeys as $sExternalKeyAttCode) {
+ /** @var \AttributeExternalKey $oExternalKeyAttDef */
+ $oExternalKeyAttDef = MetaModel::GetAttributeDef($sClass, $sExternalKeyAttCode);
+ $sRemoteClassName = $oExternalKeyAttDef->GetTargetClass();
+
+ $sRemoteObjectId = $this->Get($sExternalKeyAttCode);
+ if ($sRemoteObjectId > 0) {
+ self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sRemoteObjectId);
+ }
+
+ $sPreviousRemoteObjectId = $aPreviousValues[$sExternalKeyAttCode] ?? 0;
+ if ($sPreviousRemoteObjectId > 0) {
+ self::RegisterObjectAwaitingEventDbLinksChanged($sRemoteClassName, $sPreviousRemoteObjectId);
+ }
+ }
+ }
+
+ /**
+ * Register one object for later EVENT_DB_LINKS_CHANGED event.
+ *
+ * @param string $sClass
+ * @param string|int|null $sId
+ *
+ * @since 3.1.0 N°5906
+ */
+ private static function RegisterObjectAwaitingEventDbLinksChanged(string $sClass, $sId): void
+ {
+ if (isset(self::$aObjectsAwaitingEventDbLinksChanged[$sClass][$sId])) {
+ self::$aObjectsAwaitingEventDbLinksChanged[$sClass][$sId]++;
+ } else {
+ self::$aObjectsAwaitingEventDbLinksChanged[$sClass][$sId] = 1;
+ }
+ }
+
+ /**
+ * Fire the EVENT_DB_LINKS_CHANGED event if current object is registered
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ *
+ * @since 3.1.0 N°5906
+ */
+ final protected function FireEventDbLinksChangedForCurrentObject(): void
+ {
+ if (true === static::IsEventDBLinksChangedBlocked()) {
+ return;
+ }
+
+ $sClass = get_class($this);
+ $sId = $this->GetKey();
+ self::FireEventDbLinksChangedForClassId($sClass, $sId);
+ }
+
+ /**
+ * Fire the EVENT_DB_LINKS_CHANGED event if given object is registered, and unregister it
+ *
+ * @param string $sClass
+ * @param string|int|null $sId
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ */
+ private static function FireEventDbLinksChangedForClassId(string $sClass, $sId): void
+ {
+ if (true === static::IsEventDBLinksChangedBlocked()) {
+ return;
+ }
+
+ $bIsObjectAwaitingEventDbLinksChanged = self::RemoveObjectAwaitingEventDbLinksChanged($sClass, $sId);
+ if (false === $bIsObjectAwaitingEventDbLinksChanged) {
+ return;
+ }
+
+ $oObject = MetaModel::GetObject($sClass, $sId);
+ $oObject->FireEvent(EVENT_DB_LINKS_CHANGED);
+
+ // The event listeners might have generated new lnk instances pointing to this object, so removing object from stack to avoid reentrance
+ self::RemoveObjectAwaitingEventDbLinksChanged($sClass, $sId);
+ }
+
+ /**
+ * Remove the registration of an object concerning the EVENT_DB_LINKS_CHANGED event
+ *
+ * @param string $sClass
+ * @param string|int|null $sId
+ *
+ * @return bool true if the object [class, id] was present in the list
+ * @throws \CoreException
+ */
+ final protected static function RemoveObjectAwaitingEventDbLinksChanged(string $sClass, $sId): bool
+ {
+ $bFlagRemoved = false;
+ $aClassesHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false);
+ foreach ($aClassesHierarchy as $sClassInHierarchy) {
+ if (isset(self::$aObjectsAwaitingEventDbLinksChanged[$sClassInHierarchy][$sId])) {
+ unset(self::$aObjectsAwaitingEventDbLinksChanged[$sClassInHierarchy][$sId]);
+ $bFlagRemoved = true;
+ }
+ }
+
+ return $bFlagRemoved;
+ }
+
+ /**
+ * Fire the EVENT_DB_LINKS_CHANGED event to all the registered objects
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreException
+ *
+ * @since 3.1.0 N°5906
+ */
+ final public static function FireEventDbLinksChangedForAllObjects()
+ {
+ if (true === static::IsEventDBLinksChangedBlocked()) {
+ return;
+ }
+
+ foreach (self::$aObjectsAwaitingEventDbLinksChanged as $sClass => $aClassInstances) {
+ foreach ($aClassInstances as $sId => $iCallsNumber) {
+ self::FireEventDbLinksChangedForClassId($sClass, $sId);
+ }
+ }
+ }
+
+ /**
+ * Check if the event EVENT_DB_LINKS_CHANGED is blocked or not (for bulk operations)
+ *
+ * @return bool
+ */
+ final public static function IsEventDBLinksChangedBlocked(): bool
+ {
+ return self::$bBlockEventDBLinksChanged;
+ }
+
+ /**
+ * Block/unblock the event EVENT_DB_LINKS_CHANGED (the registration of objects on links modifications continues to work)
+ *
+ * @param bool $bBlockEventDBLinksChanged
+ */
+ final public static function SetEventDBLinksChangedBlocked(bool $bBlockEventDBLinksChanged): void
+ {
+ self::$bBlockEventDBLinksChanged = $bBlockEventDBLinksChanged;
+ }
+
/**
* @inheritDoc
* @throws \CoreException
diff --git a/application/datamodel.application.xml b/application/datamodel.application.xml
index 1f6e2d9fa..bb903757b 100644
--- a/application/datamodel.application.xml
+++ b/application/datamodel.application.xml
@@ -371,6 +371,22 @@
+
+ At least one link class was changed
+
+ cmdbAbstractObject
+
+
+
+ The object where the link is or was pointing to
+ DBObject
+
+
+ Debug string
+ string
+
+
+
An object has been re-loaded from the database
diff --git a/core/bulkchange.class.inc.php b/core/bulkchange.class.inc.php
index f4f7fbb3b..9180b5266 100644
--- a/core/bulkchange.class.inc.php
+++ b/core/bulkchange.class.inc.php
@@ -1160,136 +1160,112 @@ class BulkChange
}
$iPreviousTimeLimit = ini_get('max_execution_time');
$iLoopTimeLimit = MetaModel::GetConfig()->Get('max_execution_time_per_loop');
- foreach($this->m_aData as $iRow => $aRowData)
- {
- set_time_limit(intval($iLoopTimeLimit));
- if (isset($aResult[$iRow]["__STATUS__"]))
- {
- // An issue at the earlier steps - skip the rest
- continue;
- }
- try
- {
- $oReconciliationFilter = new DBObjectSearch($this->m_sClass);
- $bSkipQuery = false;
- foreach($this->m_aReconcilKeys as $sAttCode)
- {
- $valuecondition = null;
- if (array_key_exists($sAttCode, $this->m_aExtKeys))
- {
- if ($this->IsNullExternalKeySpec($aRowData, $sAttCode))
- {
- $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
- if ($oExtKey->IsNullAllowed())
- {
- $valuecondition = $oExtKey->GetNullValue();
- $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
- }
- else
- {
- $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
- }
- }
- else
- {
- // The value has to be found or verified
- /** var DBObjectSearch $oReconFilter */
- list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
+ // Avoid too many events
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+ try {
+ foreach ($this->m_aData as $iRow => $aRowData) {
+ set_time_limit(intval($iLoopTimeLimit));
+ if (isset($aResult[$iRow]["__STATUS__"])) {
+ // An issue at the earlier steps - skip the rest
+ continue;
+ }
+ try {
+ $oReconciliationFilter = new DBObjectSearch($this->m_sClass);
+ $bSkipQuery = false;
+ foreach ($this->m_aReconcilKeys as $sAttCode) {
+ $valuecondition = null;
+ if (array_key_exists($sAttCode, $this->m_aExtKeys)) {
+ if ($this->IsNullExternalKeySpec($aRowData, $sAttCode)) {
+ $oExtKey = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ if ($oExtKey->IsNullAllowed()) {
+ $valuecondition = $oExtKey->GetNullValue();
+ $aResult[$iRow][$sAttCode] = new CellStatus_Void($oExtKey->GetNullValue());
+ } else {
+ $aResult[$iRow][$sAttCode] = new CellStatus_NullIssue();
+ }
+ } else {
+ // The value has to be found or verified
- if (count($aMatches) == 1)
- {
- $oRemoteObj = reset($aMatches); // first item
- $valuecondition = $oRemoteObj->GetKey();
- $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
+ /** var DBObjectSearch $oReconFilter */
+ list($oReconFilter, $aMatches) = $this->ResolveExternalKey($aRowData, $sAttCode, $aResult[$iRow]);
+
+ if (count($aMatches) == 1) {
+ $oRemoteObj = reset($aMatches); // first item
+ $valuecondition = $oRemoteObj->GetKey();
+ $aResult[$iRow][$sAttCode] = new CellStatus_Void($oRemoteObj->GetKey());
+ } elseif (count($aMatches) == 0) {
+ $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
+ $aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
+ } else {
+ $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
+ }
}
- elseif (count($aMatches) == 0)
- {
- $oCellStatus_SearchIssue = $this->GetCellSearchIssue($oReconFilter);
- $aResult[$iRow][$sAttCode] = $oCellStatus_SearchIssue;
- }
- else
- {
- $aResult[$iRow][$sAttCode] = new CellStatus_Ambiguous(null, count($aMatches), $oReconFilter->serialize());
+ } else {
+ // The value is given in the data row
+ $iCol = $this->m_aAttList[$sAttCode];
+ if ($sAttCode == 'id') {
+ $valuecondition = $aRowData[$iCol];
+ } else {
+ $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
+ $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
}
}
- }
- else
- {
- // The value is given in the data row
- $iCol = $this->m_aAttList[$sAttCode];
- if ($sAttCode == 'id')
- {
- $valuecondition = $aRowData[$iCol];
- }
- else
- {
- $oAttDef = MetaModel::GetAttributeDef($this->m_sClass, $sAttCode);
- $valuecondition = $oAttDef->MakeValueFromString($aRowData[$iCol], $this->m_bLocalizedValues);
+ if (is_null($valuecondition)) {
+ $bSkipQuery = true;
+ } else {
+ $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
}
}
- if (is_null($valuecondition))
- {
- $bSkipQuery = true;
- }
- else
- {
- $oReconciliationFilter->AddCondition($sAttCode, $valuecondition, '=');
+ if ($bSkipQuery) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation'));
+ } else {
+ $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
+ switch ($oReconciliationSet->Count()) {
+ case 0:
+ $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
+ // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
+ $aVisited[] = $oTargetObj->GetKey();
+ break;
+ case 1:
+ $oTargetObj = $oReconciliationSet->Fetch();
+ $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
+ // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
+ if (!is_null($this->m_sSynchroScope)) {
+ $aVisited[] = $oTargetObj->GetKey();
+ }
+ break;
+ default:
+ // Found several matches, ambiguous
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
+ $aResult[$iRow]["id"] = new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
+ $aResult[$iRow]["finalclass"] = 'n/a';
+ }
}
+ } catch (Exception $e) {
+ $aResult[$iRow]["__STATUS__"] = new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage()));
}
- if ($bSkipQuery)
- {
- $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Reconciliation'));
- }
- else
- {
- $oReconciliationSet = new CMDBObjectSet($oReconciliationFilter);
- switch($oReconciliationSet->Count())
- {
- case 0:
- $oTargetObj = $this->CreateObject($aResult, $iRow, $aRowData, $oChange);
- // $aResult[$iRow]["__STATUS__"]=> set in CreateObject
- $aVisited[] = $oTargetObj->GetKey();
- break;
- case 1:
- $oTargetObj = $oReconciliationSet->Fetch();
- $this->UpdateObject($aResult, $iRow, $oTargetObj, $aRowData, $oChange);
- // $aResult[$iRow]["__STATUS__"]=> set in UpdateObject
- if (!is_null($this->m_sSynchroScope))
- {
- $aVisited[] = $oTargetObj->GetKey();
- }
- break;
- default:
- // Found several matches, ambiguous
- $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::S('UI:CSVReport-Row-Issue-Ambiguous'));
- $aResult[$iRow]["id"]= new CellStatus_Ambiguous(0, $oReconciliationSet->Count(), $oReconciliationFilter->serialize());
- $aResult[$iRow]["finalclass"]= 'n/a';
+ }
+
+ if (!is_null($this->m_sSynchroScope)) {
+ // Compute the delta between the scope and visited objects
+ $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
+ $oScopeSet = new DBObjectSet($oScopeSearch);
+ while ($oObj = $oScopeSet->Fetch()) {
+ $iObj = $oObj->GetKey();
+ if (!in_array($iObj, $aVisited)) {
+ set_time_limit(intval($iLoopTimeLimit));
+ $iRow++;
+ $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
}
}
}
- catch (Exception $e)
- {
- $aResult[$iRow]["__STATUS__"]= new RowStatus_Issue(Dict::Format('UI:CSVReport-Row-Issue-Internal', get_class($e), $e->getMessage()));
- }
+ } finally {
+ // Send all the retained events for further computations
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
+ cmdbAbstractObject::FireEventDbLinksChangedForAllObjects();
}
- if (!is_null($this->m_sSynchroScope))
- {
- // Compute the delta between the scope and visited objects
- $oScopeSearch = DBObjectSearch::FromOQL($this->m_sSynchroScope);
- $oScopeSet = new DBObjectSet($oScopeSearch);
- while ($oObj = $oScopeSet->Fetch())
- {
- $iObj = $oObj->GetKey();
- if (!in_array($iObj, $aVisited))
- {
- set_time_limit(intval($iLoopTimeLimit));
- $iRow++;
- $this->UpdateMissingObject($aResult, $iRow, $oObj, $oChange);
- }
- }
- }
set_time_limit(intval($iPreviousTimeLimit));
// Fill in the blanks - the result matrix is expected to be 100% complete
diff --git a/core/dbobject.class.php b/core/dbobject.class.php
index 17a794e92..faed30457 100644
--- a/core/dbobject.class.php
+++ b/core/dbobject.class.php
@@ -156,6 +156,18 @@ abstract class DBObject implements iDisplay
/** @var \DBObject Source object when updating links */
protected $m_oLinkHostObject = null;
+ /**
+ * @var array List all the CRUD stack in progress
+ *
+ * The array contains instances of
+ * ['type' => 'type of CRUD operation (INSERT, UPDATE, DELETE)',
+ * 'class' => 'class of the object in the CRUD process',
+ * 'id' => 'id of the object in the CRUD process']
+ *
+ * @since 3.1.0 N°5906
+ */
+ protected static array $m_aCrudStack = [];
+
/**
* DBObject constructor.
*
@@ -2952,148 +2964,161 @@ abstract class DBObject implements iDisplay
public function DBInsertNoReload()
{
$sClass = get_class($this);
- if (MetaModel::DBIsReadOnly())
- {
- $sErrorMessage = "Cannot Insert object of class '$sClass' because of an ongoing maintenance: the database is in ReadOnly mode";
- IssueLog::Error("$sErrorMessage\n".MyHelpers::get_callstack_text(1));
- throw new CoreException("$sErrorMessage (see the log for more information)");
- }
-
- if ($this->m_bIsInDB) {
- throw new CoreException('The object already exists into the Database, you may want to use the clone function');
- }
-
- $sClass = get_class($this);
- $sRootClass = MetaModel::GetRootClass($sClass);
-
- // Ensure the update of the values (we are accessing the data directly)
- $this->DoComputeValues();
- $this->OnInsert();
-
- // If not automatically computed, then check that the key is given by the caller
- if (!MetaModel::IsAutoIncrementKey($sRootClass)) {
- if (empty($this->m_iKey)) {
- throw new CoreWarning('Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber', array('class' => $sRootClass));
- }
- }
-
- list($bRes, $aIssues) = $this->CheckToWrite(false);
- if (!$bRes) {
- throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
- }
-
- if ($this->m_iKey < 0) {
- // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it!
- $this->m_iKey = null;
- }
-
- $this->ComputeStopWatchesDeadline(true);
-
- $iTransactionRetry = 1;
- $bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
- if ($bIsTransactionEnabled) {
- // TODO Deep clone this object before the transaction (to use it in case of rollback)
- // $iTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
- $iTransactionRetryCount = 1;
- $iTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
- $iTransactionRetry = $iTransactionRetryCount;
- }
- while ($iTransactionRetry > 0) {
- try {
- $iTransactionRetry--;
- if ($bIsTransactionEnabled) {
- CMDBSource::Query('START TRANSACTION');
- }
-
- // First query built upon on the root class, because the ID must be created first
- $this->m_iKey = $this->DBInsertSingleTable($sRootClass);
-
- // Then do the leaf class, if different from the root class
- if ($sClass != $sRootClass) {
- $this->DBInsertSingleTable($sClass);
- }
-
- // Then do the other classes
- foreach (MetaModel::EnumParentClasses($sClass) as $sParentClass) {
- if ($sParentClass == $sRootClass) {
- continue;
- }
- $this->DBInsertSingleTable($sParentClass);
- }
-
- $this->OnObjectKeyReady();
-
- $this->DBWriteLinks();
- $this->WriteExternalAttributes();
-
- // Write object creation history within the transaction
- $this->RecordObjCreation();
-
- if ($bIsTransactionEnabled) {
- CMDBSource::Query('COMMIT');
- }
- break;
- }
- catch (Exception $e) {
- IssueLog::Error($e->getMessage());
- if ($bIsTransactionEnabled) {
- CMDBSource::Query('ROLLBACK');
- if (!CMDBSource::IsInsideTransaction() && CMDBSource::IsDeadlockException($e)) {
- // Deadlock found when trying to get lock; try restarting transaction (only in main transaction)
- if ($iTransactionRetry > 0) {
- // wait and retry
- IssueLog::Error('Insert TRANSACTION Retrying...');
- usleep(random_int(1, 5) * 1000 * $iTransactionRetryDelay * ($iTransactionRetryCount - $iTransactionRetry));
- continue;
- } else {
- IssueLog::Error('Insert Deadlock TRANSACTION prevention failed.');
- }
- }
- }
- throw $e;
- }
- }
-
- $this->m_bIsInDB = true;
- $this->m_bDirty = false;
- foreach ($this->m_aCurrValues as $sAttCode => $value) {
- if (is_object($value)) {
- $value = clone $value;
- }
- $this->m_aOrigValues[$sAttCode] = $value;
- }
-
- // Prevent DBUpdate at this point (reentrance protection)
- MetaModel::StartReentranceProtection($this);
+ $this->AddCurrentObjectInCrudStack('INSERT');
try {
- $this->FireEventCreateDone();
- $this->AfterInsert();
+ if (MetaModel::DBIsReadOnly())
+ {
+ $sErrorMessage = "Cannot Insert object of class '$sClass' because of an ongoing maintenance: the database is in ReadOnly mode";
+
+ IssueLog::Error("$sErrorMessage\n".MyHelpers::get_callstack_text(1));
+ throw new CoreException("$sErrorMessage (see the log for more information)");
+ }
+
+ if ($this->m_bIsInDB) {
+ throw new CoreException('The object already exists into the Database, you may want to use the clone function');
+ }
- // Activate any existing trigger
$sClass = get_class($this);
- $aParams = array('class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
- $oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN (:class_list)'), array(), $aParams);
- while ($oTrigger = $oSet->Fetch()) {
- /** @var \Trigger $oTrigger */
- try {
- $oTrigger->DoActivate($this->ToArgs('this'));
- }
- catch (Exception $e) {
- utils::EnrichRaisedException($oTrigger, $e);
+ $sRootClass = MetaModel::GetRootClass($sClass);
+
+ // Ensure the update of the values (we are accessing the data directly)
+ $this->DoComputeValues();
+ $this->OnInsert();
+ if ($this->m_iKey < 0) {
+ // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it!
+ $this->m_iKey = null;
+ $this->UpdateCurrentObjectInCrudStack();
+ }
+ // If not automatically computed, then check that the key is given by the caller
+ if (!MetaModel::IsAutoIncrementKey($sRootClass)) {
+ if (empty($this->m_iKey)) {
+ throw new CoreWarning('Missing key for the object to write - This class is supposed to have a user defined key, not an autonumber', array('class' => $sRootClass));
}
}
- // - TriggerOnObjectMention
- $this->ActivateOnMentionTriggers(true);
+ list($bRes, $aIssues) = $this->CheckToWrite(false);
+ if (!$bRes) {
+ throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
+ }
+
+ if ($this->m_iKey < 0) {
+ // This was a temporary "memory" key: discard it so that DBInsertSingleTable will not try to use it!
+ $this->m_iKey = null;
+ }
+
+ $this->ComputeStopWatchesDeadline(true);
+
+ $iTransactionRetry = 1;
+ $bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
+ if ($bIsTransactionEnabled) {
+ // TODO Deep clone this object before the transaction (to use it in case of rollback)
+ // $iTransactionRetryCount = MetaModel::GetConfig()->Get('db_core_transactions_retry_count');
+ $iTransactionRetryCount = 1;
+ $iTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
+ $iTransactionRetry = $iTransactionRetryCount;
+ }
+ while ($iTransactionRetry > 0) {
+ try {
+ $iTransactionRetry--;
+ if ($bIsTransactionEnabled) {
+ CMDBSource::Query('START TRANSACTION');
+ }
+
+ // First query built upon on the root class, because the ID must be created first
+ $this->m_iKey = $this->DBInsertSingleTable($sRootClass);
+
+ // Then do the leaf class, if different from the root class
+ if ($sClass != $sRootClass) {
+ $this->DBInsertSingleTable($sClass);
+ }
+
+ // Then do the other classes
+ foreach (MetaModel::EnumParentClasses($sClass) as $sParentClass) {
+ if ($sParentClass == $sRootClass) {
+ continue;
+ }
+ $this->DBInsertSingleTable($sParentClass);
+ }
+
+ $this->OnObjectKeyReady();
+ $this->UpdateCurrentObjectInCrudStack();
+
+ $this->DBWriteLinks();
+ $this->WriteExternalAttributes();
+
+ // Write object creation history within the transaction
+ $this->RecordObjCreation();
+
+ if ($bIsTransactionEnabled) {
+ CMDBSource::Query('COMMIT');
+ }
+ break;
+ }
+ catch (Exception $e) {
+ IssueLog::Error($e->getMessage());
+ if ($bIsTransactionEnabled) {
+ CMDBSource::Query('ROLLBACK');
+ if (!CMDBSource::IsInsideTransaction() && CMDBSource::IsDeadlockException($e)) {
+ // Deadlock found when trying to get lock; try restarting transaction (only in main transaction)
+ if ($iTransactionRetry > 0) {
+ // wait and retry
+ IssueLog::Error('Insert TRANSACTION Retrying...');
+ usleep(random_int(1, 5) * 1000 * $iTransactionRetryDelay * ($iTransactionRetryCount - $iTransactionRetry));
+ continue;
+ } else {
+ IssueLog::Error('Insert Deadlock TRANSACTION prevention failed.');
+ }
+ }
+ }
+ throw $e;
+ }
+ }
+
+ $this->m_bIsInDB = true;
+ $this->m_bDirty = false;
+ foreach ($this->m_aCurrValues as $sAttCode => $value) {
+ if (is_object($value)) {
+ $value = clone $value;
+ }
+ $this->m_aOrigValues[$sAttCode] = $value;
+ }
+
+ // Prevent DBUpdate at this point (reentrance protection)
+ MetaModel::StartReentranceProtection($this);
+
+ try {
+ $this->FireEventCreateDone();
+ $this->AfterInsert();
+
+ // Activate any existing trigger
+ $sClass = get_class($this);
+ $aParams = array('class_list' => MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL));
+ $oSet = new DBObjectSet(DBObjectSearch::FromOQL('SELECT TriggerOnObjectCreate AS t WHERE t.target_class IN (:class_list)'), array(), $aParams);
+ while ($oTrigger = $oSet->Fetch()) {
+ /** @var \Trigger $oTrigger */
+ try {
+ $oTrigger->DoActivate($this->ToArgs('this'));
+ }
+ catch (Exception $e) {
+ utils::EnrichRaisedException($oTrigger, $e);
+ }
+ }
+
+ // - TriggerOnObjectMention
+ $this->ActivateOnMentionTriggers(true);
+ }
+ finally {
+ MetaModel::StopReentranceProtection($this);
+ }
+
+ if ($this->IsModified()) {
+ $this->DBUpdate();
+ }
}
finally {
- MetaModel::StopReentranceProtection($this);
- }
-
- if ($this->IsModified()) {
- $this->DBUpdate();
+ $this->RemoveCurrentObjectInCrudStack();
}
return $this->m_iKey;
@@ -3167,8 +3192,9 @@ abstract class DBObject implements iDisplay
return false;
}
- try
- {
+ $this->AddCurrentObjectInCrudStack('UPDATE');
+
+ try {
$this->DoComputeValues();
$this->ComputeStopWatchesDeadline(false);
$this->OnUpdate();
@@ -3391,6 +3417,7 @@ abstract class DBObject implements iDisplay
finally
{
MetaModel::StopReentranceProtection($this);
+ $this->RemoveCurrentObjectInCrudStack();
}
if ($this->IsModified() || $bModifiedByUpdateDone) {
@@ -3781,7 +3808,14 @@ abstract class DBObject implements iDisplay
if ($oToDelete->m_bIsInDB)
{
set_time_limit(intval($iLoopTimeLimit));
- $oToDelete->DBDeleteSingleObject();
+
+ $oToDelete->AddCurrentObjectInCrudStack('DELETE');
+ try {
+ $oToDelete->DBDeleteSingleObject();
+ }
+ finally {
+ $oToDelete->RemoveCurrentObjectInCrudStack();
+ }
}
}
}
@@ -5966,5 +6000,106 @@ abstract class DBObject implements iDisplay
protected function FireEventUnArchive(): void
{
}
+
+ //////////////
+ /// CRUD stack in progress
+ ///
+
+ /**
+ * Check if an object is currently involved in CRUD operation
+ *
+ * @param string $sClass
+ * @param string|null $sId
+ *
+ * @return bool
+ * @since 3.1.0 N°5609
+ */
+ final public static function IsObjectCurrentlyInCrud(string $sClass, ?string $sId): bool
+ {
+ // during insert key is reset from -1 to null
+ // so we need to handle null values (will give empty string after conversion)
+ $sConvertedId = (string)$sId;
+
+ foreach (self::$m_aCrudStack as $aCrudStackEntry) {
+ if (($sClass === $aCrudStackEntry['class'])
+ && ($sConvertedId === $aCrudStackEntry['id'])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if an object of the given class is currently involved in CRUD operation
+ *
+ * @param string $sClass
+ *
+ * @return bool
+ * @since 3.1.0 N°5609
+ */
+ final public static function IsClassCurrentlyInCrud(string $sClass): bool
+ {
+ foreach (self::$m_aCrudStack as $aCrudStackEntry) {
+ if ($sClass === $aCrudStackEntry['class']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add the current object to the CRUD stack
+ *
+ * @param string $sCrudType
+ *
+ * @return void
+ * @since 3.1.0 N°5609
+ */
+ private function AddCurrentObjectInCrudStack(string $sCrudType): void
+ {
+ self::$m_aCrudStack[] = [
+ 'type' => $sCrudType,
+ 'class' => get_class($this),
+ 'id' => (string)$this->GetKey(), // GetKey() doesn't have type hinting, so forcing type to avoid getting an int
+ ];
+ }
+
+ /**
+ * Update the last entry of the CRUD stack with the information of the current object
+ * This is calls during DBInsert since the object id changes
+ *
+ * @return void
+ * @since 3.1.0 N°5609
+ */
+ private function UpdateCurrentObjectInCrudStack(): void
+ {
+ $aCurrentCrudStack = array_pop(self::$m_aCrudStack);
+ $aCurrentCrudStack['id'] = (string)$this->GetKey();
+ self::$m_aCrudStack[] = $aCurrentCrudStack;
+ }
+
+ /**
+ * Remove the last entry of the CRUD stack
+ *
+ * @return void
+ * @since 3.1.0 N°5609
+ */
+ private function RemoveCurrentObjectInCrudStack(): void
+ {
+ array_pop(self::$m_aCrudStack);
+ }
+
+ /**
+ * Check if there are objects in the CRUD stack
+ *
+ * @return bool
+ * @since 3.1.0 N°5609
+ */
+ final protected function IsCrudStackEmpty(): bool
+ {
+ return count(self::$m_aCrudStack) === 0;
+ }
}
diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
index a7e7c56f5..11292bff2 100755
--- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
+++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
@@ -220,7 +220,26 @@
false
+
+
+ EVENT_DB_LINKS_CHANGED
+ UpdateTicketImpactedItems
+ 0
+
+
+
+
+ false
+ public
+ EventListener
+ UpdateImpactedItems();
+ $this->DBUpdate();
+}
+ ]]>
+
false
public
diff --git a/js/links/links_view_table_widget.js b/js/links/links_view_table_widget.js
index 576ed395f..461860a0b 100644
--- a/js/links/links_view_table_widget.js
+++ b/js/links/links_view_table_widget.js
@@ -36,7 +36,7 @@ $(function()
// link object deletion
CombodoLinkSetWorker.DeleteLinkedObject(this.options.link_class, sLinkedObjectKey, function (data) {
if (data.data.success === true) {
- oTrElement.remove();
+ me.$tableSettingsDialog.DataTableSettings('DoRefresh');
} else {
CombodoModal.OpenInformativeModal(data.data.error_message, 'error');
}
@@ -57,7 +57,7 @@ $(function()
// link object unlink
CombodoLinkSetWorker.DetachLinkedObject(this.options.link_class, sLinkedObjectKey, this.options.external_key_to_me, function (data) {
if (data.data.success === true) {
- oTrElement.remove();
+ me.$tableSettingsDialog.DataTableSettings('DoRefresh');
} else {
CombodoModal.OpenInformativeModal(data.data.error_message, 'error');
}
diff --git a/sources/Application/Service/Events/EventService.php b/sources/Application/Service/Events/EventService.php
index 877dd9b50..1e37ffcde 100644
--- a/sources/Application/Service/Events/EventService.php
+++ b/sources/Application/Service/Events/EventService.php
@@ -94,12 +94,8 @@ final class EventService
});
self::$aEventListeners[$sEvent] = $aEventCallbacks;
- $iTotalRegistrations = 0;
- foreach (self::$aEventListeners as $aEvent) {
- $iTotalRegistrations += count($aEvent);
- }
- $sLogEventName = "$sEvent:".self::GetSourcesAsString($sEventSource);
- EventServiceLog::Trace("Registering event '$sLogEventName' for '$sName' with id '$sId' (total $iTotalRegistrations)");
+ $sSource = self::GetSourcesAsString($sEventSource);
+ EventServiceLog::Debug("Registering Listener '$sName' for event '$sEvent' source '$sSource' from '$sModuleId'");
return $sId;
}
@@ -133,7 +129,6 @@ final class EventService
$sLogEventName = "$sEvent - ".self::GetSourcesAsString($eventSource).' '.json_encode($oEventData->GetEventData());
EventServiceLog::Trace("Fire event '$sLogEventName'");
if (!isset(self::$aEventListeners[$sEvent])) {
- EventServiceLog::DebugEvent("No listener for '$sLogEventName'", $sEvent, $eventSource);
$oKPI->ComputeStats('FireEvent', $sEvent);
return;
@@ -141,12 +136,14 @@ final class EventService
$oLastException = null;
$sLastExceptionMessage = null;
+ $bEventFired = false;
foreach (self::GetListeners($sEvent, $eventSource) as $aEventCallback) {
if (!self::MatchContext($aEventCallback['context'])) {
continue;
}
$sName = $aEventCallback['name'];
- EventServiceLog::DebugEvent("Fire event '$sLogEventName' calling '$sName'", $sEvent, $eventSource);
+ EventServiceLog::Debug("Fire event '$sLogEventName' calling '$sName'");
+ $bEventFired = true;
try {
$oEventData->SetCallbackData($aEventCallback['data']);
call_user_func($aEventCallback['callback'], $oEventData);
@@ -161,7 +158,9 @@ final class EventService
$oLastException = $e;
}
}
- EventServiceLog::DebugEvent("End of event '$sLogEventName'", $sEvent, $eventSource);
+ if ($bEventFired) {
+ EventServiceLog::Debug("End of event '$sLogEventName'");
+ }
$oKPI->ComputeStats('FireEvent', $sEvent);
if (!is_null($oLastException)) {
@@ -176,7 +175,7 @@ final class EventService
*
* @return array
*/
- public static function GetListeners(string $sEvent, $eventSource): array
+ public static function GetListeners(string $sEvent, $eventSource = null): array
{
$aListeners = [];
if (isset(self::$aEventListeners[$sEvent])) {
diff --git a/sources/Application/Service/Events/EventServiceLog.php b/sources/Application/Service/Events/EventServiceLog.php
index 771e57ef9..376f719e5 100644
--- a/sources/Application/Service/Events/EventServiceLog.php
+++ b/sources/Application/Service/Events/EventServiceLog.php
@@ -8,38 +8,8 @@ namespace Combodo\iTop\Service\Events;
use IssueLog;
use LogChannels;
-use utils;
class EventServiceLog extends IssueLog
{
const CHANNEL_DEFAULT = LogChannels::EVENT_SERVICE;
-
- /**
- * @param $sMessage
- * @param $sEvent
- * @param $sources
- *
- * @return void
- * @throws \ConfigException
- * @throws \CoreException
- */
- public static function DebugEvent($sMessage, $sEvent, $sources)
- {
- $oConfig = utils::GetConfig();
- $aLogEvents = $oConfig->Get('event_service.debug.filter_events');
- $aLogSources = $oConfig->Get('event_service.debug.filter_sources');
-
- if (is_array($aLogEvents)) {
- if (!in_array($sEvent, $aLogEvents)) {
- return;
- }
- }
- if (is_array($aLogSources)) {
- if (!EventHelper::MatchEventSource($aLogSources, $sources)) {
- return;
- }
- }
- static::Debug($sMessage);
- }
-
}
\ No newline at end of file
diff --git a/synchro/synchrodatasource.class.inc.php b/synchro/synchrodatasource.class.inc.php
index 467b36ace..570944142 100644
--- a/synchro/synchrodatasource.class.inc.php
+++ b/synchro/synchrodatasource.class.inc.php
@@ -3535,17 +3535,24 @@ class SynchroExecution
$oSetToProcess = $oSetScope;
}
- $iLastReplicaProcessed = -1;
- /** @var \SynchroReplica $oReplica */
- while ($oReplica = $oSetToProcess->Fetch())
- {
- set_time_limit(intval($iLoopTimeLimit));
- $iLastReplicaProcessed = $oReplica->GetKey();
- $this->m_oStatLog->AddTrace("Synchronizing replica id=$iLastReplicaProcessed.");
- $oReplica->Synchro($this->m_oDataSource, $this->m_aReconciliationKeys, $this->m_aAttributes, $this->m_oChange,
- $this->m_oStatLog);
- $this->m_oStatLog->AddTrace("Updating replica id=$iLastReplicaProcessed.");
- $oReplica->DBUpdate();
+ // Avoid too many events
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+ try {
+ $iLastReplicaProcessed = -1;
+ /** @var \SynchroReplica $oReplica */
+ while ($oReplica = $oSetToProcess->Fetch()) {
+ set_time_limit(intval($iLoopTimeLimit));
+ $iLastReplicaProcessed = $oReplica->GetKey();
+ $this->m_oStatLog->AddTrace("Synchronizing replica id=$iLastReplicaProcessed.");
+ $oReplica->Synchro($this->m_oDataSource, $this->m_aReconciliationKeys, $this->m_aAttributes, $this->m_oChange,
+ $this->m_oStatLog);
+ $this->m_oStatLog->AddTrace("Updating replica id=$iLastReplicaProcessed.");
+ $oReplica->DBUpdate();
+ }
+ } finally {
+ // Send all the retained events for further computations
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(false);
+ cmdbAbstractObject::FireEventDbLinksChangedForAllObjects();
}
if ($iMaxReplica)
diff --git a/tests/php-unit-tests/ItopTestCase.php b/tests/php-unit-tests/ItopTestCase.php
index eb20d234b..ea02aa365 100644
--- a/tests/php-unit-tests/ItopTestCase.php
+++ b/tests/php-unit-tests/ItopTestCase.php
@@ -155,7 +155,7 @@ class ItopTestCase extends TestCase
/**
* @param string $sObjectClass for example DBObject::class
* @param string $sMethodName
- * @param object $oObject
+ * @param ?object $oObject
* @param array $aArgs
*
* @return mixed method result
@@ -174,50 +174,79 @@ class ItopTestCase extends TestCase
}
+ /**
+ * @since 3.1.0
+ */
+ public function GetNonPublicStaticProperty(string $sClass, string $sProperty)
+ {
+ /** @noinspection OneTimeUseVariablesInspection */
+ $oProperty = $this->GetProperty($sClass, $sProperty);
+
+ return $oProperty->getValue();
+ }
+
/**
* @param object $oObject
* @param string $sProperty
*
* @return mixed property
*
- * @throws \ReflectionException
* @since 2.7.8 3.0.3 3.1.0
*/
public function GetNonPublicProperty(object $oObject, string $sProperty)
{
- $class = new \ReflectionClass(get_class($oObject));
+ /** @noinspection OneTimeUseVariablesInspection */
+ $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
+
+ return $oProperty->getValue($oObject);
+ }
+
+ /**
+ * @since 3.1.0
+ */
+ private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty
+ {
+ $class = new \ReflectionClass($sClass);
$property = $class->getProperty($sProperty);
$property->setAccessible(true);
- return $property->getValue($oObject);
+ return $property;
}
+
/**
* @param object $oObject
* @param string $sProperty
* @param $value
*
- * @throws \ReflectionException
* @since 2.7.8 3.0.3 3.1.0
*/
public function SetNonPublicProperty(object $oObject, string $sProperty, $value)
{
- $class = new \ReflectionClass(get_class($oObject));
- $property = $class->getProperty($sProperty);
- $property->setAccessible(true);
-
- $property->setValue($oObject, $value);
+ $oProperty = $this->GetProperty(get_class($oObject), $sProperty);
+ $oProperty->setValue($oObject, $value);
}
- public function RecurseRmdir($dir) {
+ /**
+ * @since 3.1.0
+ */
+ public function SetNonPublicStaticProperty(string $sClass, string $sProperty, $value)
+ {
+ $oProperty = $this->GetProperty($sClass, $sProperty);
+ $oProperty->setValue($value);
+ }
+
+ public function RecurseRmdir($dir)
+ {
if (is_dir($dir)) {
$objects = scandir($dir);
foreach ($objects as $object) {
if ($object != "." && $object != "..") {
- if (is_dir($dir.DIRECTORY_SEPARATOR.$object))
+ if (is_dir($dir.DIRECTORY_SEPARATOR.$object)) {
$this->RecurseRmdir($dir.DIRECTORY_SEPARATOR.$object);
- else
+ } else {
unlink($dir.DIRECTORY_SEPARATOR.$object);
+ }
}
}
rmdir($dir);
diff --git a/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php b/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php
new file mode 100644
index 000000000..f2395d9a0
--- /dev/null
+++ b/tests/php-unit-tests/unitary-tests/application/cmdbAbstractObjectTest.php
@@ -0,0 +1,479 @@
+GetObjectsAwaitingFireEventDbLinksChanged();
+ $this->assertSame([], $aLinkModificationsStack);
+
+ // retain events
+ cmdbAbstractObject::SetEventDBLinksChangedBlocked(true);
+
+ // Create the person
+ $oPerson = $this->CreatePerson(1);
+ $this->assertIsObject($oPerson);
+ // Create the team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ // contact types
+ $oContactType1 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
+ $oContactType1->DBInsert();
+ $oContactType2 = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.rand(10000, 99999)]);
+ $oContactType2->DBInsert();
+
+ // Prepare the link for the insertion with the team
+
+ $aValues = [
+ 'person_id' => $oPerson->GetKey(),
+ 'role_id' => $oContactType1->GetKey(),
+ 'team_id' => $oTeam->GetKey(),
+ ];
+ $oLinkPersonToTeam1 = MetaModel::NewObject(lnkPersonToTeam::class, $aValues);
+ $oLinkPersonToTeam1->DBInsert();
+
+ $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
+ self::assertCount(3, $aLinkModificationsStack);
+ $aExpectedLinkStack = [
+ 'Team' => [$oTeam->GetKey() => 1],
+ 'Person' => [$oPerson->GetKey() => 1],
+ 'ContactType' => [$oContactType1->GetKey() => 1],
+ ];
+ self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
+
+ $oLinkPersonToTeam1->Set('role_id', $oContactType2->GetKey());
+ $oLinkPersonToTeam1->DBWrite();
+ $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
+ self::assertCount(3, $aLinkModificationsStack);
+ $aExpectedLinkStack = [
+ 'Team' => [$oTeam->GetKey() => 2],
+ 'Person' => [$oPerson->GetKey() => 2],
+ 'ContactType' => [
+ $oContactType1->GetKey() => 2,
+ $oContactType2->GetKey() => 1,
+ ],
+ ];
+ self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
+ }
+
+ public function testProcessClassIdDeferredUpdate()
+ {
+ // Create the team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+
+ // --- Simulating modifications of :
+ // - lnkPersonToTeam:1 is sample data with : team_id=39 ; person_id=9 ; role_id=3
+ // - lnkPersonToTeam:2 is sample data with : team_id=39 ; person_id=14 ; role_id=0
+ $aLinkStack = [
+ 'Team' => [$oTeam->GetKey() => 2],
+ 'Person' => [
+ '9' => 1,
+ '14' => 1,
+ ],
+ 'ContactType' => [
+ '1' => 1,
+ '0' => 1,
+ ],
+ ];
+ $this->SetObjectsAwaitingFireEventDbLinksChanged($aLinkStack);
+
+ // Processing deferred updates for Team
+ $this->InvokeNonPublicMethod(get_class($oTeam), 'FireEventDbLinksChangedForCurrentObject', $oTeam, []);
+ $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
+ $aExpectedLinkStack = [
+ 'Team' => [],
+ 'Person' => [
+ '9' => 1,
+ '14' => 1,
+ ],
+ 'ContactType' => [
+ '1' => 1,
+ '0' => 1,
+ ],
+ ];
+ self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
+
+
+ // --- Simulating modifications of :
+ // - lnkApplicationSolutionToFunctionalCI::2 : applicationsolution_id=13 ; functionalci_id=29
+ // - lnkApplicationSolutionToFunctionalCI::8 : applicationsolution_id=13 ; functionalci_id=27
+ // The lnkApplicationSolutionToFunctionalCI points on root classes, so we can test unstacking for a leaf class
+ $aLinkStack = [
+ 'ApplicationSolution' => ['13' => 2],
+ 'FunctionalCI' => [
+ '29' => 1,
+ '27' => 1,
+ ],
+ ];
+ $this->SetObjectsAwaitingFireEventDbLinksChanged($aLinkStack);
+
+ // Processing deferred updates for WebServer::29
+ /** @var \cmdbAbstractObject $oLinkPersonToTeam1 */
+ $oWebServer29 = MetaModel::GetObject(WebServer::class, 29);
+ $this->InvokeNonPublicMethod(get_class($oWebServer29), 'FireEventDbLinksChangedForCurrentObject', $oWebServer29, []);
+ $aLinkModificationsStack = $this->GetObjectsAwaitingFireEventDbLinksChanged();
+ $aExpectedLinkStack = [
+ 'ApplicationSolution' => ['13' => 2],
+ 'FunctionalCI' => [
+ '27' => 1,
+ ],
+ ];
+ self::assertSame($aExpectedLinkStack, $aLinkModificationsStack);
+ }
+
+ private function GetObjectsAwaitingFireEventDbLinksChanged(): array
+ {
+ return $this->GetNonPublicStaticProperty(cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged');
+ }
+
+ private function SetObjectsAwaitingFireEventDbLinksChanged(array $aObjects): void
+ {
+ $this->SetNonPublicStaticProperty(cmdbAbstractObject::class, 'aObjectsAwaitingEventDbLinksChanged', $aObjects);
+ }
+
+ /**
+ * Check that EVENT_DB_LINKS_CHANGED events are not sent to the current updated/created object (Team)
+ * the events are sent to the other side (Person)
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function testDBInsertTeam()
+ {
+ // Prepare the link set
+ $sLinkedClass = lnkPersonToTeam::class;
+ $aLinkedObjectsArray = [];
+ $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
+ $oLinkSet = new ormLinkSet(Team::class, 'persons_list', $oSet);
+
+ // Create the 3 persons
+ for ($i = 0; $i < 3; $i++) {
+ $oPerson = $this->CreatePerson($i);
+ $this->assertIsObject($oPerson);
+ // Add the person to the link
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey()]);
+ $oLinkSet->AddItem($oLink);
+ }
+
+ $this->debug("\n-------------> Test Starts HERE\n");
+
+ $oEventReceiver = new LinksEventReceiver();
+ $oEventReceiver->RegisterCRUDListeners();
+
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertIsObject($oTeam);
+
+ // 3 links added to person (the Team side is ignored)
+ $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ }
+
+ /**
+ * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when creating a new lnk object
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function testAddLinkToTeam()
+ {
+ // Create a person
+ $oPerson = $this->CreatePerson(1);
+ $this->assertIsObject($oPerson);
+
+ // Create a Team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertIsObject($oTeam);
+
+ $this->debug("\n-------------> Test Starts HERE\n");
+ $oEventReceiver = new LinksEventReceiver();
+ $oEventReceiver->RegisterCRUDListeners();
+
+ // The link creation will signal both the Person an the Team
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
+ $oLink->DBInsert();
+
+ // 2 events one for Person and One for Team
+ $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ }
+
+ /**
+ * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when updating an existing lnk object
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function testUpdateLinkRole()
+ {
+ // Create a person
+ $oPerson = $this->CreatePerson(1);
+ $this->assertIsObject($oPerson);
+
+ // Create a Team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertIsObject($oTeam);
+
+ // Create the link
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
+ $oLink->DBInsert();
+
+ $this->debug("\n-------------> Test Starts HERE\n");
+ $oEventReceiver = new LinksEventReceiver();
+ $oEventReceiver->RegisterCRUDListeners();
+
+ // The link update will signal both the Person, the Team and the ContactType
+ // Change the role
+ $oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oLink->GetKey()]);
+ $oContactType->DBInsert();
+ $oLink->Set('role_id', $oContactType->GetKey());
+ $oLink->DBUpdate();
+
+ // 3 events one for Person, one for Team and one for ContactType
+ $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ }
+
+ /**
+ * Check that when a link changes from an object to another, then both objects are notified
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function testUpdateLinkPerson()
+ {
+ // Create 2 person
+ $oPerson1 = $this->CreatePerson(1);
+ $this->assertIsObject($oPerson1);
+
+ $oPerson2 = $this->CreatePerson(2);
+ $this->assertIsObject($oPerson2);
+
+ // Create a Team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertIsObject($oTeam);
+
+ // Create the link between Person1 and Team
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson1->GetKey(), 'team_id' => $oTeam->GetKey()]);
+ $oLink->DBInsert();
+
+ $this->debug("\n-------------> Test Starts HERE\n");
+ $oEventReceiver = new LinksEventReceiver();
+ $oEventReceiver->RegisterCRUDListeners();
+
+ // The link update will signal both the Persons and the Team
+ // Change the person
+ $oLink->Set('person_id', $oPerson2->GetKey());
+ $oLink->DBUpdate();
+
+ // 3 events 2 for Person, one for Team
+ $this->assertEquals(3, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ }
+
+ /**
+ * Check that EVENT_DB_LINKS_CHANGED events are sent to all the linked objects when deleting an existing lnk object
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ public function testDeleteLink()
+ {
+ // Create a person
+ $oPerson = $this->CreatePerson(1);
+ $this->assertIsObject($oPerson);
+
+ // Create a Team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeam1', 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertIsObject($oTeam);
+
+ // Create the link
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson->GetKey(), 'team_id' => $oTeam->GetKey()]);
+ $oLink->DBInsert();
+
+ $this->debug("\n-------------> Test Starts HERE\n");
+ $oEventReceiver = new LinksEventReceiver();
+ $oEventReceiver->RegisterCRUDListeners();
+
+ // The link delete will signal both the Person an the Team
+ $oLink->DBDelete();
+
+ // 3 events one for Person, one for Team
+ $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]);
+ }
+
+ /**
+ * Debug called by event receivers
+ *
+ * @param $sMsg
+ *
+ * @return void
+ */
+ public static function DebugStatic($sMsg)
+ {
+ if (static::$DEBUG_UNIT_TEST) {
+ if (is_string($sMsg)) {
+ echo "$sMsg\n";
+ } else {
+ print_r($sMsg);
+ }
+ }
+ }
+}
+
+
+/**
+ * Count events received
+ * And allow callbacks on events
+ */
+class LinksEventReceiver
+{
+ private $aCallbacks = [];
+
+ public static $bIsObjectInCrudStack;
+
+ public function AddCallback(string $sEvent, string $sClass, string $sFct, int $iCount = 1): void
+ {
+ $this->aCallbacks[$sEvent][$sClass] = [
+ 'callback' => [$this, $sFct],
+ 'count' => $iCount,
+ ];
+ }
+
+ public function CleanCallbacks()
+ {
+ $this->aCallbacks = [];
+ }
+
+ // Event callbacks
+ public function OnEvent(EventData $oData)
+ {
+ $sEvent = $oData->GetEvent();
+ $oObject = $oData->Get('object');
+ $sClass = get_class($oObject);
+ $iKey = $oObject->GetKey();
+ $this->Debug(__METHOD__.": received event '$sEvent' for $sClass::$iKey");
+ cmdbAbstractObjectTest::IncrementCallCount($sEvent);
+
+ if (isset($this->aCallbacks[$sEvent][$sClass])) {
+ $aCallBack = $this->aCallbacks[$sEvent][$sClass];
+ if ($aCallBack['count'] > 0) {
+ $this->aCallbacks[$sEvent][$sClass]['count']--;
+ call_user_func($this->aCallbacks[$sEvent][$sClass]['callback'], $oObject);
+ }
+ }
+ }
+
+ public function RegisterCRUDListeners(string $sEvent = null, $mEventSource = null)
+ {
+ $this->Debug('Registering Test event listeners');
+ if (is_null($sEvent)) {
+ EventService::RegisterListener(EVENT_DB_LINKS_CHANGED, [$this, 'OnEvent']);
+ return;
+ }
+ EventService::RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
+ }
+
+ /**
+ * @param $oObject
+ *
+ * @return void
+ * @throws \ArchivedObjectException
+ * @throws \CoreCannotSaveObjectException
+ * @throws \CoreException
+ * @throws \CoreUnexpectedValue
+ * @throws \CoreWarning
+ * @throws \MySQLException
+ * @throws \OQLException
+ */
+ private function AddRoleToLink($oObject): void
+ {
+ $this->Debug(__METHOD__);
+ $oContactType = MetaModel::NewObject(ContactType::class, ['name' => 'test_'.$oObject->GetKey()]);
+ $oContactType->DBInsert();
+ $oObject->Set('role_id', $oContactType->GetKey());
+ }
+
+ private function SetPersonFunction($oObject): void
+ {
+ $this->Debug(__METHOD__);
+ $oObject->Set('function', 'CRUD_function_'.rand());
+ }
+
+ private function SetPersonFirstName($oObject): void
+ {
+ $this->Debug(__METHOD__);
+ $oObject->Set('first_name', 'CRUD_first_name_'.rand());
+ }
+
+ private function CheckCrudStack(DBObject $oObject): void
+ {
+ self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(get_class($oObject), $oObject->GetKey());
+ }
+
+ private function CheckUpdateInLnk(lnkPersonToTeam $oLnkPersonToTeam)
+ {
+ $iTeamId = $oLnkPersonToTeam->Get('team_id');
+ self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(Team::class, $iTeamId);
+ }
+
+ private function Debug($msg)
+ {
+ cmdbAbstractObjectTest::DebugStatic($msg);
+ }
+}
diff --git a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
index 0a4f0949f..c54dcbdd1 100644
--- a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php
@@ -11,6 +11,7 @@ use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use ContactType;
use CoreException;
+use DBObject;
use DBObjectSet;
use DBSearch;
use lnkPersonToTeam;
@@ -440,6 +441,45 @@ class CRUDEventTest extends ItopDataTestCase
}
}
}
+
+ public function testCrudStack()
+ {
+ $oEventReceiver = new CRUDEventReceiver();
+ // Modify the person's function
+ $oEventReceiver->AddCallback(EVENT_DB_COMPUTE_VALUES, Person::class, 'CheckCrudStack');
+ $oEventReceiver->RegisterCRUDListeners(EVENT_DB_COMPUTE_VALUES);
+ $oPerson1 = $this->CreatePerson(1);
+ $this->assertTrue(CRUDEventReceiver::$bIsObjectInCrudStack);
+ $oEventReceiver->CleanCallbacks();
+
+ $oEventReceiver->AddCallback(EVENT_DB_CHECK_TO_WRITE, Person::class, 'CheckCrudStack');
+ $oEventReceiver->RegisterCRUDListeners(EVENT_DB_CHECK_TO_WRITE);
+ $this->CreatePerson(2);
+ $this->assertTrue(CRUDEventReceiver::$bIsObjectInCrudStack);
+ $oEventReceiver->CleanCallbacks();
+
+ $oEventReceiver->AddCallback(EVENT_DB_CREATE_DONE, Person::class, 'CheckCrudStack');
+ $oEventReceiver->RegisterCRUDListeners(EVENT_DB_CREATE_DONE);
+ $this->CreatePerson(3);
+ $this->assertTrue(CRUDEventReceiver::$bIsObjectInCrudStack);
+ $oEventReceiver->CleanCallbacks();
+
+ // Insert a Team with new lnkPersonToTeam - in the lnkPersonToTeam event we check that Team CRUD operation is ongoing
+ $oEventReceiver->AddCallback(EVENT_DB_CREATE_DONE, Person::class, 'CheckUpdateInLnk');
+ $sLinkedClass = lnkPersonToTeam::class;
+ $oEventReceiver->RegisterCRUDListeners(EVENT_DB_CREATE_DONE, $sLinkedClass);
+ // Prepare the link for the insertion with the team
+ $aLinkedObjectsArray = [];
+ $oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
+ $oLinkSet = new ormLinkSet(Team::class, 'persons_list', $oSet);
+ $oLink = MetaModel::NewObject(lnkPersonToTeam::class, ['person_id' => $oPerson1->GetKey()]);
+ $oLinkSet->AddItem($oLink);
+ // Create the team
+ $oTeam = MetaModel::NewObject(Team::class, ['name' => 'TestTeamWithLinkToAPerson', 'persons_list' => $oLinkSet, 'org_id' => $this->getTestOrgId()]);
+ $oTeam->DBInsert();
+ $this->assertTrue(CRUDEventReceiver::$bIsObjectInCrudStack);
+ }
+
}
@@ -472,6 +512,8 @@ class CRUDEventReceiver extends ClassesWithDebug
{
private $aCallbacks = [];
+ public static $bIsObjectInCrudStack;
+
public function AddCallback(string $sEvent, string $sClass, string $sFct, int $iCount = 1): void
{
$this->aCallbacks[$sEvent][$sClass] = [
@@ -480,6 +522,11 @@ class CRUDEventReceiver extends ClassesWithDebug
];
}
+ public function CleanCallbacks()
+ {
+ $this->aCallbacks = [];
+ }
+
// Event callbacks
public function OnEvent(EventData $oData)
{
@@ -499,7 +546,7 @@ class CRUDEventReceiver extends ClassesWithDebug
}
}
- public function RegisterCRUDListeners(string $sEvent = null)
+ public function RegisterCRUDListeners(string $sEvent = null, $mEventSource = null)
{
$this->Debug('Registering Test event listeners');
if (is_null($sEvent)) {
@@ -512,7 +559,7 @@ class CRUDEventReceiver extends ClassesWithDebug
return;
}
- EventService::RegisterListener($sEvent, [$this, 'OnEvent']);
+ EventService::RegisterListener($sEvent, [$this, 'OnEvent'], $mEventSource);
}
/**
@@ -547,4 +594,14 @@ class CRUDEventReceiver extends ClassesWithDebug
$oObject->Set('first_name', 'CRUD_first_name_'.rand());
}
+ private function CheckCrudStack(DBObject $oObject): void
+ {
+ self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(get_class($oObject), $oObject->GetKey());
+ }
+
+ private function CheckUpdateInLnk(lnkPersonToTeam $oLnkPersonToTeam)
+ {
+ $iTeamId = $oLnkPersonToTeam->Get('team_id');
+ self::$bIsObjectInCrudStack = DBObject::IsObjectCurrentlyInCrud(Team::class, $iTeamId);
+ }
}