diff --git a/application/datamodel.application.xml b/application/datamodel.application.xml index 25653c760..8b043a29f 100644 --- a/application/datamodel.application.xml +++ b/application/datamodel.application.xml @@ -188,300 +188,347 @@ An object insert in the database has been requested. All changes to the object will be persisted automatically. + + cmdbAbstractObject + DBObject::OnInsert - - + + The object inserted DBObject - - + + Debug string string - - + + An object is about to be inserted in the database (no change possible) + + cmdbAbstractObject + DBObject::OnInsert - - + + The object inserted DBObject - - + + Debug string string - - + + An object has been inserted into the database (but not reloaded). All changes to the object will be persisted automatically. + + cmdbAbstractObject + DBObject::AfterInsert - - + + The object inserted DBObject - - + + Debug string string - - + + An object update has been requested. All changes to the object will be persisted automatically. + + cmdbAbstractObject + DBObject::OnUpdate, DBObject::DoComputeValues - - + + The object updated DBObject - - + + Debug string string - - + + An object is about to be updated in the database (no change possible) + + cmdbAbstractObject + DBObject::OnUpdate - - + + The object updated DBObject - - + + Debug string string - - + + An object has been updated into the database and reloaded. All changes to the object will be persisted automatically. + + cmdbAbstractObject + DBObject::AfterUpdate - - + + The object updated DBObject - - + + Debug string string - - + + An object is about to be deleted in the database + + cmdbAbstractObject + DBObject::OnDelete - - + + The object deleted DBObject - - + + Debug string string - - + + An object has been deleted into the database + + cmdbAbstractObject + DBObject::AfterDelete - - + + The object deleted DBObject - - + + Debug string string - - + + A stimulus is about to be applied to an object - - + + cmdbAbstractObject + + + The object where the stimulus is targeted DBObject - - + + Current stimulus applied string - - + + Object previous state string - - + + Object new state string - - + + The object must be saved in the database boolean - - + + Debug string string - - + + A stimulus has been applied to an object - - + + cmdbAbstractObject + + + The object where the stimulus is targeted DBObject - - + + Current stimulus applied string - - + + Object previous state string - - + + Object new state string - - + + The object is asked to be saved in the database boolean - - + + Debug string string - - + + A stimulus has failed - - + + cmdbAbstractObject + + + The action that failed to apply the stimulus string - - + + The object where the stimulus is targeted DBObject - - + + Current stimulus applied string - - + + Object previous state string - - + + Object new state string - - + + The object must be saved in the database boolean - - + + Debug string string - - + + An object has been re-loaded from the database - - + + cmdbAbstractObject + + + The object re-loaded DBObject - - + + Debug string string - - + + Check an object before it is written into the database (no change possible) + + cmdbAbstractObject + cmdbAbstractObject::DoCheckToWrite - - + + The object to check DBObject - - + + Array of strings where all the errors found during the object checking are added array - - + + Debug string string - - + + Check an object before it is deleted from the database (no change possible) + + cmdbAbstractObject + cmdbAbstractObject::DoCheckToDelete - - + + The object to check DBObject - - + + Array of strings where all the errors found during the object checking are added array - - + + Debug string string - - + + A document has been downloaded from the GUI - - + + ormDocument + + + The object containing the document DBObject - - + + The document downloaded ormDocument - - + + Debug string string - - + + The current page is completely displayed - Class hierarchy of the displayed page - - + + CLIPage + WebPage + + + The page displayed Page - - + + Debug string string - - + + Inform the listeners about the connection states - - - + + The login step result code (LoginWebPage::EXIT_CODE_...) integer - - + + Current login state (LoginWebPage::LOGIN_STATE_CONNECTED...) string - - + + diff --git a/core/designdocument.class.inc.php b/core/designdocument.class.inc.php index a9448859d..4384c2a7d 100644 --- a/core/designdocument.class.inc.php +++ b/core/designdocument.class.inc.php @@ -26,8 +26,8 @@ namespace Combodo\iTop; -use \DOMDocument; -use \DOMFormatException; +use DOMDocument; +use DOMFormatException; /** * Class \Combodo\iTop\DesignDocument @@ -175,6 +175,26 @@ class DesignElement extends \DOMElement return $this->ownerDocument->GetNodes($sXPath, $this); } + public static function ToArray(DesignElement $oNode) + { + $aRes = []; + + if ($oNode->GetNodes('./*')->length == 0) { + return $oNode->GetText(''); + } + foreach ($oNode->GetNodes('./*') as $oSubNode) { + /** @var \Combodo\iTop\DesignElement $oSubNode */ + $aSubArray = DesignElement::ToArray($oSubNode); + if ($oSubNode->hasAttribute('id')) { + $aRes[$oSubNode->getAttribute('id')] = $aSubArray; + } else { + $aRes[$oSubNode->tagName] = $aSubArray; + } + } + + return $aRes; + } + /** * Create an HTML representation of the DOM, for debugging purposes * diff --git a/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml b/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml index ebaed8844..07cb4c904 100755 --- a/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml +++ b/datamodels/2.x/itop-attachments/datamodel.itop-attachments.xml @@ -291,34 +291,40 @@ An attachment has been updated in database. Attachment::AfterUpdate - - + + Attachment + + + The attachment updated DBObject - - + + Array of all the attributes changed array - - + + Debug string string - - + + An attachment has been deleted from database. Attachment::AfterDelete - - + + Attachment + + + The attachment deleted DBObject - - + + Debug string string - - + + diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 9b88bc07e..e8fadd120 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -804,6 +804,11 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:Schema:DisplaySelector/Code' => 'Code~~', 'UI:Schema:Attribute/Filter' => 'Filter~~', 'UI:Schema:DefaultNullValue' => 'Default null : "%1$s"~~', + 'UI:Schema:Events' => 'Events', + 'UI:Schema:Events:Defined' => 'Defined events', + 'UI:Schema:Events:NoEvent' => 'No event defined', + 'UI:Schema:Events:Listeners' => 'Event listeners', + 'UI:Schema:Events:NoListener' => 'No event listener', 'UI:LinksWidget:Autocomplete+' => 'Type the first 3 characters...', 'UI:Edit:SearchQuery' => 'Select a predefined query', 'UI:Edit:TestQuery' => 'Test query', diff --git a/pages/schema.php b/pages/schema.php index eb9c1fd43..a807ddf8c 100644 --- a/pages/schema.php +++ b/pages/schema.php @@ -12,6 +12,7 @@ use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockF use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentWithSideContent; +use Combodo\iTop\Service\EventService; require_once('../approot.inc.php'); require_once(APPROOT.'/application/application.inc.php'); @@ -264,7 +265,58 @@ function DisplayTriggers($oPage, $sClass) cmdbAbstractObject::DisplaySet($oPage, $oSet, array('block_id' => 'triggers')); } +function DisplayEvents(WebPage $oPage, $sClass) +{ + $aEvents = EventService::GetEventsByClass($sClass); + $aColumns = [ + 'event' => ['label' => 'Event'], + 'description' => ['label' => 'Description'], + ]; + $aRows = []; + foreach ($aEvents as $sEvent => $aEventInfo) { + $aDesc = $aEventInfo['description']; + $aRows[] = [ + 'event' => $sEvent, + 'description' => $aDesc['description'] ?? '', + ]; + } + $oTable = DataTableUIBlockFactory::MakeForStaticData(Dict::S('UI:Schema:Events:Defined'), $aColumns, $aRows); + $oPage->AddSubBlock($oTable); + $oObject = MetaModel::NewObject($sClass); + $aSources = [$oObject->GetObjectUniqId()]; + foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sParentClass) { + $aSources[] = $sParentClass; + } + $aListeners = []; + foreach (array_keys($aEvents) as $sEvent) { + $aListeners = array_merge($aListeners, EventService::GetListeners($sEvent, $aSources)); + } + $aColumns = [ + 'event' => ['label' => 'Event'], + 'listener' => ['label' => 'Listener'], + 'priority' => ['label' => 'Priority'], + 'module' => ['label' => 'Module'], + ]; + $aRows = []; + foreach ($aListeners as $aListener) { + if (is_object($aListener['callback'][0])) { + $sListener = get_class($aListener['callback'][0]).'->'.$aListener['callback'][1].'(Combodo\iTop\Service\EventData $oEventData)'; + } else { + $sListener = $aListener['callback'][0].'::'.$aListener['callback'][1].'(Combodo\iTop\Service\EventData $oEventData)'; + } + $aRows[] = [ + 'event' => $aListener['event'], + 'listener' => $sListener, + 'priority' => $aListener['priority'], + 'module' => $aListener['module'], + ]; + } + + $oTable = DataTableUIBlockFactory::MakeForStaticData(Dict::S('UI:Schema:Events:Listeners'), $aColumns, $aRows); + $oPage->AddSubBlock($oTable); + +} /** * Display the list of classes from the business model */ @@ -1061,6 +1113,9 @@ EOF $oPage->SetCurrentTab('UI:Schema:Triggers'); DisplayTriggers($oPage, $sClass); + $oPage->SetCurrentTab('UI:Schema:Events'); + DisplayEvents($oPage, $sClass); + $oPage->SetCurrentTab(); $oPage->SetCurrentTabContainer(); } diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 417c2f064..9928d1f95 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1078,23 +1078,13 @@ EOF protected function CompileEvent(DesignElement $oEvent, string $sModuleName) { $sName = $oEvent->getAttribute('id'); - $oDescription = $oEvent->GetOptionalElement('description'); - $sDescription = empty($oDescription) ? '' : $oDescription->GetText(''); - $aArguments = []; - foreach ($oEvent->GetNodes('./arguments/argument') as $oArgumentNode) { - $aArg = []; - $sArgId = $oArgumentNode->getAttribute('id'); - $oArgDesc = $oArgumentNode->GetOptionalElement('description'); - $aArg['description'] = empty($oArgDesc) ? '' : $oArgDesc->GetText(''); - $oArgType = $oArgumentNode->GetOptionalElement('type'); - $aArg['type'] = empty($oArgType) ? '' : $oArgType->GetText(''); - $aArguments[$sArgId] = $aArg; - } - $sArguments = var_export($aArguments, true); + $aEventDescription = DesignElement::ToArray($oEvent); + + $sDescription = var_export($aEventDescription, true); $sConstant = $sName; $sOutput = "define('$sConstant', '$sName');\n"; - $sOutput .= "Combodo\iTop\Service\EventService::RegisterEvent('$sName', '$sDescription', $sArguments, '$sModuleName');\n"; + $sOutput .= "Combodo\iTop\Service\EventService::RegisterEvent('$sName', $sDescription, '$sModuleName');\n"; return $sOutput; } @@ -1329,7 +1319,7 @@ EOF } $sListenerPriority = (float)($oListener->GetChildText('priority', '0')); - $sEvents .= "\n Combodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", $sEventListener, \$this->m_sObjectUniqId, \"$sListenerId\", null, $sListenerPriority);"; + $sEvents .= "\n Combodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", $sEventListener, \$this->m_sObjectUniqId, \"$sListenerId\", null, $sListenerPriority, '$sModuleRelativeDir');"; } } @@ -3698,7 +3688,7 @@ EOF; $sEventSource = $aHook['source']; $sContext = $aHook['context']; $sPriority = $aHook['priority']; - $sRegister .= "\nCombodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", '$sClassName::$sCallback', $sEventSource, null, $sContext, $sPriority);"; + $sRegister .= "\nCombodo\iTop\Service\EventService::RegisterListener(\"$sEventName\", '$sClassName::$sCallback', $sEventSource, null, $sContext, $sPriority, '$sModuleId');"; $sCallbackFct = $aHook['content']; $sMethods .= "\n {$sCallbackFct}\n\n"; } diff --git a/sources/Application/Service/EventService.php b/sources/Application/Service/EventService.php index 3f09ea672..3bb1f4bb2 100644 --- a/sources/Application/Service/EventService.php +++ b/sources/Application/Service/EventService.php @@ -12,13 +12,13 @@ use Exception; use ExecutionKPI; use IssueLog; use LogChannels; +use ReflectionClass; class EventService { public static $aEventListeners = []; private static $iEventIdCounter = 0; private static $aEventDescription = []; - Private static $aReentranceProtection = []; /** * Register a callback for a specific event @@ -33,20 +33,24 @@ class EventService * @return string Id of the registration (used for unregistering) * */ - public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, $aCallbackData = [], $context = null, float $fPriority = 0.0): string + public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string { - is_callable($callback, false, $sName); + if (!is_callable($callback, false, $sName)) { + return false; + } $aEventCallbacks = self::$aEventListeners[$sEvent] ?? []; $sId = 'event_'.self::$iEventIdCounter++; $aEventCallbacks[] = array( 'id' => $sId, + 'event' => $sEvent, 'callback' => $callback, 'source' => $sEventSource, 'name' => $sName, 'data' => $aCallbackData, 'context' => $context, 'priority' => $fPriority, + 'module' => $sModuleId, ); usort($aEventCallbacks, function ($a, $b) { $fPriorityA = $a['priority']; @@ -96,31 +100,15 @@ class EventService return; } - foreach (self::$aEventListeners[$sEvent] as $aEventCallback) { - if (!self::MatchEventSource($aEventCallback['source'], $eventSource)) { - continue; - } + foreach (self::GetListeners($sEvent, $eventSource) as $aEventCallback) { if (!self::MatchContext($aEventCallback['context'])) { continue; } $sName = $aEventCallback['name']; - // 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", LogChannels::EVENT_SERVICE); - self::UnRegisterCallback($aEventCallback['id']); - } + $oEventData->SetCallbackData($aEventCallback['data']); + call_user_func($aEventCallback['callback'], $oEventData); } catch (Exception $e) { IssueLog::Error("Event '$sLogEventName' for '$sName' id {$aEventCallback['id']} failed with error: ".$e->getMessage()); @@ -130,6 +118,20 @@ class EventService $oKPI->ComputeStats('FireEvent', $sEvent); } + public static function GetListeners($sEvent, $eventSource) + { + $aListeners = []; + if (isset(self::$aEventListeners[$sEvent])) { + foreach (self::$aEventListeners[$sEvent] as $aEventCallback) { + if (self::MatchEventSource($aEventCallback['source'], $eventSource)) { + $aListeners[] = $aEventCallback; + } + } + } + + return $aListeners; + } + private static function MatchEventSource($srcRegistered, $srcEvent): bool { if (empty($srcRegistered)) { @@ -208,7 +210,7 @@ class EventService * * @param string $sId the callback registration id */ - public static function UnRegisterCallback(string $sId) + public static function UnRegisterListener(string $sId) { $bRemoved = self::Browse(function ($sEvent, $idx, $aEventCallback) use ($sId) { if ($aEventCallback['id'] == $sId) { @@ -232,7 +234,7 @@ class EventService * * @param string $sEvent event to unregister */ - public static function UnRegisterEvent(string $sEvent) + public static function UnRegisterEventListeners(string $sEvent) { if (!isset(self::$aEventListeners[$sEvent])) { IssueLog::Trace("No registration found for event '$sEvent'", LogChannels::EVENT_SERVICE); @@ -274,7 +276,7 @@ class EventService } // For information only - public static function RegisterEvent(string $sEvent, string $sDescription, array $aArguments, string $sModule) + public static function RegisterEvent(string $sEvent, array $aDescription, string $sModule) { if (isset(self::$aEventDescription[$sEvent])) { $sPrevious = self::$aEventDescription[$sEvent]['module']; @@ -282,14 +284,29 @@ class EventService } self::$aEventDescription[$sEvent] = [ - 'constant'=> 'EVENT_SERVICE_'.strtoupper(self::FromCamelCase($sEvent)), 'name'=> $sEvent, - 'description' => $sDescription, - 'arguments' => $aArguments, + 'description' => $aDescription, 'module' => $sModule, ]; } + public static function GetEventsByClass($sClass) + { + $aRes = []; + $oClass = new ReflectionClass($sClass); + foreach (self::$aEventDescription as $sEvent => $aEventInfo) { + if (isset($aEventInfo['description']['sources'])) { + foreach ($aEventInfo['description']['sources'] as $sSource) { + if ($sClass == $sSource || $oClass->isSubclassOf($sSource)) { + $aRes[$sEvent] = $aEventInfo; + } + } + } + } + + return $aRes; + } + // Intentionally duplicated from SetupUtils, not yet loaded when RegisterEvent is called private static function FromCamelCase($sInput) { $sPattern = '!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!'; diff --git a/test/service/EventTest.php b/test/service/EventTest.php index 0bfe93c3f..432a59b2a 100644 --- a/test/service/EventTest.php +++ b/test/service/EventTest.php @@ -228,7 +228,7 @@ class EventTest extends ItopTestCase EventService::FireEvent(new EventData('event2')); $this->assertEquals(4, self::$iEventCalls); - EventService::UnRegisterEvent('event1'); + EventService::UnRegisterEventListeners('event1'); EventService::FireEvent(new EventData('event1')); $this->assertEquals(4, self::$iEventCalls); @@ -282,7 +282,7 @@ class EventTest extends ItopTestCase EventService::FireEvent(new EventData('event2')); $this->assertEquals(4, self::$iEventCalls); - EventService::UnRegisterCallback($sIdToRemove); + EventService::UnRegisterListener($sIdToRemove); EventService::FireEvent(new EventData('event1')); $this->assertEquals(6, self::$iEventCalls);