From 8bfc54e6b471b2b6fd89e7d46b282a3330d76959 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 15 May 2020 20:42:05 +0200 Subject: [PATCH] Event Service --- application/applicationextension.inc.php | 4 +- application/cmdbabstract.class.inc.php | 4 + application/loginbasic.class.inc.php | 1 + application/loginexternal.class.inc.php | 3 +- application/loginform.class.inc.php | 1 + application/loginurl.class.inc.php | 3 +- application/loginwebpage.class.inc.php | 10 + core/datamodel.core.xml | 234 +++++++++- core/dbobject.class.php | 52 ++- core/log.class.inc.php | 36 +- core/ormdocument.class.inc.php | 10 +- .../src/Controller/ObjectController.php | 4 + lib/composer/autoload_classmap.php | 3 + lib/composer/autoload_static.php | 3 + setup/compiler.class.inc.php | 192 ++++++++- sources/application/Service/Event.php | 298 +++++++++++++ sources/application/Service/EventData.php | 84 ++++ sources/application/Service/EventName.php | 38 ++ test/phpunit.xml.dist | 3 + test/service/EventTest.php | 406 ++++++++++++++++++ 20 files changed, 1364 insertions(+), 25 deletions(-) create mode 100644 sources/application/Service/Event.php create mode 100644 sources/application/Service/EventData.php create mode 100644 sources/application/Service/EventName.php create mode 100644 test/service/EventTest.php diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 7b1baa97d..81a6d9c66 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -299,6 +299,7 @@ abstract class AbstractPreferencesExtension implements iPreferencesExtension * * @api * @package Extensibility + * @deprecated */ interface iApplicationUIExtension { @@ -441,6 +442,7 @@ interface iApplicationUIExtension * @api * @package Extensibility * @since 2.7.0 + * @deprecated */ abstract class AbstractApplicationUIExtension implements iApplicationUIExtension { @@ -1770,4 +1772,4 @@ class RestUtils interface iModuleExtension { public function __construct(); -} \ No newline at end of file +} diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index d0298dc2c..9978ece10 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -17,6 +17,8 @@ * You should have received a copy of the GNU Affero General Public License */ +use Combodo\iTop\Service\EventName; + define('OBJECT_PROPERTIES_TAB', 'ObjectProperties'); define('HILIGHT_CLASS_CRITICAL', 'red'); @@ -4162,6 +4164,7 @@ EOF $this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues); } } + $this->FireEvent(EventName::ON_CHECK_TO_WRITE, array('error_messages' => &$this->m_aCheckIssues)); // User rights // @@ -4210,6 +4213,7 @@ EOF $this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues); } } + $this->FireEvent(EventName::ON_CHECK_TO_DELETE, array('error_messages' => &$this->m_aDeleteIssues)); // User rights // diff --git a/application/loginbasic.class.inc.php b/application/loginbasic.class.inc.php index 660b45cba..c62336be8 100644 --- a/application/loginbasic.class.inc.php +++ b/application/loginbasic.class.inc.php @@ -56,6 +56,7 @@ class LoginBasic extends AbstractLoginFSMExtension list($sAuthUser, $sAuthPwd) = $this->GetAuthUserAndPassword(); if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $_SESSION['login_mode'], 'internal')) { + $_SESSION['auth_user'] = $sAuthUser; $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } diff --git a/application/loginexternal.class.inc.php b/application/loginexternal.class.inc.php index d4fcb7182..bbdede7c4 100644 --- a/application/loginexternal.class.inc.php +++ b/application/loginexternal.class.inc.php @@ -40,6 +40,7 @@ class LoginExternal extends AbstractLoginFSMExtension $sAuthUser = $this->GetAuthUser(); if (!UserRights::CheckCredentials($sAuthUser, '', $_SESSION['login_mode'], 'external')) { + $_SESSION['auth_user'] = $sAuthUser; $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } @@ -86,4 +87,4 @@ class LoginExternal extends AbstractLoginFSMExtension /** @var string $sAuthUser */ return $sAuthUser; // Retrieve the value } -} \ No newline at end of file +} diff --git a/application/loginform.class.inc.php b/application/loginform.class.inc.php index a4fb99db5..973a54396 100644 --- a/application/loginform.class.inc.php +++ b/application/loginform.class.inc.php @@ -68,6 +68,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension $sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data'); if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $_SESSION['login_mode'], 'internal')) { + $_SESSION['auth_user'] = $sAuthUser; $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } diff --git a/application/loginurl.class.inc.php b/application/loginurl.class.inc.php index 8a215e8f5..4fb3bbee4 100644 --- a/application/loginurl.class.inc.php +++ b/application/loginurl.class.inc.php @@ -55,6 +55,7 @@ class LoginURL extends AbstractLoginFSMExtension $sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data'); if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $_SESSION['login_mode'], 'internal')) { + $_SESSION['auth_user'] = $sAuthUser; $iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS; return LoginWebPage::LOGIN_FSM_ERROR; } @@ -90,4 +91,4 @@ class LoginURL extends AbstractLoginFSMExtension } return LoginWebPage::LOGIN_FSM_CONTINUE; } -} \ No newline at end of file +} diff --git a/application/loginwebpage.class.inc.php b/application/loginwebpage.class.inc.php index ab594b21d..1d48af07e 100644 --- a/application/loginwebpage.class.inc.php +++ b/application/loginwebpage.class.inc.php @@ -24,6 +24,9 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +use Combodo\iTop\Service\Event; +use Combodo\iTop\Service\EventName; + /** * Web page used for displaying the login form */ @@ -447,6 +450,7 @@ class LoginWebPage extends NiceWebPage $_SESSION['login_state'] = self::LOGIN_STATE_START; } $sLoginState = $_SESSION['login_state']; + $bFireEvent = ($sLoginState != self::LOGIN_STATE_CONNECTED); $sSessionLog = ''; if ($bLoginDebug) @@ -487,10 +491,15 @@ class LoginWebPage extends NiceWebPage $iResponse = $oLoginFSMExtensionInstance->LoginAction($sLoginState, $iErrorCode); if ($iResponse == self::LOGIN_FSM_RETURN) { + if ($bFireEvent) + { + Event::FireEvent(EventName::LOGIN, null, array('code' => $iErrorCode, 'state' => $sLoginState)); + } return $iErrorCode; // Asked to exit FSM, generally login OK } if ($iResponse == self::LOGIN_FSM_ERROR) { + Event::FireEvent(EventName::LOGIN, null, array('code' => $iErrorCode, 'state' => $sLoginState)); $sLoginState = self::LOGIN_STATE_SET_ERROR; // Next state will be error // An error was detected, skip the other plugins turn break; @@ -504,6 +513,7 @@ class LoginWebPage extends NiceWebPage } catch (Exception $e) { + Event::FireEvent(EventName::LOGIN, null, array('state' => $_SESSION['login_state'])); IssueLog::Error($e->getTraceAsString()); static::ResetSession(); die($e->getMessage()); diff --git a/core/datamodel.core.xml b/core/datamodel.core.xml index 624854e32..fd500739c 100644 --- a/core/datamodel.core.xml +++ b/core/datamodel.core.xml @@ -254,5 +254,237 @@ + + + An object details is about to be displayed to a user + Class hierarchy of the displayed object + + + The object displayed + DBObject + + + Debug string + string + + + + + An object is about to be inserted in the database + DBObject::OnInsert + + + The object inserted + DBObject + + + Debug string + string + + + + + A key (id) has been generated for an object inserted into the database (use GetKey() to read the new key) + DBObject::OnObjectKeyReady + + + The object inserted + DBObject + + + Debug string + string + + + + + An object has been inserted into the database + DBObject::AfterInsert + + + The object inserted + DBObject + + + Debug string + string + + + + + An object is about to be updated in the database + DBObject::OnUpdate + + + The object updated + DBObject + + + Debug string + string + + + + + An object has been updated into the database + DBObject::AfterUpdate + + + The object updated + DBObject + + + Debug string + string + + + + + An object is about to be deleted in the database + DBObject::OnDelete + + + The object deleted + DBObject + + + Debug string + string + + + + + An object has been deleted into the database + DBObject::AfterDelete + + + The object deleted + DBObject + + + Debug string + string + + + + + A stimulus is about to be applied to an object + + + The object where the stimulus is to be applied + DBObject + + + Debug string + string + + + + + A stimulus has been applied to an object + + + The object where the stimulus has been applied + DBObject + + + Debug string + string + + + + + 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 + + + The object re-loaded + DBObject + + + Debug string + string + + + + + Check an object before it is written into the database + 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 + 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 + + + The object containing the document + DBObject + + + The document downloaded + ormDocument + + + Debug string + string + + + + - \ No newline at end of file + diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 6b1745a12..c908ca7aa 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -15,6 +15,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +use Combodo\iTop\Service\Event; +use Combodo\iTop\Service\EventName; /** * All objects to be displayed in the application (either as a list or as details) @@ -153,9 +155,14 @@ abstract class DBObject implements iDisplay protected $m_aSynchroData = null; protected $m_sHighlightCode = null; protected $m_aCallbacks = array(); + /** + * @var string local events suffix + */ + protected $m_sEventUniqId = ''; + protected static $m_iEventUniqCounter = 0; - /** + /** * DBObject constructor. * * You should preferably use MetaModel::NewObject() instead of this constructor. @@ -179,6 +186,9 @@ abstract class DBObject implements iDisplay $this->m_bFullyLoaded = $this->IsFullyLoaded(); $this->m_aTouchedAtt = array(); $this->m_aModifiedAtt = array(); + $this->m_sEventUniqId = 'DataModel_'.self::$m_iEventUniqCounter++; + $this->RegisterEvents(); + $this->FireEvent(EventName::DB_OBJECT_LOADED); return; } // Creation of a brand new object @@ -205,6 +215,14 @@ abstract class DBObject implements iDisplay } $this->UpdateMetaAttributes(); + + $this->m_sEventUniqId = 'DataModel_'.self::$m_iEventUniqCounter++; + $this->RegisterEvents(); + $this->FireEvent(EventName::DB_OBJECT_NEW); + } + + protected function RegisterEvents() + { } /** @@ -351,6 +369,7 @@ abstract class DBObject implements iDisplay public function Reload($bAllowAllData = false) { assert($this->m_bIsInDB); + $this->FireEvent(EventName::DB_OBJECT_RELOAD); $aRow = MetaModel::MakeSingleRow(get_class($this), $this->m_iKey, false /* must be found */, true /* AllowAllData */); if (empty($aRow)) { @@ -2687,6 +2706,7 @@ abstract class DBObject implements iDisplay // Ensure the update of the values (we are accessing the data directly) $this->DoComputeValues(); $this->OnInsert(); + $this->FireEvent(EventName::BEFORE_INSERT); if ($this->m_iKey < 0) { @@ -2759,6 +2779,7 @@ abstract class DBObject implements iDisplay } $this->OnObjectKeyReady(); + $this->FireEvent(EventName::DB_OBJECT_KEY_READY); $this->DBWriteLinks(); $this->WriteExternalAttributes(); @@ -2789,6 +2810,7 @@ abstract class DBObject implements iDisplay } $this->AfterInsert(); + $this->FireEvent(EventName::AFTER_INSERT); // Activate any existing trigger $sClass = get_class($this); @@ -3037,6 +3059,8 @@ abstract class DBObject implements iDisplay } } $this->OnUpdate(); + $this->FireEvent(EventName::BEFORE_UPDATE); + $aChanges = $this->ListChanges(); if (count($aChanges) == 0) @@ -3239,6 +3263,7 @@ abstract class DBObject implements iDisplay try { $this->AfterUpdate(); + $this->FireEvent(EventName::AFTER_UPDATE); // Reload to get the external attributes if ($bHasANewExternalKeyValue) @@ -3357,6 +3382,7 @@ abstract class DBObject implements iDisplay } $this->OnDelete(); + $this->FireEvent(EventName::BEFORE_DELETE); // Activate any existing trigger $sClass = get_class($this); @@ -3458,6 +3484,8 @@ abstract class DBObject implements iDisplay } $this->AfterDelete(); + $this->FireEvent(EventName::AFTER_DELETE); + $this->m_bIsInDB = false; // Fix for N°926: do NOT reset m_iKey as it can be used to have it for reporting purposes (see the REST service to delete @@ -3643,6 +3671,8 @@ abstract class DBObject implements iDisplay $aTransitionDef = $aStateTransitions[$sStimulusCode]; + $this->FireEvent(EventName::BEFORE_APPLY_STIMULUS); + // 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) $sPreviousState = $this->Get($sStateAttCode); @@ -3754,6 +3784,8 @@ abstract class DBObject implements iDisplay /** @var \Trigger $oTrigger */ $oTrigger->DoActivate($this->ToArgs('this')); } + + $this->FireEvent(EventName::AFTER_APPLY_STIMULUS); } else { @@ -5384,5 +5416,23 @@ abstract class DBObject implements iDisplay } return $oExpression->Evaluate($aArgs); } + + /** + * @param $sEvent + * @param array $aEventData + * + * @throws \CoreException + */ + public function FireEvent($sEvent, $aEventData = array()) + { + $aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey(); + $aEventData['object'] = $this; + $aEventSources = array($this->m_sEventUniqId); + foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) + { + $aEventSources[] = $sClass; + } + Event::FireEvent($sEvent, $aEventSources, $aEventData); + } } diff --git a/core/log.class.inc.php b/core/log.class.inc.php index dfb0fa3e6..b25ba37d0 100644 --- a/core/log.class.inc.php +++ b/core/log.class.inc.php @@ -603,16 +603,24 @@ abstract class LogAPI } public static function Log($sLevel, $sMessage, $sChannel = null, $aContext = array()) + { + if (self::CanLog($sLevel, $sChannel)) + { + static::$m_oFileLog->$sLevel($sMessage, $sChannel, $aContext); + } + } + + public static function CanLog($sLevel, $sChannel = null) { if (! static::$m_oFileLog) { - return; + return false; } if (! isset(self::$aLevelsPriority[$sLevel])) { IssueLog::Error("invalid log level '{$sLevel}'"); - return; + return false; } if (is_null($sChannel)) @@ -621,25 +629,21 @@ abstract class LogAPI } $sMinLogLevel = self::GetMinLogLevel($sChannel); - if ($sMinLogLevel === false || $sMinLogLevel === 'false') { - return; + return false; } - if (is_string($sMinLogLevel)) + if (! isset(self::$aLevelsPriority[$sMinLogLevel])) { - if (! isset(self::$aLevelsPriority[$sMinLogLevel])) - { - throw new Exception("invalid configuration for log_level '{$sMinLogLevel}' is not within the list: ".implode(',', array_keys(self::$aLevelsPriority))); - } - elseif (self::$aLevelsPriority[$sLevel] < self::$aLevelsPriority[$sMinLogLevel]) - { - //priority too low regarding the conf, do not log this - return; - } + throw new Exception("invalid configuration for log_level '{$sMinLogLevel}' is not within the list: ".implode(',', array_keys(self::$aLevelsPriority))); + } + elseif (self::$aLevelsPriority[$sLevel] < self::$aLevelsPriority[$sMinLogLevel]) + { + //priority too low regarding the conf, do not log this + return false; } - static::$m_oFileLog->$sLevel($sMessage, $sChannel, $aContext); + return true; } /** @@ -830,4 +834,4 @@ class LogFileRotationProcess implements iScheduledProcess throw new ProcessException(self::class.' : The configured filename builder is invalid (log_filename_builder_impl="'.$sLogFileNameBuilder.'")'); } -} \ No newline at end of file +} diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index aa8b91eba..96e001c33 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -25,6 +25,9 @@ * @license http://opensource.org/licenses/AGPL-3.0 */ +use Combodo\iTop\Service\EventName; +use Combodo\iTop\Service\Event; + /** * ormDocument @@ -188,7 +191,6 @@ class ormDocument * @param string $sContentDisposition Either 'inline' or 'attachment' * @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file * @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField - * @return none */ public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) { @@ -207,6 +209,12 @@ class ormDocument $oDocument = $oObj->Get($sAttCode); if (is_object($oDocument)) { + $aEventData = array( + 'debug_info' => $oDocument->GetFileName(), + 'object' => $oObj, + 'document' => $oDocument, + ); + Event::FireEvent(EventName::DOWNLOAD_DOCUMENT, $sClass, $aEventData); $oPage->TrashUnexpectedOutput(); $oPage->SetContentType($oDocument->GetMimeType()); $oPage->SetContentDisposition($sContentDisposition,$oDocument->GetFileName()); diff --git a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php index c8efc1b78..a9318690b 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Controller/ObjectController.php @@ -28,6 +28,8 @@ use BinaryExpression; use Combodo\iTop\Portal\Brick\CreateBrick; use Combodo\iTop\Portal\Helper\ApplicationHelper; use Combodo\iTop\Portal\Helper\ContextManipulatorHelper; +use Combodo\iTop\Service\Event; +use Combodo\iTop\Service\EventName; use DBObject; use DBObjectSearch; use DBObjectSet; @@ -121,6 +123,8 @@ class ObjectController extends BrickController $sOperation = $oRequestManipulator->ReadParam('operation', ''); + $oObject->FireEvent(EventName::OBJECT_DETAILS); + $aData = array('sMode' => 'view'); $aData['form'] = $oObjectFormHandler->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId); $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 8f8e6b200..f8ac9d928 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -150,6 +150,9 @@ return array( 'Combodo\\iTop\\Composer\\iTopComposer' => $baseDir . '/sources/Composer/iTopComposer.php', 'Combodo\\iTop\\DesignDocument' => $baseDir . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => $baseDir . '/core/designdocument.class.inc.php', + 'Combodo\\iTop\\Service\\Event' => $baseDir . '/sources/application/Service/Event.php', + 'Combodo\\iTop\\Service\\EventData' => $baseDir . '/sources/application/Service/EventData.php', + 'Combodo\\iTop\\Service\\EventName' => $baseDir . '/sources/application/Service/EventName.php', 'Combodo\\iTop\\TwigExtension' => $baseDir . '/application/twigextension.class.inc.php', 'Config' => $baseDir . '/core/config.class.inc.php', 'ConfigException' => $baseDir . '/core/config.class.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 818c364ae..d8e1818b7 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -380,6 +380,9 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'Combodo\\iTop\\Composer\\iTopComposer' => __DIR__ . '/../..' . '/sources/Composer/iTopComposer.php', 'Combodo\\iTop\\DesignDocument' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', 'Combodo\\iTop\\DesignElement' => __DIR__ . '/../..' . '/core/designdocument.class.inc.php', + 'Combodo\\iTop\\Service\\Event' => __DIR__ . '/../..' . '/sources/application/Service/Event.php', + 'Combodo\\iTop\\Service\\EventData' => __DIR__ . '/../..' . '/sources/application/Service/EventData.php', + 'Combodo\\iTop\\Service\\EventName' => __DIR__ . '/../..' . '/sources/application/Service/EventName.php', 'Combodo\\iTop\\TwigExtension' => __DIR__ . '/../..' . '/application/twigextension.class.inc.php', 'Config' => __DIR__ . '/../..' . '/core/config.class.inc.php', 'ConfigException' => __DIR__ . '/../..' . '/core/config.class.inc.php', diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index dd9b32bce..adb31b42e 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -241,8 +241,9 @@ class MFCompiler $this->Log("Root class (with child classes): ".$oClass->getAttribute('id')); $this->aRootClasses[$oClass->getAttribute('id')] = $oClass; } - - $this->LoadSnippets(); + + $this->LoadSnippets(); + $this->LoadHooks(); // Compile, module by module // @@ -1119,6 +1120,67 @@ EOF $aClassParams['indexes'] = var_export($aIndexes, true); } + $sEvents = ''; + $sMethods = ''; + $oHooks = $oClass->GetOptionalElement('hooks'); + if ($oHooks) + { + foreach($oHooks->getElementsByTagName('hook') as $oHook) + { + /** @var \DOMElement $oHook */ + $sEventName = $oHook->getAttribute('id'); + $oListeners = $oHook->GetOptionalElement('listeners'); + if ($oListeners) + { + foreach ($oListeners->getElementsByTagName('listener') as $oListener) + { + /** @var \DOMElement $oListener */ + $sListenerId = $oListener->getAttribute('id'); + $oCallback = $oListener->GetUniqueElement('callback', false); + if (is_object($oCallback)) + { + $sCallback = $oCallback->GetText(); + } + else + { + $oExecute = $oListener->GetUniqueElement('execute', true); + $sExecute = trim($oExecute->GetText()); + $sCallback = "EventHook_{$sEventName}_{$sListenerId}"; + $sCallbackFct = preg_replace('@^function\s*\(@', "public function {$sCallback}(", $sExecute); + if ($sExecute == $sCallbackFct) + { + throw new DOMFormatException("Malformed tag in class: {$sClass} hook: {$sEventName} listener: {$sListenerId}"); + } + $sMethods .= "\n {$sCallbackFct}\n\n"; + } + if (strpos($sCallback, '::') === false) + { + $sEventListener = 'array($this, \''.$sCallback.'\')'; + } + else + { + $sEventListener = "'{$sCallback}'"; + } + + $sListenerPriority = (float)($oListener->GetChildText('priority', '0')); + $sEvents .= "\n Combodo\iTop\Service\Event::Register(\"$sEventName\", $sEventListener, \$this->m_sEventUniqId, \"$sListenerId\", null, $sListenerPriority);"; + } + } + } + } + + if (!empty($sEvents)) + { + $sMethods .= <<GetOptionalElement('archive')) { $bEnabled = $this->GetPropBoolean($oArchive, 'enabled', false); @@ -1862,7 +1924,6 @@ EOF } // Methods - $sMethods = ""; $oMethods = $oClass->GetUniqueElement('methods'); foreach($oMethods->getElementsByTagName('method') as $oMethod) { @@ -1993,6 +2054,7 @@ $sLifecycle $sHighlightScale $sZlists; EOF; + // some other stuff (magical attributes like friendlyName) are done in MetaModel::InitClasses and though not present in the // generated PHP $aPHP[$sClassName] = $this->GeneratePhpCodeForClass($sClassName, $sParentClass, $sClassParams, $sInitMethodCalls, @@ -2930,6 +2992,130 @@ EOF; foreach($this->aSnippets as $sModuleId => $void) { uasort($this->aSnippets[$sModuleId]['before'], array(get_class($this), 'SortOnRank')); + uasort($this->aSnippets[$sModuleId]['after'], array(get_class($this), 'SortOnRank')); + } + } + + /** + * @throws \DOMFormatException + */ + protected function LoadHooks() + { + $sClassName = 'GlobalEventHooks'; + $sModuleId = '_core_'; + if (!array_key_exists($sModuleId, $this->aSnippets)) + { + $this->aSnippets[$sModuleId] = array('before' => array(), 'after' => array()); + } + $oHooks = $this->oFactory->GetNodes('/itop_design/hooks/hook'); + $aHooks = array(); + foreach($oHooks as $oHook) + { + /** @var \DOMElement $oHook */ + $sEventName = $oHook->getAttribute('id'); + $oListeners = $oHook->GetOptionalElement('listeners'); + if ($oListeners) + { + foreach ($oListeners->getElementsByTagName('listener') as $oListener) + { + /** @var \DOMElement $oListener */ + $sListenerId = $oListener->getAttribute('id'); + $oExecute = $oListener->GetUniqueElement('execute', true); + $sExecute = trim($oExecute->GetText()); + $sCallback = "{$sEventName}_{$sListenerId}"; + $sCallbackFct = preg_replace('@^function\s*\(@', "public static function {$sCallback}(", $sExecute); + if ($sExecute == $sCallbackFct) + { + throw new DOMFormatException("Malformed tag in hook: {$sEventName} listener: {$sListenerId}"); + } + $fPriority = (float)($oListener->GetChildText('priority', '0')); + + $aFilters = array(); + $oFilters = $oListener->GetOptionalElement('filters'); + if ($oFilters) + { + foreach ($oFilters->getElementsByTagName('filter') as $oFilter) + { + $aFilters[] = $oFilter->GetText(); + } + } + if (empty($aFilters)) + { + $sEventSource = 'null'; + } + else + { + $sEventSource = 'array("'.implode('", "', $aFilters).'")'; + } + + $aContexts = array(); + $oContexts = $oListener->GetOptionalElement('Contexts'); + if ($oContexts) + { + foreach ($oContexts->getElementsByTagName('Context') as $oContext) + { + $aContexts[] = $oContext->GetText(); + } + } + if (empty($aContexts)) + { + $sContext = 'null'; + } + else + { + $sContext = 'array("'.implode('", "', $aContexts).'")'; + } + + $aHooks[] = array( + 'event_name' => $sEventName, + 'callback' => $sCallback, + 'content' => $sCallbackFct, + 'priority' => $fPriority, + 'source' => $sEventSource, + 'context' => $sContext, + ); + } + } + } + + if (empty($aHooks)) + { + return; + } + + $sRegister = ''; + $sMethods = ''; + foreach ($aHooks as $aHook) + { + $sCallback = $aHook['callback']; + $sEventName = $aHook['event_name']; + $sEventSource = $aHook['source']; + $sContext = $aHook['context']; + $sPriority = $aHook['priority']; + $sRegister .= "\nCombodo\iTop\Service\Event::Register(\"$sEventName\", '$sClassName::$sCallback', $sEventSource, null, $sContext, $sPriority);"; + $sCallbackFct = $aHook['content']; + $sMethods .= "\n {$sCallbackFct}\n\n"; + } + + $sContent = <<aSnippets[$sModuleId]['after'][] = array( + 'rank' => $fOrder, + 'content' => $sContent, + 'snippet_id' => $sClassName, + ); + foreach($this->aSnippets as $sModuleId => $void) + { + uasort($this->aSnippets[$sModuleId]['after'], array(get_class($this), 'SortOnRank')); } } diff --git a/sources/application/Service/Event.php b/sources/application/Service/Event.php new file mode 100644 index 000000000..edb0f2924 --- /dev/null +++ b/sources/application/Service/Event.php @@ -0,0 +1,298 @@ + $sId, + 'callback' => $callback, + 'source' => $sEventSource, + 'name' => $sName, + 'data' => $aCallbackData, + 'context' => $context, + 'priority' => $fPriority, + ); + usort($aEventCallbacks, function ($a, $b) { + $fPriorityA = $a['priority']; + $fPriorityB = $b['priority']; + if ($fPriorityA == $fPriorityB) { + return 0; + } + return ($fPriorityA < $fPriorityB) ? -1 : 1; + }); + self::$aEvents[$sEvent] = $aEventCallbacks; + + if (IssueLog::CanLog(IssueLog::LEVEL_DEBUG, LOG_EVENT_SERVICE_CHANNEL)) + { + $iTotalRegistrations = 0; + foreach (self::$aEvents as $aEvent) + { + $iTotalRegistrations += count($aEvent); + } + $sEventName = "$sEvent:".self::GetSourcesAsString($sEventSource); + IssueLog::Trace("Registering event '$sEventName' for '$sName' with id '$sId' (total $iTotalRegistrations)", LOG_EVENT_SERVICE_CHANNEL); + } + return $sId; + } + + /** + * Fire an event. Call all the callbacks registered for this event. + * + * @param string $sEvent event to trigger + * @param string|array $eventSource source of the event + * @param array $aEventData event related data + * + * @throws \Exception from the callback + */ + public static function FireEvent($sEvent, $eventSource = null, $aEventData = array()) + { + $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); + if (!isset(self::$aEvents[$sEvent])) + { + IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + $oKPI->ComputeStats('FireEvent', $sEvent); + return; + } + + foreach (self::$aEvents[$sEvent] as $aEventCallback) + { + if (!self::MatchEventSource($aEventCallback['source'], $eventSource)) + { + continue; + } + if (!self::MatchContext($aEventCallback['context'])) + { + continue; + } + $sName = $aEventCallback['name']; + IssueLog::Debug("Fire event '$sEventName'$sSource calling '{$sName}'", LOG_EVENT_SERVICE_CHANNEL); + try + { + if (is_callable($aEventCallback['callback'])) + { + call_user_func($aEventCallback['callback'], new EventData($sEvent, $eventSource, $aEventData, $aEventCallback['data'])); + } + else + { + IssueLog::Debug("Callback '{$sName}' not a callable anymore, unregister", LOG_EVENT_SERVICE_CHANNEL); + self::UnRegisterCallback($aEventCallback['id']); + } + } + catch (Exception $e) + { + IssueLog::Error("Event '$sEventName' for '{$sName}' id {$aEventCallback['id']} failed with error: ".$e->getMessage()); + throw $e; + } + } + $oKPI->ComputeStats('FireEvent', $sEvent); + } + + private static function MatchEventSource($srcRegistered, $srcEvent) + { + if (empty($srcRegistered)) + { + // no filtering + return true; + } + if (empty($srcEvent)) + { + // no match (the registered source is not empty) + return false; + } + if (is_string($srcRegistered)) + { + $aSrcRegistered = array($srcRegistered); + } + elseif (is_array($srcRegistered)) + { + $aSrcRegistered = $srcRegistered; + } + else + { + $aSrcRegistered = array(); + } + + if (is_string($srcEvent)) + { + $aSrcEvent = array($srcEvent); + } + elseif (is_array($srcEvent)) + { + $aSrcEvent = $srcEvent; + } + else + { + $aSrcEvent = array(); + } + + foreach ($aSrcRegistered as $sSrcRegistered) + { + foreach ($aSrcEvent as $sSrcEvent) + { + if ($sSrcRegistered == $sSrcEvent) + { + // sources matches + return true; + } + } + } + // no match + return false; + } + + private static function MatchContext($registeredContext) + { + if (empty($registeredContext)) + { + return true; + } + if (is_string($registeredContext)) + { + $aContexts = array($registeredContext); + } + elseif (is_array($registeredContext)) + { + $aContexts = $registeredContext; + } + else + { + return false; + } + foreach ($aContexts as $sContext) + { + if (ContextTag::Check($sContext)) + { + return true; + } + } + return false; + } + + private static function GetSourcesAsString($srcRegistered) + { + if (empty($srcRegistered)) + { + return ''; + } + if (is_string($srcRegistered)) + { + return $srcRegistered; + } + if (is_array($srcRegistered)) + { + $sStr = implode(',', $srcRegistered); + } + return ''; + } + + /** + * Unregister a previously registered callback + * + * @param string $sId the callback registration id + */ + public static function UnRegisterCallback($sId) + { + $bRemoved = self::Browse(function ($sEvent, $idx, $aEventCallback) use ($sId) { + if ($aEventCallback['id'] == $sId) + { + $sName = self::$aEvents[$sEvent][$idx]['name']; + unset (self::$aEvents[$sEvent][$idx]); + IssueLog::Trace("Unregistered callback '{$sName}' id {$sId}' on event '{$sEvent}'", LOG_EVENT_SERVICE_CHANNEL); + return false; + } + return true; + }); + + if (!$bRemoved) + { + IssueLog::Trace("No registration found for callback '{$sId}'", LOG_EVENT_SERVICE_CHANNEL); + } + } + + /** + * Unregister an event + * + * @param string $sEvent event to unregister + */ + public static function UnRegisterEvent($sEvent) + { + if (!isset(self::$aEvents[$sEvent])) + { + IssueLog::Trace("No registration found for event '$sEvent'", LOG_EVENT_SERVICE_CHANNEL); + return; + } + + unset(self::$aEvents[$sEvent]); + IssueLog::Trace("Unregistered all the callbacks on event '{$sEvent}'", LOG_EVENT_SERVICE_CHANNEL); + } + + /** + * Unregister all the events + */ + public static function UnRegisterAll() + { + self::$aEvents = array(); + IssueLog::Trace("Unregistered all events", LOG_EVENT_SERVICE_CHANNEL); + } + + /** + * Browse all the registrations + * + * @param \Closure $callback function($sEvent, $idx, $aEventCallback) to call (return false to interrupt the browsing) + * + * @return bool true if interrupted else false + */ + private static function Browse(closure $callback) + { + foreach (self::$aEvents as $sEvent => $aCallbackList) + { + foreach ($aCallbackList as $idx => $aEventCallback) + { + if (call_user_func($callback, $sEvent, $idx, $aEventCallback) === false) + { + return true; + } + } + } + return false; + } +} diff --git a/sources/application/Service/EventData.php b/sources/application/Service/EventData.php new file mode 100644 index 000000000..b3ee713d6 --- /dev/null +++ b/sources/application/Service/EventData.php @@ -0,0 +1,84 @@ +sEvent = $sEvent; + $this->mEventData = $aEventData; + $this->sEventSource = $sEventSource; + $this->mCallbackData = $aCallbackData; + } + + /** + * @return string + */ + public function GetEvent() + { + return $this->sEvent; + } + + public function Get($sParam) + { + if (is_array($this->mEventData) && isset($this->mEventData[$sParam])) + { + return $this->mEventData[$sParam]; + } + + if (is_array($this->mCallbackData) && isset($this->mCallbackData[$sParam])) + { + return $this->mCallbackData[$sParam]; + } + return null; + } + + /** + * @return string + */ + public function GetEventSource() + { + return $this->sEventSource; + } + + /** + * @return mixed + */ + public function GetEventData() + { + return $this->mEventData; + } + + /** + * @return mixed + */ + public function GetCallbackData() + { + return $this->mCallbackData; + } +} diff --git a/sources/application/Service/EventName.php b/sources/application/Service/EventName.php new file mode 100644 index 000000000..9f3836835 --- /dev/null +++ b/sources/application/Service/EventName.php @@ -0,0 +1,38 @@ + status + + service + coreExtensions diff --git a/test/service/EventTest.php b/test/service/EventTest.php new file mode 100644 index 000000000..db0a491d2 --- /dev/null +++ b/test/service/EventTest.php @@ -0,0 +1,406 @@ +expectException(TypeError::class); + Event::Register('event', $callback); + } + + public function BadCallbackProvider() + { + return array( + array('toto'), + array('EventTest::toto'), + array(array('EventTest', 'toto')), + array(array($this, 'toto')), + ); + } + + public function testNoParameterCallbackFunction() + { + $sId = Event::Register('event', function () { $this->debug("Closure: event received !!!"); self::IncrementCallCount(); }); + $this->debug("Registered $sId"); + Event::FireEvent('event'); + $this->assertEquals(1, self::$iEventCalls); + } + + /** + * @dataProvider GoodCallbackProvider + * + * @param $callback + * + * @throws \Exception + */ + public function testMethodCallbackFunction(callable $callback) + { + $sId = Event::Register('event', $callback); + $this->debug("Registered $sId"); + Event::FireEvent('event'); + $this->assertEquals(1, self::$iEventCalls); + Event::FireEvent('event'); + $this->assertEquals(2, self::$iEventCalls); + } + + public function GoodCallbackProvider() + { + $oReceiver = new TestEventReceiver(); + return array( + 'method' => array(array($oReceiver, 'OnEvent1')), + 'static' => array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1'), + 'static2' => array(array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver', 'OnStaticEvent1')), + ); + } + + public function testBrokenCallback() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event_a', array($oReceiver, 'BrokenCallback')); + + $this->expectException(TypeError::class); + Event::FireEvent('event_a'); + } + + public function testRemovedCallback() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event_a', array($oReceiver, 'OnEvent1')); + + $oReceiver = null; + gc_collect_cycles(); + + Event::FireEvent('event_a'); + $this->assertEquals(1, self::$iEventCalls); + } + + public function testMultiEvent() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event_a', array($oReceiver, 'OnEvent1')); + Event::Register('event_a', array($oReceiver, 'OnEvent2')); + Event::Register('event_a', array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver', 'OnStaticEvent1')); + Event::Register('event_a', 'Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent2'); + + Event::Register('event_b', array($oReceiver, 'OnEvent1')); + Event::Register('event_b', array($oReceiver, 'OnEvent2')); + Event::Register('event_b', array('Combodo\iTop\Test\UnitTest\Service\TestEventReceiver', 'OnStaticEvent1')); + Event::Register('event_b', 'Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent2'); + + Event::FireEvent('event_a'); + $this->assertEquals(4, self::$iEventCalls); + Event::FireEvent('event_b'); + $this->assertEquals(8, self::$iEventCalls); + } + + public function testMultiSameEvent() + { + $oReceiver = new TestEventReceiver(); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + + Event::FireEvent('event1'); + $this->assertEquals(4, self::$iEventCalls); + } + + public function testData() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event1', array($oReceiver, 'OnEventWithData'), ''); + Event::Register('event1', array($oReceiver, 'OnEventWithData'), ''); + Event::FireEvent('event1', '', array('text' => 'Event Data 1')); + $this->assertEquals(2, self::$iEventCalls); + } + + public function testPriority() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event1', array($oReceiver, 'OnEvent1'), '', null, null, 0); + Event::Register('event1', array($oReceiver, 'OnEvent2'), '', null, null, 1); + + Event::Register('event2', array($oReceiver, 'OnEvent1'), '', null, null, 1); + Event::Register('event2', array($oReceiver, 'OnEvent2'), '', null, null, 0); + + Event::FireEvent('event1'); + $this->assertEquals(2, self::$iEventCalls); + Event::FireEvent('event2'); + $this->assertEquals(4, self::$iEventCalls); + } + + public function testContext() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event1', array($oReceiver, 'OnEvent1'), '', null, null, 0); + Event::Register('event1', array($oReceiver, 'OnEvent2'), '', null, 'test_context', 1); + Event::FireEvent('event1'); + $this->assertEquals(1, self::$iEventCalls); + ContextTag::AddContext('test_context'); + Event::FireEvent('event1'); + $this->assertEquals(3, self::$iEventCalls); + } + + public function testEventSource() + { + $oReceiver = new TestEventReceiver(); + Event::Register('event1', array($oReceiver, 'OnEvent1'), 'A', null, null, 0); + Event::Register('event1', array($oReceiver, 'OnEvent2'), 'A', null, null, 1); + Event::Register('event1', 'Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1', null, null, null, 2); + + Event::Register('event2', array($oReceiver, 'OnEvent1'), 'A', null, null, 1); + Event::Register('event2', 'Combodo\iTop\Test\UnitTest\Service\TestEventReceiver::OnStaticEvent1', null, null, null, 2); + Event::Register('event2', array($oReceiver, 'OnEvent2'), 'B', null, null, 0); + + Event::FireEvent('event1', 'A'); + $this->assertEquals(3, self::$iEventCalls); + Event::FireEvent('event2', 'A'); + $this->assertEquals(5, self::$iEventCalls); + Event::FireEvent('event1'); + $this->assertEquals(6, self::$iEventCalls); + Event::FireEvent('event2'); + $this->assertEquals(7, self::$iEventCalls); + Event::FireEvent('event2', array('A', 'B')); + $this->assertEquals(10, self::$iEventCalls); + + } + + + public function testUnRegisterEvent() + { + $oReceiver = new TestEventReceiver(); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event2', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + + Event::FireEvent('event1'); + $this->assertEquals(3, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(4, self::$iEventCalls); + + Event::UnRegisterEvent('event1'); + + Event::FireEvent('event1'); + $this->assertEquals(4, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(5, self::$iEventCalls); + } + + public function testUnRegisterAll() + { + $oReceiver = new TestEventReceiver(); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event2', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + + Event::FireEvent('event1'); + $this->assertEquals(3, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(4, self::$iEventCalls); + + Event::UnRegisterAll(); + + Event::FireEvent('event1'); + $this->assertEquals(4, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(4, self::$iEventCalls); + } + + public function testUnRegisterCallback() + { + $oReceiver = new TestEventReceiver(); + $sIdToRemove = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sIdToRemove"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event1', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + $sId = Event::Register('event2', array($oReceiver, 'OnEvent1')); + $this->debug("Registered $sId"); + + Event::FireEvent('event1'); + $this->assertEquals(3, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(4, self::$iEventCalls); + + Event::UnRegisterCallback($sIdToRemove); + + Event::FireEvent('event1'); + $this->assertEquals(6, self::$iEventCalls); + + Event::FireEvent('event2'); + $this->assertEquals(7, self::$iEventCalls); + } + + public static function IncrementCallCount() + { + self::$iEventCalls++; + } + + /** + * 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); + } + } + } +} + +class TestEventReceiver +{ + + // Event callbacks + public function OnEvent1(EventData $oData) + { + $sEvent = $oData->GetEvent(); + $this->Debug(__METHOD__.": received event '{$sEvent}'"); + EventTest::IncrementCallCount(); + } + + // Event callbacks + public function BrokenCallback(array $aData) + { + $sEvent = $aData['event']; + $this->Debug(__METHOD__.": received event '{$sEvent}'"); + EventTest::IncrementCallCount(); + } + + // Event callbacks + public function OnEvent2(EventData $oData) + { + $sEvent = $oData->GetEvent(); + $this->Debug(__METHOD__.": received event '{$sEvent}'"); + EventTest::IncrementCallCount(); + } + + public function OnEventWithData(EventData $oData) + { + $sEvent = $oData->GetEvent(); + $mEventData = $oData->GetEventData(); + $this->Debug(__METHOD__.": received event '{$sEvent}'"); + EventTest::IncrementCallCount(); + $this->Debug($mEventData); + } + + // Event callbacks + public static function OnStaticEvent1(EventData $oData) + { + $sEvent = $oData->GetEvent(); + self::DebugStatic(__METHOD__.": received event '{$sEvent}'"); + EventTest::IncrementCallCount(); + } + + // Event callbacks + public static function OnStaticEvent2(EventData $oData) + { + $sEvent = $oData->GetEvent(); + 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); + } + } + } + +}