diff --git a/application/utils.inc.php b/application/utils.inc.php index 7f761d864c..cf62cabc70 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -3075,4 +3075,9 @@ HTML; return $sUrl; } + + public static function GetUniqId() + { + return hash('sha256', uniqid(sprintf('%x', rand()), true).sprintf('%x', rand())); + } } diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 4a070d2397..5d665943b0 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -608,15 +608,7 @@ abstract class CMDBObject extends DBObject public function DBUpdate() { - // Copy the changes list before the update (the list should be reset afterwards) - $aChanges = $this->ListChanges(); - if (count($aChanges) == 0) - { - return; - } - - $ret = parent::DBUpdate(); - return $ret; + parent::DBUpdate(); } diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml index c2306317d1..55bce1be8b 100644 --- a/core/datamodel.core.xml +++ b/core/datamodel.core.xml @@ -136,32 +136,6 @@ - - An object has been loaded from the database - - - The object loaded - DBObject - - - Debug string - string - - - - - An object has been created in memory - - - The object created - DBObject - - - Debug string - string - - - An object has been re-loaded from the database @@ -193,7 +167,7 @@ - + Check an object before it is deleted from the database cmdbAbstractObject::DoCheckToDelete diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 7b564005ad..dc3aa14312 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -149,7 +149,6 @@ abstract class DBObject implements iDisplay * @var string local events suffix */ protected $m_sEventUniqId = ''; - protected static $m_iEventUniqCounter = 0; /** @@ -176,9 +175,8 @@ abstract class DBObject implements iDisplay $this->m_bFullyLoaded = $this->IsFullyLoaded(); $this->m_aTouchedAtt = array(); $this->m_aModifiedAtt = array(); - $this->m_sEventUniqId = 'DataModel_'.get_class($this).'_'.self::$m_iEventUniqCounter++; + $this->m_sEventUniqId = get_class($this).'::'.$this->GetKey().'_'.utils::GetUniqId(); $this->RegisterEvents(); - $this->FireEvent(EVENT_SERVICE_DB_OBJECT_LOADED); return; } // Creation of a brand new object @@ -206,9 +204,8 @@ abstract class DBObject implements iDisplay $this->UpdateMetaAttributes(); - $this->m_sEventUniqId = 'DataModel_'.self::$m_iEventUniqCounter++; + $this->m_sEventUniqId = get_class($this).'::0'.'_'.utils::GetUniqId(); $this->RegisterEvents(); - $this->FireEvent(EVENT_SERVICE_DB_OBJECT_NEW); } protected function RegisterEvents() @@ -504,7 +501,6 @@ abstract class DBObject implements iDisplay // Load extended data if ($aExtendedDataSpec != null) { - $aExtendedDataSpec['table']; foreach($aExtendedDataSpec['fields'] as $sColumn) { $sColRef = $sClassAlias.'_extdata_'.$sColumn; @@ -1633,10 +1629,10 @@ abstract class DBObject implements iDisplay { if ($sType == FriendlyNameType::SHORT) { return $this->Get('friendlyname'); - } else { - $oExpression = MetaModel::GetNameExpression(get_class($this), $sType); - $this->EvaluateExpression($oExpression); } + $oExpression = MetaModel::GetNameExpression(get_class($this), $sType); + + return $this->EvaluateExpression($oExpression); } /** @@ -2809,6 +2805,10 @@ abstract class DBObject implements iDisplay } $sClass = get_class($this); + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBInsert $sClass::0 Starting", LogChannels::DM_CRUD); + } + $sRootClass = MetaModel::GetRootClass($sClass); // Ensure the update of the values (we are accessing the data directly) @@ -2893,7 +2893,6 @@ abstract class DBObject implements iDisplay } $this->OnObjectKeyReady(); - $this->FireEvent(EVENT_SERVICE_DB_OBJECT_KEY_READY); $this->DBWriteLinks(); $this->WriteExternalAttributes(); @@ -3105,6 +3104,11 @@ abstract class DBObject implements iDisplay } $this->Reload(); + $sClass = get_class($this); + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBInsert $sClass::{$this->m_iKey} Created", LogChannels::DM_CRUD); + } + return $this->m_iKey; } @@ -3168,15 +3172,22 @@ abstract class DBObject implements iDisplay } // Protect against reentrance (e.g. cascading the update of ticket logs) static $aUpdateReentrance = array(); - $sKey = get_class($this).'::'.$this->GetKey(); + $sClass = get_class($this); + $sKey = $sClass.'::'.$this->GetKey(); + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBUpdate $sKey Starting", LogChannels::DM_CRUD); + } + if (array_key_exists($sKey, $aUpdateReentrance)) { + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBUpdate $sKey Rejected (reentrance)", LogChannels::DM_CRUD); + } + return false; } $aUpdateReentrance[$sKey] = true; - $this->InitPreviousValuesForUpdatedAttributes(); - try { $this->DoComputeValues(); @@ -3184,7 +3195,7 @@ abstract class DBObject implements iDisplay $sState = $this->GetState(); if ($sState != '') { - foreach (MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef) + foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { if ($oAttDef instanceof AttributeStopWatch) { @@ -3202,12 +3213,16 @@ abstract class DBObject implements iDisplay $this->OnUpdate(); $this->FireEvent(EVENT_SERVICE_BEFORE_UPDATE); + $this->InitPreviousValuesForUpdatedAttributes(); $aChanges = $this->ListChanges(); if (count($aChanges) == 0) { // Attempting to update an unchanged object unset($aUpdateReentrance[$sKey]); + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBUpdate $sKey Aborted (no change)", LogChannels::DM_CRUD); + } return $this->m_iKey; } @@ -3218,7 +3233,7 @@ abstract class DBObject implements iDisplay { throw new CoreCannotSaveObjectException(array( 'issues' => $aIssues, - 'class' => get_class($this), + 'class' => $sClass, 'id' => $this->GetKey() )); } @@ -3227,7 +3242,6 @@ abstract class DBObject implements iDisplay $aOriginalValues = $this->m_aOrigValues; // Activate any existing trigger - $sClass = get_class($this); // - TriggerOnObjectMention $this->ActivateOnMentionTriggers(false); @@ -3236,7 +3250,7 @@ abstract class DBObject implements iDisplay $aDBChanges = array(); foreach ($aChanges as $sAttCode => $valuecurr) { - $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); if ($oAttDef->IsExternalKey()) { $bHasANewExternalKeyValue = true; @@ -3275,7 +3289,7 @@ abstract class DBObject implements iDisplay // Update the left & right indexes for each hierarchical key foreach ($aHierarchicalKeys as $sAttCode => $oAttDef) { - $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode); + $sTable = MetaModel::DBGetTable($sClass, $sAttCode); $sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey(); $aRes = CMDBSource::QueryToArray($sSQL); $iMyLeft = $aRes[0]['left']; @@ -3315,7 +3329,7 @@ abstract class DBObject implements iDisplay // Update scalar attributes if (count($aDBChanges) != 0) { - $oFilter = new DBObjectSearch(get_class($this)); + $oFilter = new DBObjectSearch($sClass); $oFilter->AddCondition('id', $this->m_iKey, '='); $oFilter->AllowAllData(); @@ -3360,7 +3374,7 @@ abstract class DBObject implements iDisplay $aErrors = array($e->getMessage()); throw new CoreCannotSaveObjectException(array( 'id' => $this->GetKey(), - 'class' => get_class($this), + 'class' => $sClass, 'issues' => $aErrors ), $e); } @@ -3383,7 +3397,7 @@ abstract class DBObject implements iDisplay $aErrors = array($e->getMessage()); throw new CoreCannotSaveObjectException(array( 'id' => $this->GetKey(), - 'class' => get_class($this), + 'class' => $sClass, 'issues' => $aErrors, )); } @@ -3412,7 +3426,6 @@ abstract class DBObject implements iDisplay } $this->AfterUpdate(); - $this->FireEvent(EVENT_SERVICE_AFTER_UPDATE); // Reload to get the external attributes if ($bHasANewExternalKeyValue) { @@ -3432,13 +3445,18 @@ abstract class DBObject implements iDisplay catch (Exception $e) { $aErrors = array($e->getMessage()); - throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => get_class($this), 'issues' => $aErrors)); + throw new CoreCannotSaveObjectException(array('id' => $this->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); } } finally { unset($aUpdateReentrance[$sKey]); } + $this->FireEvent(EVENT_SERVICE_AFTER_UPDATE, ['changes' => $aChanges]); + + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: DBUpdate $sKey Updated", LogChannels::DM_CRUD); + } return $this->m_iKey; } @@ -3931,7 +3949,9 @@ abstract class DBObject implements iDisplay IssueLog::Error("$sClass: Transition $sStimulusCode is not allowed in ".$this->Get($sStateAttCode)); return false; } - + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: ApplyStimulus $sStimulusCode $sClass::{$this->m_iKey} Starting", LogChannels::DM_CRUD); + } // save current object values in case of an action failure (in memory rollback) $aBackupValues = array(); foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) { @@ -3947,7 +3967,7 @@ abstract class DBObject implements iDisplay $aTransitionDef = $aStateTransitions[$sStimulusCode]; - $this->FireEvent(EVENT_SERVICE_BEFORE_APPLY_STIMULUS); + $this->FireEvent(EVENT_SERVICE_BEFORE_APPLY_STIMULUS, ['stimulus' => $sStimulusCode]); // Change the state before proceeding to the actions, this is necessary because an action might // trigger another stimuli (alternative: push the stimuli into a queue) @@ -4067,7 +4087,7 @@ abstract class DBObject implements iDisplay } } - $this->FireEvent(EVENT_SERVICE_AFTER_APPLY_STIMULUS); + $this->FireEvent(EVENT_SERVICE_AFTER_APPLY_STIMULUS, ['stimulus' => $sStimulusCode]); } else { @@ -4077,7 +4097,9 @@ abstract class DBObject implements iDisplay $this->m_aCurrValues[$sAttCode] = $aBackupValues[$sAttCode]; } } - + if ($sClass == 'UserRequest') { + IssueLog::Debug("CRUD: ApplyStimulus $sStimulusCode $sClass::{$this->m_iKey} Ending", LogChannels::DM_CRUD); + } return $bSuccess; } diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php index e2510555c2..a61e7f31cd 100644 --- a/core/kpi.class.inc.php +++ b/core/kpi.class.inc.php @@ -15,8 +15,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -use Combodo\iTop\Service\EventData; -use Combodo\iTop\Service\EventService; /** @@ -39,8 +37,6 @@ class ExecutionKPI * @var array[ExecutionKPI] */ static protected $m_aExecutionStack = []; // embedded execution stats - // Event listener - static protected $oKPIEventListener; protected $m_fStarted = null; protected $m_fChildrenDuration = 0; // Count embedded @@ -292,9 +288,6 @@ class ExecutionKPI public function __construct() { - if (is_null(static::$oKPIEventListener)) { - static::$oKPIEventListener = new KPIEventListener(); - } $this->ResetCounters(); self::Push($this); } @@ -306,7 +299,7 @@ class ExecutionKPI */ private static function Push(ExecutionKPI $oExecutionKPI) { - array_push(self::$m_aExecutionStack, $oExecutionKPI); + self::$m_aExecutionStack[] = $oExecutionKPI; } /** @@ -456,21 +449,3 @@ class ExecutionKPI return 0; } } - -class KPIEventListener -{ - // Data model is not yet loaded, so EVENT_SERVICE_DB_OBJECT_LOADED is not yet defined - const EVENT_SERVICE_DB_OBJECT_LOADED = 'DBObjectLoaded'; - - public function __construct() - { - EventService::RegisterListener(self::EVENT_SERVICE_DB_OBJECT_LOADED, [get_class($this), 'OnObjectLoaded']); - } - - public static function OnObjectLoaded(EventData $oEventData) - { - $oObject = $oEventData->GetEventData()['object']; - $oKPI = new ExecutionKPI(); - $oKPI->ComputeStats('Objects Loaded (no duration)', get_class($oObject)); - } -} \ No newline at end of file diff --git a/core/log.class.inc.php b/core/log.class.inc.php index 7597bad666..311653ad5a 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -568,6 +568,14 @@ class LogChannels public const INLINE_IMAGE = 'InlineImage'; public const PORTAL = 'portal'; + + /** + * @var string + * @since 3.1.0 specific channel for event service + */ + public const EVENT_SERVICE = 'EventService'; + + public const DM_CRUD = 'DMCRUD'; } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index a688e4c6d8..30afdde343 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -357,6 +357,7 @@ return array( 'Combodo\\iTop\\Renderer\\FormRenderer' => $baseDir . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => $baseDir . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\Service\\EventData' => $baseDir . '/sources/application/Service/EventData.php', + 'Combodo\\iTop\\Service\\EventHelper' => $baseDir . '/sources/application/Service/EventHelper.php', 'Combodo\\iTop\\Service\\EventService' => $baseDir . '/sources/application/Service/EventService.php', 'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php', 'CompileCSSService' => $baseDir . '/application/compilecssservice.class.inc.php', @@ -499,7 +500,6 @@ return array( 'JSButtonItem' => $baseDir . '/application/applicationextension.inc.php', 'JSPopupMenuItem' => $baseDir . '/application/applicationextension.inc.php', 'JsonPage' => $baseDir . '/sources/application/WebPage/JsonPage.php', - 'KPIEventListener' => $baseDir . '/core/kpi.class.inc.php', 'KeyValueStore' => $baseDir . '/core/counter.class.inc.php', 'ListExpression' => $baseDir . '/core/oql/expression.class.inc.php', 'ListOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 4e57a99398..2151d531d8 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -587,6 +587,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Renderer\\FormRenderer' => __DIR__ . '/../..' . '/sources/Renderer/FormRenderer.php', 'Combodo\\iTop\\Renderer\\RenderingOutput' => __DIR__ . '/../..' . '/sources/Renderer/RenderingOutput.php', 'Combodo\\iTop\\Service\\EventData' => __DIR__ . '/../..' . '/sources/application/Service/EventData.php', + 'Combodo\\iTop\\Service\\EventHelper' => __DIR__ . '/../..' . '/sources/application/Service/EventHelper.php', 'Combodo\\iTop\\Service\\EventService' => __DIR__ . '/../..' . '/sources/application/Service/EventService.php', 'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php', 'CompileCSSService' => __DIR__ . '/../..' . '/application/compilecssservice.class.inc.php', @@ -729,7 +730,6 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'JSButtonItem' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'JSPopupMenuItem' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', 'JsonPage' => __DIR__ . '/../..' . '/sources/application/WebPage/JsonPage.php', - 'KPIEventListener' => __DIR__ . '/../..' . '/core/kpi.class.inc.php', 'KeyValueStore' => __DIR__ . '/../..' . '/core/counter.class.inc.php', 'ListExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php', 'ListOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', diff --git a/sources/Application/Service/EventHelper.php b/sources/Application/Service/EventHelper.php new file mode 100644 index 0000000000..3d3a7ffdc6 --- /dev/null +++ b/sources/Application/Service/EventHelper.php @@ -0,0 +1,42 @@ +GetEventSource(); $oKPI = new ExecutionKPI(); $sSource = isset($aEventData['debug_info']) ? " {$aEventData['debug_info']}" : ''; - $sEventName = "$sEvent:".self::GetSourcesAsString($eventSource); - IssueLog::Trace("Fire event '$sEventName'$sSource", LOG_EVENT_SERVICE_CHANNEL); + $sLogEventName = "$sEvent - ".self::GetSourcesAsString($eventSource); + IssueLog::Trace("Fire event '$sLogEventName'$sSource", LogChannels::EVENT_SERVICE); if (!isset(self::$aEventListeners[$sEvent])) { - IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("No registration found for event '$sEvent'", LogChannels::EVENT_SERVICE); $oKPI->ComputeStats('FireEvent', $sEvent); return; @@ -105,18 +104,26 @@ class EventService continue; } $sName = $aEventCallback['name']; - IssueLog::Debug("Fire event '$sEventName'$sSource calling '$sName'", LOG_EVENT_SERVICE_CHANNEL); + // Reentrance protection + if (isset(self::$aReentranceProtection[$aEventCallback['id']])) { + if (!self::$aReentranceProtection[$aEventCallback['id']]) { + unset(self::$aReentranceProtection[$aEventCallback['id']]); + } + IssueLog::Debug("Reentrance protection for '$sLogEventName'$sSource for '$sName'", LogChannels::EVENT_SERVICE); + continue; + } + IssueLog::Debug("Fire event '$sLogEventName'$sSource calling '$sName'", LogChannels::EVENT_SERVICE); try { if (is_callable($aEventCallback['callback'])) { $oEventData->SetCallbackData($aEventCallback['data']); call_user_func($aEventCallback['callback'], $oEventData); } else { - IssueLog::Debug("Callback '$sName' not a callable anymore, unregister", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Debug("Callback '$sName' not a callable anymore, unregister", LogChannels::EVENT_SERVICE); self::UnRegisterCallback($aEventCallback['id']); } } catch (Exception $e) { - IssueLog::Error("Event '$sEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage()); + IssueLog::Error("Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage()); throw $e; } } @@ -134,19 +141,19 @@ class EventService return false; } if (is_string($srcRegistered)) { - $aSrcRegistered = array($srcRegistered); + $aSrcRegistered = [$srcRegistered]; } elseif (is_array($srcRegistered)) { $aSrcRegistered = $srcRegistered; } else { - $aSrcRegistered = array(); + $aSrcRegistered = []; } if (is_string($srcEvent)) { - $aSrcEvent = array($srcEvent); + $aSrcEvent = [$srcEvent]; } elseif (is_array($srcEvent)) { $aSrcEvent = $srcEvent; } else { - $aSrcEvent = array(); + $aSrcEvent = []; } foreach ($aSrcEvent as $sSrcEvent) { @@ -187,10 +194,10 @@ class EventService return ''; } if (is_string($srcRegistered)) { - return $srcRegistered; + return substr($srcRegistered, 0, 30); } if (is_array($srcRegistered)) { - return implode(',', $srcRegistered); + return substr(implode(',', $srcRegistered), 0, 30); } return ''; @@ -207,7 +214,7 @@ class EventService if ($aEventCallback['id'] == $sId) { $sName = self::$aEventListeners[$sEvent][$idx]['name']; unset (self::$aEventListeners[$sEvent][$idx]); - IssueLog::Trace("Unregistered callback '$sName' id $sId' on event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("Unregistered callback '$sName' id $sId' on event '$sEvent'", LogChannels::EVENT_SERVICE); return false; } @@ -216,7 +223,7 @@ class EventService }); if (!$bRemoved) { - IssueLog::Trace("No registration found for callback '$sId'", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("No registration found for callback '$sId'", LogChannels::EVENT_SERVICE); } } @@ -228,13 +235,13 @@ class EventService public static function UnRegisterEvent(string $sEvent) { if (!isset(self::$aEventListeners[$sEvent])) { - IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("No registration found for event '$sEvent'", LogChannels::EVENT_SERVICE); return; } unset(self::$aEventListeners[$sEvent]); - IssueLog::Trace("Unregistered all the callbacks on event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("Unregistered all the callbacks on event '$sEvent'", LogChannels::EVENT_SERVICE); } /** @@ -243,7 +250,7 @@ class EventService public static function UnRegisterAll() { self::$aEventListeners = array(); - IssueLog::Trace("Unregistered all events", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Trace("Unregistered all events", LogChannels::EVENT_SERVICE); } /** @@ -271,7 +278,7 @@ class EventService { if (isset(self::$aEventDescription[$sEvent])) { $sPrevious = self::$aEventDescription[$sEvent]['module']; - IssueLog::Error("The Event $sEvent defined by $sModule has already been defined in $sPrevious, check your delta", LOG_EVENT_SERVICE_CHANNEL); + IssueLog::Error("The Event $sEvent defined by $sModule has already been defined in $sPrevious, check your delta", LogChannels::EVENT_SERVICE); } self::$aEventDescription[$sEvent] = [ @@ -295,9 +302,31 @@ class EventService return implode('_', $aRet); } - public static function GetDefinedEventsAsJSON() { return json_encode(self::$aEventDescription, JSON_PRETTY_PRINT); } + + /** + * @param string $sListenerId The id given by the RegisterListener() method + * @param bool $bPermanentProtection Be very careful when setting false, infinite loops can occur + * + * @return void + */ + public static function EnableReentranceProtection($sListenerId, $bPermanentProtection = true) + { + self::$aReentranceProtection[$sListenerId] = $bPermanentProtection; + } + + /** + * @param string $sListenerId The id given by the RegisterListener() method + * + * @return void + */ + public static function DisableReentranceProtection($sListenerId) + { + if (isset(self::$aReentranceProtection[$sListenerId])) { + unset(self::$aReentranceProtection[$sListenerId]); + } + } } diff --git a/test/service/EventTest.php b/test/service/EventTest.php index 9c5f26e355..4bf8918c80 100644 --- a/test/service/EventTest.php +++ b/test/service/EventTest.php @@ -6,6 +6,7 @@ use Combodo\iTop\Service\EventData; use Combodo\iTop\Service\EventService; use Combodo\iTop\Test\UnitTest\ItopTestCase; use ContextTag; +use Exception; use TypeError; /** @@ -55,7 +56,10 @@ class EventTest extends ItopTestCase public function testNoParameterCallbackFunction() { - $sId = EventService::RegisterListener('event', function () { $this->debug("Closure: event received !!!"); self::IncrementCallCount(); }); + $sId = EventService::RegisterListener('event', function () { + $this->debug("Closure: event received !!!"); + self::IncrementCallCount(); + }); $this->debug("Registered $sId"); EventService::FireEvent(new EventData('event')); $this->assertEquals(1, self::$iEventCalls); @@ -81,9 +85,10 @@ class EventTest extends ItopTestCase public function GoodCallbackProvider() { $oReceiver = new TestEventReceiver(); + return array( - 'method' => array(array($oReceiver, 'OnEvent1')), - 'static' => array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1'), + 'method' => array(array($oReceiver, 'OnEvent1')), + 'static' => array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1'), 'static2' => array(array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver', 'OnStaticEvent1')), ); } @@ -291,6 +296,157 @@ class EventTest extends ItopTestCase self::$iEventCalls++; } + /** + * @dataProvider ReentranceProvider + */ + public function testReentrance($aClasses, $iEventCount) + { + foreach ($aClasses as $sName => $aClass) { + new TestReentrance($sName, $aClass['prio'], $aClass['events'], $aClass['permanent_protection']); + } + EventService::FireEvent(new EventData('event1', 'main')); + $this->assertEquals($iEventCount, self::$iEventCalls); + } + + public function ReentranceProvider() + { + return [ + '1 class' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + ], + 'iEventCount' => 1, + ], + '2 classes - 1' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + ], + 'iEventCount' => 3, + ], + '2 classes - 2' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + ], + 'iEventCount' => 4, + ], + '2 classes - 3' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => true]]], + ], + 'iEventCount' => 3, + ], + '3 classes - 1' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => true]]], + ], + 'iEventCount' => 11, + ], + '3 classes - non permanent' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => false, 'event2' => true], 'listener2' => ['event1' => false, 'event2' => true]]], + ], + 'iEventCount' => 12, + ], + '2 classes - loop' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + ], + 'iEventCount' => 4, + ], + '2 classes - loop2' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['event1' => true, 'event2' => false], 'listener2' => ['event1' => false, 'event2' => false]]], + ], + 'iEventCount' => 5, + ], + ]; + } + + /** + * @dataProvider ReentranceCRUDProvider + */ + public function testReentranceCRUD($aClasses, $iEventCount) + { + foreach ($aClasses as $sName => $aClass) { + new TestReentranceCRUD($sName, $aClass['prio'], $aClass['events'], $aClass['permanent_protection']); + } + $oObject = new TestCRUDObject(); + $oObject->DBInsert('main'); + $this->assertEquals($iEventCount, self::$iEventCalls); + } + + public function ReentranceCRUDProvider() + { + return [ + '1 class' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + ], + 'iEventCount' => 1, + ], + '2 classes - 1' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + ], + 'iEventCount' => 2, + ], + '2 classes - 2'=> [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + ], + 'iEventCount' => 4, + ], + '2 classes - 3' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => true]]], + ], + 'iEventCount' => 3, + ], + '3 classes - 1' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => true]]], + ], + 'iEventCount' => 11, + ], + '3 classes - non permanent' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => false]]], + 'Class C' => ['prio' => 20, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => true], 'listener2' => ['update' => true]]], + ], + 'iEventCount' => 12, + ], + '2 classes - loop' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + ], + 'iEventCount' => 4, + ], + '2 classes - loop2' => [ + 'aClasses' => [ + 'Class A' => ['prio' => 0, 'permanent_protection' => false, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + 'Class B' => ['prio' => 10, 'permanent_protection' => true, 'events' => ['listener1' => ['update' => false], 'listener2' => ['update' => false]]], + ], + 'iEventCount' => 5, + ], + ]; + } + /** * static version of the debug to be accessible from other objects * @@ -298,21 +454,50 @@ class EventTest extends ItopTestCase */ public static function DebugStatic($sMsg) { - if (DEBUG_UNIT_TEST) - { - if (is_string($sMsg)) - { + if (DEBUG_UNIT_TEST) { + if (is_string($sMsg)) { echo "$sMsg\n"; - } - else - { + } else { print_r($sMsg); } } } } -class TestEventReceiver +class TestClassesWithDebug +{ + /** + * static version of the debug to be accessible from other objects + * + * @param $sMsg + */ + public static function DebugStatic($sMsg) + { + if (DEBUG_UNIT_TEST) { + if (is_string($sMsg)) { + echo "$sMsg\n"; + } else { + print_r($sMsg); + } + } + } + + /** + * @param $sMsg + */ + public function Debug($sMsg) + { + if (DEBUG_UNIT_TEST) { + if (is_string($sMsg)) { + echo "$sMsg\n"; + } else { + print_r($sMsg); + } + } + } +} + +class TestEventReceiver extends TestClassesWithDebug { // Event callbacks @@ -363,44 +548,201 @@ class TestEventReceiver self::DebugStatic(__METHOD__.": received event '{$sEvent}'"); EventTest::IncrementCallCount(); } - - /** - * static version of the debug to be accessible from other objects - * - * @param $sMsg - */ - public static function DebugStatic($sMsg) - { - if (DEBUG_UNIT_TEST) - { - if (is_string($sMsg)) - { - echo "$sMsg\n"; - } - else - { - print_r($sMsg); - } - } - } - /** - * static version of the debug to be accessible from other objects - * - * @param $sMsg - */ - public function Debug($sMsg) - { - if (DEBUG_UNIT_TEST) - { - if (is_string($sMsg)) - { - echo "$sMsg\n"; - } - else - { - print_r($sMsg); - } - } - } - } + +class TestCRUDObject extends TestClassesWithDebug +{ + private static $aEventList = []; + + public function DBInsert($sName) + { + $this->sName = $sName; + $this->FireEvent('insert'); + } + + public function DBUpdate($sName) + { + $this->sName = $sName; + $this->FireEvent('update'); + } + + public function FireEvent($sEvent, $aEventData = []) + { + $aEventData['name'] = $this->sName; + $aEventData['object'] = $this; + $oEventData = new EventData($sEvent, $this->sName, $aEventData); + + $bFireEventNow = empty(self::$aEventList['event']); + self::$aEventList['event'][] = $oEventData; + + $iCount = 0; + while ($bFireEventNow) { + $sEvent = $oEventData->GetEvent(); + $sName = $oEventData->Get('name'); + $this->Debug("Object from $sName: Fire event '$sEvent'"); + EventService::FireEvent($oEventData); + $this->Debug("Object from $sName: End of event '$sEvent'"); + + array_shift(self::$aEventList['event']); + $oEventData = reset(self::$aEventList['event']); + + if ($oEventData === false) { + $bFireEventNow = false; + } + if ($iCount++ > 10) { + throw new Exception('Infinite loop'); + } + } + } +} + +class TestReentranceCRUD extends TestClassesWithDebug +{ + public $sName; + public $sEvent1; + public $sEvent2; + public $bPermanentProtection; + + private static $iIndent1 = 0; + private static $iIndent2 = 0; + + public $aSendEvent = []; + + /** + * @param $sName + * @param $fPriority + * @param array $aSendEvent + * @param bool $bPermanentProtection + */ + public function __construct($sName, $fPriority, array $aSendEvent, $bPermanentProtection) + { + $this->sName = $sName; + $this->sEvent1 = EventService::RegisterListener('insert', [$this, 'OnInsert'], null, [], null, $fPriority); + $this->sEvent2 = EventService::RegisterListener('update', [$this, 'OnUpdate'], null, [], null, $fPriority); + $this->aSendEvent = $aSendEvent; + $this->bPermanentProtection = $bPermanentProtection; + } + + public function OnInsert(EventData $oData) + { + $sIndent = str_repeat(' ', self::$iIndent1); + $oObject = $oData->Get('object'); + $sEvent = $oData->GetEvent(); + $sSource = $oData->GetEventSource(); + $sName = $sIndent.$this->sName; + $this->Debug("$sName: received event '$sEvent' from '$sSource'"); + EventTest::IncrementCallCount(); + + if ($this->aSendEvent['listener1']['update']) { + self::$iIndent2 += 1; + $this->Debug("$sName: Update"); + EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection); + $oObject->DBUpdate($sName); + EventService::DisableReentranceProtection($this->sEvent2); + self::$iIndent2 -= 1; + } + } + + public function OnUpdate(EventData $oData) + { + $sIndent = str_repeat(' ', self::$iIndent2); + $oObject = $oData->Get('object'); + $sEvent = $oData->GetEvent(); + $sSource = $oData->GetEventSource(); + $sName = $sIndent.$this->sName; + $this->Debug("$sName: received event '$sEvent' from '$sSource'"); + EventTest::IncrementCallCount(); + if ($this->aSendEvent['listener2']['update']) { + self::$iIndent2 += 1; + $this->Debug("$sName: Update"); + EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection); + $oObject->DBUpdate($sName); + EventService::DisableReentranceProtection($this->sEvent2); + self::$iIndent2 -= 1; + } + } +} + +class TestReentrance extends TestClassesWithDebug +{ + public $sName; + public $sEvent1; + public $sEvent2; + public $bPermanentProtection; + + private static $iIndent1 = 0; + private static $iIndent2 = 0; + + public $aSendEvent = []; + + /** + * @param $sName + * @param $fPriority + * @param array $aSendEvent + * @param bool $bPermanentProtection + */ + public function __construct($sName, $fPriority, array $aSendEvent, $bPermanentProtection) + { + $this->sName = $sName; + $this->sEvent1 = EventService::RegisterListener('event1', [$this, 'ListenerEvent1'], null, [], null, $fPriority); + $this->sEvent2 = EventService::RegisterListener('event2', [$this, 'ListenerEvent2'], null, [], null, $fPriority); + $this->aSendEvent = $aSendEvent; + $this->bPermanentProtection = $bPermanentProtection; + } + + public function ListenerEvent1(EventData $oData) + { + $sIndent = str_repeat(' ', self::$iIndent1); + $sEvent = $oData->GetEvent(); + $sSource = $oData->GetEventSource(); + $sName = $sIndent.$this->sName; + $this->Debug("$sName: received event '$sEvent' from '$sSource'"); + EventTest::IncrementCallCount(); + if ($this->aSendEvent['listener1']['event1']) { + $this->Debug("$sName: Fire event 'event1'"); + self::$iIndent1 += 1; + EventService::EnableReentranceProtection($this->sEvent1, $this->bPermanentProtection); + EventService::FireEvent(new EventData('event1', $sName)); + EventService::DisableReentranceProtection($this->sEvent1); + self::$iIndent1 -= 1; + $this->Debug("$sName: End of event 'event1'"); + } + if ($this->aSendEvent['listener1']['event2']) { + $this->Debug("$sName: Fire event 'event2'"); + self::$iIndent2 += 1; + EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection); + EventService::FireEvent(new EventData('event2', $sName)); + EventService::DisableReentranceProtection($this->sEvent2); + self::$iIndent2 -= 1; + $this->Debug("$sName: End of event 'event2'"); + } + } + + public function ListenerEvent2(EventData $oData) + { + $sIndent = str_repeat(' ', self::$iIndent2); + $sEvent = $oData->GetEvent(); + $sSource = $oData->GetEventSource(); + $sName = $sIndent.$this->sName; + $this->Debug("$sName: received event '$sEvent' from '$sSource'"); + EventTest::IncrementCallCount(); + if ($this->aSendEvent['listener2']['event1']) { + $this->Debug("$sName: Fire event 'event1'"); + self::$iIndent1 += 1; + EventService::EnableReentranceProtection($this->sEvent1, $this->bPermanentProtection); + EventService::FireEvent(new EventData('event1', $sName)); + EventService::DisableReentranceProtection($this->sEvent1); + self::$iIndent1 -= 1; + $this->Debug("$sName: End of event 'event1'"); + } + if ($this->aSendEvent['listener2']['event2']) { + $this->Debug("$sName: Fire event 'event2'"); + self::$iIndent2 += 1; + EventService::EnableReentranceProtection($this->sEvent2, $this->bPermanentProtection); + EventService::FireEvent(new EventData('event2', $sName)); + EventService::DisableReentranceProtection($this->sEvent2); + self::$iIndent2 -= 1; + $this->Debug("$sName: End of event 'event2'"); + } + } +} \ No newline at end of file