mirror of
https://github.com/Combodo/iTop.git
synced 2026-08-22 14:18:18 +02:00
New CRUD behaviour (removed Reload in DBInsert and DBUpdate) and protection against reentrance
This commit is contained in:
@@ -4435,7 +4435,15 @@ HTML;
|
||||
*/
|
||||
public function DBInsertNoReload()
|
||||
{
|
||||
$res = parent::DBInsertNoReload();
|
||||
return $this->DBInsert();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function DBInsert()
|
||||
{
|
||||
$res = parent::DBInsert();
|
||||
|
||||
$this->SetWarningsAsSessionMessages('create');
|
||||
|
||||
@@ -4480,13 +4488,13 @@ HTML;
|
||||
|
||||
// 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()
|
||||
static $aUpdateReentrance = array();
|
||||
$sKey = get_class($this).'::'.$this->GetKey();
|
||||
if (array_key_exists($sKey, $aUpdateReentrance))
|
||||
{
|
||||
if (!MetaModel::StartReentranceProtection($this)) {
|
||||
$sClass = get_class($this);
|
||||
$sKey = $this->GetKey();
|
||||
IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Rejected (reentrance)", LogChannels::DM_CRUD);
|
||||
|
||||
return $res;
|
||||
}
|
||||
$aUpdateReentrance[$sKey] = true;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -4497,13 +4505,13 @@ HTML;
|
||||
$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
unset($aUpdateReentrance[$sKey]);
|
||||
MetaModel::StopReentranceProtection($this);
|
||||
}
|
||||
|
||||
if ($this->IsModified()) {
|
||||
return $this->DBUpdate();
|
||||
}
|
||||
|
||||
return $res;
|
||||
|
||||
@@ -8154,6 +8154,11 @@ class AttributeImage extends AttributeBlob
|
||||
return $oDoc;
|
||||
}
|
||||
|
||||
public function GetDefaultValue(DBObject $oHostObject = null)
|
||||
{
|
||||
return new ormDocument('', '', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the supplied ormDocument actually contains an image
|
||||
* {@inheritDoc}
|
||||
@@ -13076,7 +13081,7 @@ class AttributeObsolescenceFlag extends AttributeBoolean
|
||||
|
||||
public function GetDefaultValue(DBObject $oHostObject = null)
|
||||
{
|
||||
return $this->MakeRealValue("", $oHostObject);
|
||||
return $this->MakeRealValue(false, $oHostObject);
|
||||
}
|
||||
|
||||
public function IsNullAllowed()
|
||||
|
||||
@@ -629,12 +629,6 @@ abstract class CMDBObject extends DBObject
|
||||
return $newKey;
|
||||
}
|
||||
|
||||
public function DBUpdate()
|
||||
{
|
||||
parent::DBUpdate();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param null $oDeletionPlan
|
||||
*
|
||||
|
||||
@@ -148,10 +148,10 @@ abstract class DBObject implements iDisplay
|
||||
/**
|
||||
* @var string local events suffix
|
||||
*/
|
||||
protected $m_sEventUniqId = '';
|
||||
|
||||
private static $aUpdateReentrance = [];
|
||||
protected $m_sObjectUniqId = '';
|
||||
|
||||
protected $m_bIsReadOnly = false;
|
||||
protected $m_sReadOnlyMessage = '';
|
||||
|
||||
/**
|
||||
* DBObject constructor.
|
||||
@@ -177,7 +177,7 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_bFullyLoaded = $this->IsFullyLoaded();
|
||||
$this->m_aTouchedAtt = array();
|
||||
$this->m_aModifiedAtt = array();
|
||||
$this->m_sEventUniqId = get_class($this).'::'.$this->GetKey().'_'.utils::GetUniqId();
|
||||
$this->m_sObjectUniqId = get_class($this).'::'.$this->GetKey().'_'.utils::GetUniqId();
|
||||
$this->RegisterEvents();
|
||||
return;
|
||||
}
|
||||
@@ -200,7 +200,7 @@ abstract class DBObject implements iDisplay
|
||||
|
||||
$this->UpdateMetaAttributes();
|
||||
|
||||
$this->m_sEventUniqId = get_class($this).'::0'.'_'.utils::GetUniqId();
|
||||
$this->m_sObjectUniqId = get_class($this).'::0'.'_'.utils::GetUniqId();
|
||||
$this->RegisterEvents();
|
||||
}
|
||||
|
||||
@@ -465,7 +465,17 @@ abstract class DBObject implements iDisplay
|
||||
// which is something that could happen on open joins
|
||||
$sAttRef = $sClassAlias.$sAttCode;
|
||||
|
||||
if (array_key_exists($sAttRef, $aRow))
|
||||
// Due to custom formatting rules, empty friendlynames may be rendered as non-empty strings
|
||||
// let's fix this and make sure we render an empty string if the key == 0
|
||||
if ($oAttDef instanceof AttributeExternalField && $oAttDef->IsFriendlyName()) {
|
||||
$sKeyRef = $sClassAlias.$oAttDef->GetKeyAttCode();
|
||||
if (array_key_exists($sKeyRef, $aRow) && $aRow[$sKeyRef] == '0') {
|
||||
$value = '';
|
||||
$bIsDefined = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$bIsDefined && array_key_exists($sAttRef, $aRow))
|
||||
{
|
||||
$value = $oAttDef->FromSQLToValue($aRow, $sAttRef);
|
||||
$bIsDefined = true;
|
||||
@@ -552,6 +562,11 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
public function Set($sAttCode, $value)
|
||||
{
|
||||
$sMessage = $this->IsReadOnly();
|
||||
if ($sMessage !== false) {
|
||||
throw new CoreException($sMessage);
|
||||
}
|
||||
|
||||
if ($sAttCode == 'finalclass') {
|
||||
// Ignore it - this attribute is set upon object creation and that's it
|
||||
return false;
|
||||
@@ -2783,180 +2798,7 @@ abstract class DBObject implements iDisplay
|
||||
*/
|
||||
public function DBInsertNoReload()
|
||||
{
|
||||
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();
|
||||
$this->EventInsertRequested();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate check - ensure DB integrity
|
||||
list($bRes, $aIssues) = $this->CheckToWrite();
|
||||
if (!$bRes)
|
||||
{
|
||||
throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
|
||||
}
|
||||
|
||||
// Stop watches
|
||||
$sState = $this->GetState();
|
||||
if ($sState != '')
|
||||
{
|
||||
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
|
||||
{
|
||||
if ($oAttDef instanceof AttributeStopWatch)
|
||||
{
|
||||
if (in_array($sState, $oAttDef->GetStates()))
|
||||
{
|
||||
// Start the stop watch and compute the deadlines
|
||||
/** @var \ormStopWatch $oSW */
|
||||
$oSW = $this->Get($sAttCode);
|
||||
$oSW->Start($this, $oAttDef);
|
||||
$oSW->ComputeDeadlines($this, $oAttDef);
|
||||
$this->Set($sAttCode, $oSW);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->EventInsertBefore();
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
$this->AfterInsert();
|
||||
|
||||
// Prevent DBUpdate at this point (reentrance protection)
|
||||
$sClass = get_class($this);
|
||||
$sClassKey = $sClass.'::'.$this->m_iKey;
|
||||
self::$aUpdateReentrance[$sClassKey] = true;
|
||||
$this->EventInsertAfter();
|
||||
unset(self::$aUpdateReentrance[$sClassKey]);
|
||||
|
||||
// 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);
|
||||
|
||||
if ($this->IsModified()) {
|
||||
$this->DBUpdate();
|
||||
}
|
||||
|
||||
return $this->m_iKey;
|
||||
return $this->DBInsert();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3093,8 +2935,162 @@ abstract class DBObject implements iDisplay
|
||||
throw new CoreException("$sErrorMessage (see the log for more information)");
|
||||
}
|
||||
|
||||
$this->DBInsertNoReload();
|
||||
$this->Reload();
|
||||
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();
|
||||
$this->EventInsertRequested();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate check - ensure DB integrity
|
||||
$this->SetReadOnly('No modification allowed during CheckToWrite');
|
||||
list($bRes, $aIssues) = $this->CheckToWrite();
|
||||
$this->SetReadWrite();
|
||||
if (!$bRes) {
|
||||
throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
|
||||
}
|
||||
|
||||
// Stop watches
|
||||
$sState = $this->GetState();
|
||||
if ($sState != '') {
|
||||
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
|
||||
if ($oAttDef instanceof AttributeStopWatch) {
|
||||
if (in_array($sState, $oAttDef->GetStates())) {
|
||||
// Start the stop watch and compute the deadlines
|
||||
/** @var \ormStopWatch $oSW */
|
||||
$oSW = $this->Get($sAttCode);
|
||||
$oSW->Start($this, $oAttDef);
|
||||
$oSW->ComputeDeadlines($this, $oAttDef);
|
||||
$this->Set($sAttCode, $oSW);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->SetReadOnly('No modification allowed during The Event :'.EVENT_SERVICE_DB_BEFORE_INSERT);
|
||||
$this->EventInsertBefore();
|
||||
$this->SetReadWrite();
|
||||
|
||||
$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->AfterInsert();
|
||||
|
||||
$this->EventInsertAfter();
|
||||
|
||||
// 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);
|
||||
|
||||
MetaModel::StopReentranceProtection($this);
|
||||
|
||||
if ($this->IsModified()) {
|
||||
$this->DBUpdate();
|
||||
}
|
||||
|
||||
return $this->m_iKey;
|
||||
}
|
||||
@@ -3160,17 +3156,13 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
// Protect against reentrance (e.g. cascading the update of ticket logs)
|
||||
$sClass = get_class($this);
|
||||
$sKey = $sClass.'::'.$this->GetKey();
|
||||
$sKey = $this->GetKey();
|
||||
if (!MetaModel::StartReentranceProtection($this)) {
|
||||
|
||||
if (array_key_exists($sKey, self::$aUpdateReentrance))
|
||||
{
|
||||
if ($sClass == 'UserRequest') {
|
||||
IssueLog::Debug("CRUD: DBUpdate $sKey Rejected (reentrance)", LogChannels::DM_CRUD);
|
||||
}
|
||||
IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Rejected (reentrance)", LogChannels::DM_CRUD);
|
||||
|
||||
return false;
|
||||
}
|
||||
self::$aUpdateReentrance[$sKey] = true;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -3203,16 +3195,16 @@ abstract class DBObject implements iDisplay
|
||||
if (count($aChanges) == 0)
|
||||
{
|
||||
// Attempting to update an unchanged object
|
||||
unset(self::$aUpdateReentrance[$sKey]);
|
||||
if ($sClass == 'UserRequest') {
|
||||
IssueLog::Debug("CRUD: DBUpdate $sKey Aborted (no change)", LogChannels::DM_CRUD);
|
||||
}
|
||||
MetaModel::StopReentranceProtection($this);
|
||||
IssueLog::Debug("CRUD: DBUpdate $sClass::$sKey Aborted (no change)", LogChannels::DM_CRUD);
|
||||
|
||||
return $this->m_iKey;
|
||||
}
|
||||
|
||||
// Ultimate check - ensure DB integrity
|
||||
$this->SetReadOnly('No modification allowed during CheckToWrite');
|
||||
list($bRes, $aIssues) = $this->CheckToWrite();
|
||||
$this->SetReadWrite();
|
||||
if (!$bRes)
|
||||
{
|
||||
throw new CoreCannotSaveObjectException(['issues' => $aIssues, 'class' => $sClass, 'id' => $this->GetKey()]);
|
||||
@@ -3221,20 +3213,11 @@ abstract class DBObject implements iDisplay
|
||||
// Save the original values (will be reset to the new values when the object get written to the DB)
|
||||
$aOriginalValues = $this->m_aOrigValues;
|
||||
|
||||
// Activate any existing trigger
|
||||
// - TriggerOnObjectMention
|
||||
$this->ActivateOnMentionTriggers(false);
|
||||
|
||||
$bHasANewExternalKeyValue = false;
|
||||
$aHierarchicalKeys = [];
|
||||
$aDBChanges = [];
|
||||
foreach ($aChanges as $sAttCode => $valuecurr)
|
||||
{
|
||||
$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
|
||||
if ($oAttDef->IsExternalKey())
|
||||
{
|
||||
$bHasANewExternalKeyValue = true;
|
||||
}
|
||||
if ($oAttDef->IsBasedOnDBColumns())
|
||||
{
|
||||
$aDBChanges[$sAttCode] = $aChanges[$sAttCode];
|
||||
@@ -3255,7 +3238,10 @@ abstract class DBObject implements iDisplay
|
||||
$iIsTransactionRetryDelay = MetaModel::GetConfig()->Get('db_core_transactions_retry_delay_ms');
|
||||
$iTransactionRetry = $iTransactionRetryCount;
|
||||
}
|
||||
$this->SetReadOnly('No modification allowed during The Event :'.EVENT_SERVICE_DB_BEFORE_UPDATE);
|
||||
$this->EventUpdateBefore();
|
||||
$this->SetReadWrite();
|
||||
|
||||
while ($iTransactionRetry > 0)
|
||||
{
|
||||
try
|
||||
@@ -3384,18 +3370,13 @@ abstract class DBObject implements iDisplay
|
||||
$this->m_aModifiedAtt = array();
|
||||
|
||||
try {
|
||||
// Reload to get the external attributes
|
||||
if ($bHasANewExternalKeyValue) {
|
||||
$this->Reload(true /* AllowAllData */);
|
||||
} else {
|
||||
// Reset original values although the object has not been reloaded
|
||||
foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded)
|
||||
// Reset original values although the object has not been reloaded
|
||||
foreach ($this->m_aLoadedAtt as $sAttCode => $bLoaded)
|
||||
{
|
||||
if ($bLoaded)
|
||||
{
|
||||
if ($bLoaded)
|
||||
{
|
||||
$value = $this->m_aCurrValues[$sAttCode];
|
||||
$this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value;
|
||||
}
|
||||
$value = $this->m_aCurrValues[$sAttCode];
|
||||
$this->m_aOrigValues[$sAttCode] = is_object($value) ? clone $value : $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3413,6 +3394,11 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
}
|
||||
|
||||
// Activate any existing trigger
|
||||
// - TriggerOnObjectMention
|
||||
// Forgotten by the fix of N°3245
|
||||
$this->ActivateOnMentionTriggers(false);
|
||||
|
||||
$this->AfterUpdate();
|
||||
|
||||
$this->EventUpdateAfter(['changes' => $aChanges]);
|
||||
@@ -3425,7 +3411,7 @@ abstract class DBObject implements iDisplay
|
||||
}
|
||||
finally
|
||||
{
|
||||
unset(self::$aUpdateReentrance[$sKey]);
|
||||
MetaModel::StopReentranceProtection($this);
|
||||
}
|
||||
|
||||
if ($this->IsModified()) {
|
||||
@@ -5795,6 +5781,37 @@ abstract class DBObject implements iDisplay
|
||||
return $oExpression->Evaluate($aArgs);
|
||||
}
|
||||
|
||||
final public function SetReadOnly($sMessage) {
|
||||
$this->m_bIsReadOnly = true;
|
||||
$this->m_sReadOnlyMessage = $sMessage;
|
||||
}
|
||||
|
||||
final public function SetReadWrite() {
|
||||
$this->m_bIsReadOnly = false;
|
||||
$this->m_sReadOnlyMessage = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
public function IsReadOnly()
|
||||
{
|
||||
if ($this->m_bIsReadOnly) {
|
||||
return $this->m_sReadOnlyMessage;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique id of the object instance in memory
|
||||
* @return string
|
||||
*/
|
||||
public function GetObjectUniqId()
|
||||
{
|
||||
return $this->m_sObjectUniqId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sEvent
|
||||
* @param array $aEventData
|
||||
@@ -5806,7 +5823,7 @@ abstract class DBObject implements iDisplay
|
||||
{
|
||||
$aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey();
|
||||
$aEventData['object'] = $this;
|
||||
$aEventSources = [$this->m_sEventUniqId];
|
||||
$aEventSources = [$this->m_sObjectUniqId];
|
||||
foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) {
|
||||
$aEventSources[] = $sClass;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ abstract class MetaModel
|
||||
/** @var string */
|
||||
protected static $m_sEnvironment = 'production';
|
||||
|
||||
protected static $m_aReentryProtection = [];
|
||||
|
||||
/**
|
||||
* MetaModel constructor.
|
||||
*/
|
||||
@@ -6823,6 +6825,11 @@ abstract class MetaModel
|
||||
*/
|
||||
public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
|
||||
{
|
||||
$oObject = self::GetReentryObject($sClass, $iKey);
|
||||
if ($oObject !== false) {
|
||||
return $oObject;
|
||||
}
|
||||
|
||||
$oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
|
||||
|
||||
if (empty($oObject))
|
||||
@@ -7559,6 +7566,35 @@ abstract class MetaModel
|
||||
/** @var AttributeEnum $oAttDef */
|
||||
return $oAttDef->GetStyle($sValue);
|
||||
}
|
||||
|
||||
protected static function GetReentryObject($sClass, $sKey)
|
||||
{
|
||||
if (isset(self::$m_aReentryProtection[$sClass][$sKey])) {
|
||||
return self::$m_aReentryProtection[$sClass][$sKey];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DBObject $oObject
|
||||
*
|
||||
* @return bool true if reentry possible
|
||||
*/
|
||||
public static function StartReentranceProtection(DBObject $oObject)
|
||||
{
|
||||
if (isset(self::$m_aReentryProtection[get_class($oObject)][$oObject->GetKey()])) {
|
||||
return false;
|
||||
}
|
||||
self::$m_aReentryProtection[get_class($oObject)][$oObject->GetKey()] = $oObject;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function StopReentranceProtection(DBObject $oObject)
|
||||
{
|
||||
if (isset(self::$m_aReentryProtection[get_class($oObject)][$oObject->GetKey()])) {
|
||||
unset(self::$m_aReentryProtection[get_class($oObject)][$oObject->GetKey()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1319,7 +1319,7 @@ EOF
|
||||
}
|
||||
|
||||
$sListenerPriority = (float)($oListener->GetChildText('priority', '0'));
|
||||
$sEvents .= "\n Combodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", $sEventListener, \$this->m_sEventUniqId, \"$sListenerId\", null, $sListenerPriority);";
|
||||
$sEvents .= "\n Combodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", $sEventListener, \$this->m_sObjectUniqId, \"$sListenerId\", null, $sListenerPriority);";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
219
test/core/CRUD/DBObjectTest.php
Normal file
219
test/core/CRUD/DBObjectTest.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2022 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Test\UnitTest\Core\CRUD;
|
||||
|
||||
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||
use lnkContactToFunctionalCI;
|
||||
use MetaModel;
|
||||
|
||||
/**
|
||||
* @runTestsInSeparateProcesses
|
||||
* @preserveGlobalState disabled
|
||||
* @backupGlobals disabled
|
||||
*/
|
||||
class DBObjectTest extends ItopDataTestCase
|
||||
{
|
||||
const USE_TRANSACTION = true;
|
||||
const CREATE_TEST_ORG = true;
|
||||
|
||||
|
||||
public function testReloadNotNecessaryForInsert()
|
||||
{
|
||||
$oPerson = $this->CreatePersonInstance();
|
||||
|
||||
// Insert without Reload
|
||||
$oPerson->DBInsert();
|
||||
|
||||
$aValues1 = [];
|
||||
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
|
||||
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
|
||||
continue;
|
||||
}
|
||||
$aValues1[$sAttCode] = $oPerson->Get($sAttCode);
|
||||
}
|
||||
$sOrgName1 = $oPerson->Get('org_name');
|
||||
/** @var \ormLinkSet $oCIList1 */
|
||||
$oCIList1 = $oPerson->Get('cis_list');
|
||||
$oTeamList1 = $oPerson->Get('team_list');
|
||||
|
||||
$sPerson1 = print_r($oPerson, true);
|
||||
|
||||
// 1st Reload
|
||||
$oPerson->Reload(true);
|
||||
$sPerson2 = print_r($oPerson, true);
|
||||
$this->assertNotEquals($sPerson1, $sPerson2);
|
||||
|
||||
$aValues2 = [];
|
||||
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
|
||||
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
|
||||
continue;
|
||||
}
|
||||
$aValues2[$sAttCode] = $oPerson->Get($sAttCode);
|
||||
}
|
||||
|
||||
$sOrgName2 = $oPerson->Get('org_name');
|
||||
/** @var \ormLinkSet $oCIList2 */
|
||||
$oCIList2 = $oPerson->Get('cis_list');
|
||||
$oTeamList2 = $oPerson->Get('team_list');
|
||||
|
||||
$this->assertEquals($sOrgName1, $sOrgName2);
|
||||
$this->assertTrue($oCIList1->Equals($oCIList2));
|
||||
$this->assertTrue($oTeamList1->Equals($oTeamList2));
|
||||
$this->assertEquals($aValues1, $aValues2);
|
||||
|
||||
// 2nd Reload
|
||||
$oPerson->Reload(true);
|
||||
$sPerson3 = print_r($oPerson, true);
|
||||
$this->assertEquals($sPerson2, $sPerson3);
|
||||
|
||||
}
|
||||
|
||||
public function testFriendlynameResetOnExtKeyReset()
|
||||
{
|
||||
$oPerson = $this->CreatePersonInstance();
|
||||
$oManager = $this->CreatePersonInstance();
|
||||
$oManager->DBInsert();
|
||||
|
||||
$oPerson->Set('manager_id', $oManager->GetKey());
|
||||
|
||||
$this->assertNotEmpty($oPerson->Get('manager_id_friendlyname'));
|
||||
|
||||
$oPerson->Set('manager_id', 0);
|
||||
|
||||
$this->assertEmpty($oPerson->Get('manager_id_friendlyname'));
|
||||
}
|
||||
|
||||
public function testReloadNotNecessaryForUpdate()
|
||||
{
|
||||
$oPerson = $this->CreatePersonInstance();
|
||||
$oPerson->DBInsert();
|
||||
$oManager = $this->CreatePersonInstance();
|
||||
$oManager->DBInsert();
|
||||
|
||||
$oPerson->Set('manager_id', $oManager->GetKey());
|
||||
$oPerson->DBUpdate();
|
||||
|
||||
$sManagerFriendlyname1 = $oPerson->Get('manager_id_friendlyname');
|
||||
$oCIList1 = $oPerson->Get('cis_list');
|
||||
$oTeamList1 = $oPerson->Get('team_list');
|
||||
$aValues1 = [];
|
||||
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
|
||||
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
|
||||
continue;
|
||||
}
|
||||
$aValues1[$sAttCode] = $oPerson->Get($sAttCode);
|
||||
}
|
||||
|
||||
$sPerson1 = print_r($oPerson, true);
|
||||
|
||||
// 1st Reload
|
||||
$oPerson->Reload(true);
|
||||
|
||||
$sPerson2 = print_r($oPerson, true);
|
||||
$this->assertNotEquals($sPerson1, $sPerson2);
|
||||
|
||||
$sManagerFriendlyname2 = $oPerson->Get('manager_id_friendlyname');
|
||||
$oCIList2 = $oPerson->Get('cis_list');
|
||||
$oTeamList2 = $oPerson->Get('team_list');
|
||||
$aValues2 = [];
|
||||
foreach (MetaModel::GetAttributesList('Person') as $sAttCode) {
|
||||
if (MetaModel::GetAttributeDef('Person', $sAttCode) instanceof \AttributeLinkedSet) {
|
||||
continue;
|
||||
}
|
||||
$aValues2[$sAttCode] = $oPerson->Get($sAttCode);
|
||||
}
|
||||
|
||||
$this->assertEquals($sManagerFriendlyname1, $sManagerFriendlyname2);
|
||||
$this->assertTrue($oCIList1->Equals($oCIList2));
|
||||
$this->assertTrue($oTeamList1->Equals($oTeamList2));
|
||||
$this->assertEquals($aValues1, $aValues2);
|
||||
|
||||
// 2nd Reload
|
||||
$oPerson->Reload(true);
|
||||
$sPerson3 = print_r($oPerson, true);
|
||||
$this->assertEquals($sPerson2, $sPerson3);
|
||||
}
|
||||
|
||||
public function testGetObjectUpdateUnderReentryProtection()
|
||||
{
|
||||
$oPerson = $this->CreatePersonInstance();
|
||||
$oPerson->DBInsert();
|
||||
|
||||
$oPerson->Set('email', 'test@combodo.com');
|
||||
$oPerson->DBUpdate();
|
||||
|
||||
$this->assertFalse($oPerson->IsModified());
|
||||
|
||||
$oNewPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
|
||||
$this->assertNotEquals($oPerson->GetObjectUniqId(), $oNewPerson->GetObjectUniqId());
|
||||
|
||||
MetaModel::StartReentranceProtection($oPerson);
|
||||
|
||||
$oPerson->Set('email', 'test1@combodo.com');
|
||||
$oPerson->DBUpdate();
|
||||
|
||||
$this->assertTrue($oPerson->IsModified());
|
||||
|
||||
$oNewPerson = MetaModel::GetObject('Person', $oPerson->GetKey());
|
||||
$this->assertEquals($oPerson->GetObjectUniqId(), $oNewPerson->GetObjectUniqId());
|
||||
|
||||
MetaModel::StopReentranceProtection($oPerson);
|
||||
}
|
||||
|
||||
public function testObjectIsReadOnly()
|
||||
{
|
||||
$oPerson = $this->CreatePersonInstance();
|
||||
|
||||
$sMessage = 'Not allowed to write to this object !';
|
||||
$oPerson->SetReadOnly($sMessage);
|
||||
try {
|
||||
$oPerson->Set('email', 'test1@combodo.com');
|
||||
$this->assertTrue(false, 'Set() should have raised a CoreException');
|
||||
}
|
||||
catch (\CoreException $e) {
|
||||
$this->assertEquals($sMessage, $e->getMessage());
|
||||
}
|
||||
|
||||
$oPerson->SetReadWrite();
|
||||
|
||||
$oPerson->Set('email', 'test1@combodo.com');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
*/
|
||||
private function CreatePersonInstance()
|
||||
{
|
||||
$oServer1 = $this->CreateServer(1);
|
||||
$oServer2 = $this->CreateServer(2);
|
||||
|
||||
$sClass = 'Person';
|
||||
$aParams = [
|
||||
'name' => 'Person_'.rand(10000, 99999),
|
||||
'first_name' => 'Test',
|
||||
'org_id' => $this->getTestOrgId(),
|
||||
];
|
||||
|
||||
$oPerson = MetaModel::NewObject($sClass);
|
||||
foreach ($aParams as $sAttCode => $oValue) {
|
||||
$oPerson->Set($sAttCode, $oValue);
|
||||
}
|
||||
|
||||
$oNewLink1 = new lnkContactToFunctionalCI();
|
||||
$oNewLink1->Set('functionalci_id', $oServer1->GetKey());
|
||||
$oNewLink2 = new lnkContactToFunctionalCI();
|
||||
$oNewLink2->Set('functionalci_id', $oServer2->GetKey());
|
||||
$oCIs = $oPerson->Get('cis_list');
|
||||
$oCIs->AddItem($oNewLink1);
|
||||
$oCIs->AddItem($oNewLink2);
|
||||
$oPerson->Set('cis_list', $oCIs);
|
||||
|
||||
return $oPerson;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user