N°5906 - CRUD Event - fire event EVENT_DB_LINKS_CHANGED when an n-n link is created/updated/deleted

This commit is contained in:
Eric Espie
2023-02-17 14:25:01 +01:00
parent 72dad5dd07
commit 8940051c3d
12 changed files with 1304 additions and 375 deletions

View File

@@ -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

View File

@@ -371,6 +371,22 @@
</event_datum>
</event_data>
</event>
<event id="EVENT_DB_LINKS_CHANGED" _delta="define">
<description>At least one link class was changed</description>
<sources>
<source id="cmdbAbstractObject">cmdbAbstractObject</source>
</sources>
<event_data>
<event_datum id="object">
<description>The object where the link is or was pointing to</description>
<type>DBObject</type>
</event_datum>
<event_datum id="debug_info">
<description>Debug string</description>
<type>string</type>
</event_datum>
</event_data>
</event>
<event id="EVENT_DB_OBJECT_RELOAD" _delta="define">
<description>An object has been re-loaded from the database</description>
<sources>

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -220,7 +220,26 @@
<read_only>false</read_only>
</field>
</fields>
<event_listeners>
<listener id="UpdateImpactAnalysis">
<event>EVENT_DB_LINKS_CHANGED</event>
<callback>UpdateTicketImpactedItems</callback>
<rank>0</rank>
</listener>
</event_listeners>
<methods>
<method id="UpdateTicketImpactedItems">
<comment/>
<static>false</static>
<access>public</access>
<type>EventListener</type>
<code><![CDATA[
public function UpdateTicketImpactedItems(Combodo\iTop\Service\Events\EventData $oEventData) {
$this->UpdateImpactedItems();
$this->DBUpdate();
}
]]></code>
</method>
<method id="DBInsertNoReload">
<static>false</static>
<access>public</access>

View File

@@ -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');
}

View File

@@ -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])) {

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -0,0 +1,479 @@
<?php
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
* @backupGlobals disabled
*/
class cmdbAbstractObjectTest extends ItopDataTestCase {
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = true;
// Count the events by name
private static array $aEventCalls = [];
private static int $iEventCalls = 0;
protected function setUp(): void
{
parent::setUp();
}
public static function IncrementCallCount(string $sEvent)
{
self::$aEventCalls[$sEvent] = (self::$aEventCalls[$sEvent] ?? 0) + 1;
self::$iEventCalls++;
}
public function testCheckLinkModifications() {
$aLinkModificationsStack = $this->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);
}
}

View File

@@ -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);
}
}