diff --git a/application/query.class.inc.php b/application/query.class.inc.php index 074b24e00..2430ebb94 100644 --- a/application/query.class.inc.php +++ b/application/query.class.inc.php @@ -296,7 +296,7 @@ class QueryOQL extends Query } catch (OQLException $e) { - $oAlert = AlertUIBlockFactory::MakeForFailure(Dict::Format('UI:RunQuery:Error'), $e->getHtmlDesc()) + $oAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:RunQuery:Error'), $e->getHtmlDesc()) ->SetIsClosable(false) ->SetIsCollapsible(false); $oAlert->AddCSSClass('mb-5'); diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 9673f79d1..8d7da1a58 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -6,7 +6,9 @@ use Combodo\iTop\Core\MetaModel\FriendlyNameType; use Combodo\iTop\Service\Events\EventData; +use Combodo\iTop\Service\Events\EventException; use Combodo\iTop\Service\Events\EventService; +use Combodo\iTop\Service\Events\EventServiceLog; use Combodo\iTop\Service\TemporaryObjects\TemporaryObjectManager; /** @@ -203,6 +205,8 @@ abstract class DBObject implements iDisplay const MAX_UPDATE_LOOP_COUNT = 10; + private $aEventListeners = []; + /** * DBObject constructor. * @@ -255,6 +259,10 @@ abstract class DBObject implements iDisplay $this->RegisterEventListeners(); } + /** + * @see RegisterCRUDListener + * @see EventService::RegisterListener() + */ protected function RegisterEventListeners() { } @@ -6181,6 +6189,51 @@ abstract class DBObject implements iDisplay return OPT_ATT_NORMAL; } + public final function GetListeners(): array + { + $aListeners = []; + foreach ($this->aEventListeners as $aEventListener) { + $aListeners = array_merge($aListeners, $aEventListener); + } + return $aListeners; + } + + /** + * Register a callback for a specific event. The method to call will be saved in the object instance itself whereas calling {@see EventService::RegisterListener()} would + * save a callable (thus the method name AND the whole DBObject instance) + * + * @param string $sEvent corresponding event + * @param string $callback The callback method to call + * @param float $fPriority optional priority for callback order + * @param string $sModuleId + * + * @see EventService::RegisterListener() + * + * @since 3.1.0-3 3.1.1 3.2.0 N°6716 + */ + final protected function RegisterCRUDListener(string $sEvent, string $callback, float $fPriority = 0.0, string $sModuleId = '') + { + $aEventCallbacks = $this->aEventListeners[$sEvent] ?? []; + + $aEventCallbacks[] = array( + 'event' => $sEvent, + 'callback' => $callback, + 'priority' => $fPriority, + 'module' => $sModuleId, + ); + usort($aEventCallbacks, function ($a, $b) { + $fPriorityA = $a['priority']; + $fPriorityB = $b['priority']; + if ($fPriorityA == $fPriorityB) { + return 0; + } + + return ($fPriorityA < $fPriorityB) ? -1 : 1; + }); + + $this->aEventListeners[$sEvent] = $aEventCallbacks; + } + /** * @param string $sEvent * @param array $aEventData @@ -6192,15 +6245,53 @@ abstract class DBObject implements iDisplay */ public function FireEvent(string $sEvent, array $aEventData = array()): void { - if (EventService::IsEventRegistered($sEvent)) { - $aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey(); - $aEventData['object'] = $this; - $aEventSources = [$this->m_sObjectUniqId]; - foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) { - $aEventSources[] = $sClass; + $aEventData['debug_info'] = 'from: '.get_class($this).':'.$this->GetKey(); + $aEventData['object'] = $this; + + // Call local listeners first + $aEventCallbacks = $this->aEventListeners[$sEvent] ?? []; + $oFirstException = null; + $sFirstExceptionMessage = ''; + foreach ($aEventCallbacks as $aEventCallback) { + $oKPI = new ExecutionKPI(); + $sCallback = $aEventCallback['callback']; + if (!method_exists($this, $sCallback)) { + EventServiceLog::Error("Callback '".get_class($this).":$sCallback' does not exist"); + continue; + } + EventServiceLog::Debug("Fire event '$sEvent' calling '".get_class($this).":$sCallback'"); + try { + call_user_func([$this, $sCallback], new EventData($sEvent, null, $aEventData)); + } + catch (EventException $e) { + EventServiceLog::Error("Event '$sEvent' for '$sCallback'} failed with blocking error: ".$e->getMessage()); + throw $e; + } + catch (Exception $e) { + $sMessage = "Event '$sEvent' for '$sCallback'} failed with non-blocking error: ".$e->getMessage(); + EventServiceLog::Error($sMessage); + if (is_null($oFirstException)) { + $sFirstExceptionMessage = $sMessage; + $oFirstException = $e; + } + } + finally { + $oKPI->ComputeStats('FireEvent', $sEvent); } - EventService::FireEvent(new EventData($sEvent, $aEventSources, $aEventData)); } + if (!is_null($oFirstException)) { + throw new Exception($sFirstExceptionMessage, $oFirstException->getCode(), $oFirstException); + } + + // Call global event listeners + if (!EventService::IsEventRegistered($sEvent)) { + return; + } + $aEventSources = []; + foreach (MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL, false) as $sClass) { + $aEventSources[] = $sClass; + } + EventService::FireEvent(new EventData($sEvent, $aEventSources, $aEventData)); } ////////////////// diff --git a/core/dict.class.inc.php b/core/dict.class.inc.php index ff9ef890a..d3e66e8fc 100644 --- a/core/dict.class.inc.php +++ b/core/dict.class.inc.php @@ -3,7 +3,7 @@ // // This file is part of iTop. // -// iTop is free software; you can redistribute it and/or modify +// iTop is free software; you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. @@ -116,33 +116,50 @@ class Dict * @return string */ public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false) + { + $aInfo = self::GetLabelAndLangCode($sStringCode, $sDefault, $bUserLanguageOnly); + return $aInfo['label']; + } + + /** + * Returns a localised string from the dictonary with its associated lang code + * + * @param string $sStringCode The code identifying the dictionary entry + * @param string $sDefault Default value if there is no match in the dictionary + * @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise + * + * @return array{ + * lang: string, label: string + * } with localized label string and used lang code + */ + private static function GetLabelAndLangCode($sStringCode, $sDefault = null, $bUserLanguageOnly = false) { // Attempt to find the string in the user language // $sLangCode = self::GetUserLanguage(); self::InitLangIfNeeded($sLangCode); - if (!array_key_exists($sLangCode, self::$m_aData)) + if (! array_key_exists($sLangCode, self::$m_aData)) { - IssueLog::Warning("Cannot find $sLangCode in dictionnaries. default labels displayed"); + IssueLog::Warning("Cannot find $sLangCode in all registered dictionaries."); // It may happen, when something happens before the dictionaries get loaded - return $sStringCode; + return [ 'label' => $sStringCode, 'lang' => $sLangCode ]; } $aCurrentDictionary = self::$m_aData[$sLangCode]; if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary)) { - return $aCurrentDictionary[$sStringCode]; + return [ 'label' => $aCurrentDictionary[$sStringCode], 'lang' => $sLangCode ]; } if (!$bUserLanguageOnly) { // Attempt to find the string in the default language // self::InitLangIfNeeded(self::$m_sDefaultLanguage); - + $aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage]; if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary)) { - return $aDefaultDictionary[$sStringCode]; + return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => self::$m_sDefaultLanguage ]; } // Attempt to find the string in english // @@ -151,17 +168,17 @@ class Dict $aDefaultDictionary = self::$m_aData['EN US']; if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary)) { - return $aDefaultDictionary[$sStringCode]; + return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => 'EN US' ]; } } // Could not find the string... // if (is_null($sDefault)) { - return $sStringCode; + return [ 'label' => $sStringCode, 'lang' => null ]; } - return $sDefault; + return [ 'label' => $sDefault, 'lang' => null ]; } @@ -177,19 +194,25 @@ class Dict */ public static function Format($sFormatCode /*, ... arguments ... */) { - $sLocalizedFormat = self::S($sFormatCode); + ['label' => $sLocalizedFormat, 'lang' => $sLangCode] = self::GetLabelAndLangCode($sFormatCode); + $aArguments = func_get_args(); array_shift($aArguments); - + if ($sLocalizedFormat == $sFormatCode) { // Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded) return $sFormatCode.' - '.implode(', ', $aArguments); } - return vsprintf($sLocalizedFormat, $aArguments); + try{ + return vsprintf($sLocalizedFormat, $aArguments); + } catch(\Throwable $e){ + \IssueLog::Error("Cannot format dict key", null, ["sFormatCode" => $sFormatCode, "sLangCode" => $sLangCode, 'exception_msg' => $e->getMessage() ]); + return $sFormatCode.' - '.implode(', ', $aArguments); + } } - + /** * Initialize a the entries for a given language (replaces the former Add() method) * @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US' @@ -199,7 +222,7 @@ class Dict { self::$m_aData[$sLanguageCode] = $aEntries; } - + /** * Set the list of available languages * @param hash $aLanguagesList @@ -260,7 +283,7 @@ class Dict { $sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php'; require_once($sDictFile); - + if (self::GetApcService()->function_exists('apc_store') && (self::$m_sApplicationPrefix !== null)) { @@ -270,7 +293,7 @@ class Dict } return $bResult; } - + /** * Enable caching (cached using APC) * @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance @@ -313,14 +336,14 @@ class Dict } } } - + public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US') { $aMissing = array(); // Strings missing for the target language $aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary $aNotTranslated = array(); // Strings having the same value in both dictionaries $aOK = array(); // Strings having different values in both dictionaries - + foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue) { if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode])) @@ -328,7 +351,7 @@ class Dict $aMissing[$sStringCode] = $sValue; } } - + foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue) { if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef])) @@ -351,7 +374,7 @@ class Dict } return array($aMissing, $aUnexpected, $aNotTranslated, $aOK); } - + public static function Dump() { MyHelpers::var_dump_html(self::$m_aData); @@ -374,7 +397,7 @@ class Dict // No need to actually load the strings since it's only used to know the list of languages // at setup time !! } - + /** * Export all the dictionary entries - of the given language - whose code matches the given prefix * missing entries in the current language will be replaced by entries in the default language @@ -387,7 +410,7 @@ class Dict self::InitLangIfNeeded(self::$m_sDefaultLanguage); $aEntries = array(); $iLength = strlen($sStartingWith); - + // First prefill the array with entries from the default language foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry) { @@ -396,7 +419,7 @@ class Dict $aEntries[$sCode] = $sEntry; } } - + // Now put (overwrite) the entries for the user language foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry) { diff --git a/datamodels/2.x/itop-service-mgmt-provider/dictionaries/fr.dict.itop-service-mgmt-provider.php b/datamodels/2.x/itop-service-mgmt-provider/dictionaries/fr.dict.itop-service-mgmt-provider.php index b1247c6db..7511f54cb 100644 --- a/datamodels/2.x/itop-service-mgmt-provider/dictionaries/fr.dict.itop-service-mgmt-provider.php +++ b/datamodels/2.x/itop-service-mgmt-provider/dictionaries/fr.dict.itop-service-mgmt-provider.php @@ -99,9 +99,9 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Class:Contract/Attribute:organization_name' => 'Nom client', 'Class:Contract/Attribute:organization_name+' => 'Nom commun', 'Class:Contract/Attribute:contacts_list' => 'Contacts', - 'Class:Contract/Attribute:contacts_list+' => 'Tous les contacts for ce contrat client', + 'Class:Contract/Attribute:contacts_list+' => 'Tous les contacts pour ce contrat client', 'Class:Contract/Attribute:documents_list' => 'Documents', - 'Class:Contract/Attribute:documents_list+' => 'Tous les documents for ce contrat client', + 'Class:Contract/Attribute:documents_list+' => 'Tous les documents pour ce contrat client', 'Class:Contract/Attribute:description' => 'Description', 'Class:Contract/Attribute:description+' => '', 'Class:Contract/Attribute:start_date' => 'Date de début', diff --git a/datamodels/2.x/itop-service-mgmt/dictionaries/de.dict.itop-service-mgmt.php b/datamodels/2.x/itop-service-mgmt/dictionaries/de.dict.itop-service-mgmt.php index a2039d864..69a6cd3ff 100644 --- a/datamodels/2.x/itop-service-mgmt/dictionaries/de.dict.itop-service-mgmt.php +++ b/datamodels/2.x/itop-service-mgmt/dictionaries/de.dict.itop-service-mgmt.php @@ -157,6 +157,8 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Class:ProviderContract/Attribute:contracttype_id+' => '', 'Class:ProviderContract/Attribute:contracttype_name' => 'Vertragstyp-Name', 'Class:ProviderContract/Attribute:contracttype_name+' => '', + 'Class:ProviderContract/Attribute:services_list' => 'Services', + 'Class:ProviderContract/Attribute:services_list+' => 'Alle für diesen Vertrag erworbenen Services', )); // diff --git a/datamodels/2.x/itop-service-mgmt/dictionaries/en.dict.itop-service-mgmt.php b/datamodels/2.x/itop-service-mgmt/dictionaries/en.dict.itop-service-mgmt.php index df3df9095..baddc80c5 100644 --- a/datamodels/2.x/itop-service-mgmt/dictionaries/en.dict.itop-service-mgmt.php +++ b/datamodels/2.x/itop-service-mgmt/dictionaries/en.dict.itop-service-mgmt.php @@ -169,6 +169,8 @@ Dict::Add('EN US', 'English', 'English', array( 'Class:ProviderContract/Attribute:contracttype_id+' => '', 'Class:ProviderContract/Attribute:contracttype_name' => 'Contract type name', 'Class:ProviderContract/Attribute:contracttype_name+' => '', + 'Class:ProviderContract/Attribute:services_list' => 'Services', + 'Class:ProviderContract/Attribute:services_list+' => 'All the services purchased with this contract', )); // diff --git a/datamodels/2.x/itop-service-mgmt/dictionaries/fr.dict.itop-service-mgmt.php b/datamodels/2.x/itop-service-mgmt/dictionaries/fr.dict.itop-service-mgmt.php index 37ad423d6..60aab6793 100644 --- a/datamodels/2.x/itop-service-mgmt/dictionaries/fr.dict.itop-service-mgmt.php +++ b/datamodels/2.x/itop-service-mgmt/dictionaries/fr.dict.itop-service-mgmt.php @@ -157,6 +157,8 @@ Dict::Add('FR FR', 'French', 'Français', array( 'Class:ProviderContract/Attribute:contracttype_id+' => '', 'Class:ProviderContract/Attribute:contracttype_name' => 'Nom Type de contrat', 'Class:ProviderContract/Attribute:contracttype_name+' => '', + 'Class:ProviderContract/Attribute:services_list' => 'Services', + 'Class:ProviderContract/Attribute:services_list+' => 'Tous les services achetés par ce contrat', )); // diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 8e05bd527..23867e105 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -837,7 +837,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:RunQuery:DevelopedOQLCount' => 'Developed OQL for count', 'UI:RunQuery:ResultSQLCount' => 'Resulting SQL for count', 'UI:RunQuery:ResultSQL' => 'Resulting SQL', - 'UI:RunQuery:Error' => 'An error occured while running the query', + 'UI:RunQuery:Error' => 'An error occured while running the query: %1$s', 'UI:Query:UrlForExcel' => 'URL to use for MS-Excel web queries', 'UI:Query:UrlV1' => 'The list of fields has been left unspecified. The page export-V2.php cannot be invoked without this information. Therefore, the URL suggested here below points to the legacy page: export.php. This legacy version of the export has the following limitation: the list of exported fields may vary depending on the output format and the data model of '.ITOP_APPLICATION_SHORT.'.
Should you want to guarantee that the list of exported columns will remain stable on the long run, then you must specify a value for the attribute "Fields" and use the page export-V2.php.', 'UI:Schema:Title' => ITOP_APPLICATION_SHORT.' objects schema', diff --git a/dictionaries/hu.dictionary.itop.core.php b/dictionaries/hu.dictionary.itop.core.php index 53bddc6f5..b21c58135 100755 --- a/dictionaries/hu.dictionary.itop.core.php +++ b/dictionaries/hu.dictionary.itop.core.php @@ -21,7 +21,7 @@ * along with iTop. If not, see */ Dict::Add('HU HU', 'Hungarian', 'Magyar', array( - 'Core:DeletedObjectLabel' => '%1s (törölve)', + 'Core:DeletedObjectLabel' => '%1$s (törölve)', 'Core:DeletedObjectTip' => 'A %1$s objektum törölve (%2$s)', 'Core:UnknownObjectLabel' => 'Objektum nem található (osztály: %1$s, id: %2$d)', 'Core:UnknownObjectTip' => 'Az objektumot nem sikerült megtalálni. Lehet, hogy már törölték egy ideje, és a naplót azóta törölték.', diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php index 807e6c997..0d491bb8a 100755 --- a/dictionaries/hu.dictionary.itop.ui.php +++ b/dictionaries/hu.dictionary.itop.ui.php @@ -715,7 +715,7 @@ Reméljük, hogy ezt a verziót ugyanúgy kedvelni fogja, mint ahogy mi élvezt 'UI:CSVReport-Value-Issue-Null' => 'A nulla nem engedélyezett', 'UI:CSVReport-Value-Issue-NotFound' => 'Az objektum nincs meg', 'UI:CSVReport-Value-Issue-FoundMany' => '%1$d egyezés található', - 'UI:CSVReport-Value-Issue-Readonly' => 'A \'%1$\'s attribútum csak olvasható (jelenlegi érték: %2$s, várható érték: %3$s)', + 'UI:CSVReport-Value-Issue-Readonly' => 'A \'%1$s attribútum csak olvasható (jelenlegi érték: %2$s, várható érték: %3$s)', 'UI:CSVReport-Value-Issue-Format' => 'A bevitel feldolgozása sikertelen: %1$s', 'UI:CSVReport-Value-Issue-NoMatch' => 'A \'%1$s\' attribútum nem várt értéket kapott: nincs egyezés, ellenőrizze a beírást', 'UI:CSVReport-Value-Issue-AllowedValues' => 'Allowed \'%1$s\' value(s): %2$s~~', diff --git a/dictionaries/ja.dictionary.itop.core.php b/dictionaries/ja.dictionary.itop.core.php index 1aa08cd38..e19e757e5 100644 --- a/dictionaries/ja.dictionary.itop.core.php +++ b/dictionaries/ja.dictionary.itop.core.php @@ -20,7 +20,7 @@ * @licence http://opensource.org/licenses/AGPL-3.0 */ Dict::Add('JA JP', 'Japanese', '日本語', array( - 'Core:DeletedObjectLabel' => '%1s (削除されました)', + 'Core:DeletedObjectLabel' => '%1$s (削除されました)', 'Core:DeletedObjectTip' => 'オブジェクトは削除されました %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'オブジェクトは見つかりません (クラス: %1$s, id: %2$d)', 'Core:UnknownObjectTip' => 'オブジェクトは見つかりません。しばらく前に削除され、その後ログが削除されたかもしれません。', diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php index 6f6caf18a..20be119c0 100644 --- a/dictionaries/ja.dictionary.itop.ui.php +++ b/dictionaries/ja.dictionary.itop.ui.php @@ -1159,8 +1159,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'Enum:Undefined' => '未定義', 'UI:DurationForm_Days_Hours_Minutes_Seconds' => '%1$s 日 %2$s 時 %3$s 分 %4$s 秒', 'UI:ModifyAllPageTitle' => '全てを修正', - 'UI:Modify_ObjectsOf_Class' => 'Modifying objects of class %1$s~~', - 'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$Sの%1$dオブジェクトを修正', + 'UI:Modify_N_ObjectsOf_Class' => 'クラス%2$sの%1$dオブジェクトを修正', 'UI:Modify_M_ObjectsOf_Class_OutOf_N' => 'クラス%2$sの%3$d中%1$dを修正', 'UI:Menu:ModifyAll' => '修正...', 'UI:Menu:ModifyAll_Class' => 'Modify %1$s objects...~~', @@ -1180,7 +1179,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:BulkModify_Count_DistinctValues' => '%1$d 個の個別の値:', 'UI:BulkModify:Value_Exists_N_Times' => '%1$s, %2$d 回存在', 'UI:BulkModify:N_MoreValues' => '%1$d 個以上の値...', - 'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$にセットしょうとしています。', + 'UI:AttemptingToSetAReadOnlyAttribute_Name' => '読み込み専用フィールド %1$sにセットしょうとしています。', 'UI:FailedToApplyStimuli' => 'アクションは失敗しました。', 'UI:StimulusModify_N_ObjectsOf_Class' => '%1$s: クラス%3$sの%2$dオブジェクトを修正', 'UI:CaseLogTypeYourTextHere' => 'テキストを入力ください:', diff --git a/dictionaries/ru.dictionary.itop.core.php b/dictionaries/ru.dictionary.itop.core.php index 5f084e8d5..a2adb9837 100644 --- a/dictionaries/ru.dictionary.itop.core.php +++ b/dictionaries/ru.dictionary.itop.core.php @@ -9,7 +9,7 @@ * */ Dict::Add('RU RU', 'Russian', 'Русский', array( - 'Core:DeletedObjectLabel' => '%1ы (удален)', + 'Core:DeletedObjectLabel' => '%1$sы (удален)', 'Core:DeletedObjectTip' => 'Объект был удален %1$s (%2$s)', 'Core:UnknownObjectLabel' => 'Объект не найден (class: %1$s, id: %2$d)', 'Core:UnknownObjectTip' => 'Объект не удается найти. Возможно, он был удален некоторое время назад, и журнал с тех пор был очищен.', diff --git a/dictionaries/sk.dictionary.itop.ui.php b/dictionaries/sk.dictionary.itop.ui.php index 6a4391c38..d2c802bf1 100644 --- a/dictionaries/sk.dictionary.itop.ui.php +++ b/dictionaries/sk.dictionary.itop.ui.php @@ -497,7 +497,7 @@ We hope you’ll enjoy this version as much as we enjoyed imagining and creating 'UI:Error:MandatoryTemplateParameter_group_by' => 'Parameter group_by je povinný. Skontrolujte definíciu šablóny zobrazenia.', 'UI:Error:InvalidGroupByFields' => 'Neplatný zoznam polí pre skupinu podľa: "%1$s".', 'UI:Error:UnsupportedStyleOfBlock' => 'Chyba: nepodporovaný štýl bloku: "%1$s".', - 'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %l$s nebol nájdený ako externý kľúč v triede %2$s', + 'UI:Error:IncorrectLinkDefinition_LinkedClass_Class' => 'Nesprávna definícia spojenia : trieda objektov na manažovanie : %1$s nebol nájdený ako externý kľúč v triede %2$s', 'UI:Error:Object_Class_Id_NotFound' => 'Objekt: %1$s:%2$d nebol nájdený.', 'UI:Error:WizardCircularReferenceInDependencies' => 'Chyba: Cyklický odkaz v závislostiach medzi poliami, skontrolujte dátový model.', 'UI:Error:UploadedFileTooBig' => 'Nahraný súbor je príliš veľký. (Max povolená veľkosť je %1$s). Ak chcete zmeniť tento limit, obráťte sa na správcu ITOP . (Skontrolujte, PHP konfiguráciu pre upload_max_filesize a post_max_size na serveri).', diff --git a/dictionaries/tr.dictionary.itop.core.php b/dictionaries/tr.dictionary.itop.core.php index 908beb972..e6e697fcd 100644 --- a/dictionaries/tr.dictionary.itop.core.php +++ b/dictionaries/tr.dictionary.itop.core.php @@ -278,7 +278,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', array( 'Change:AttName_SetTo_NewValue_PreviousValue_OldValue' => '%1$s\'nin değeri %2$s olarak atandı (önceki değer: %3$s)', 'Change:AttName_SetTo' => '%1$s\'nin değeri %2$s olarak atandı', 'Change:Text_AppendedTo_AttName' => '%2$s\'ye %1$s eklendi', - 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$\'nin değeri deiştirildi, önceki değer: %2$s', + 'Change:AttName_Changed_PreviousValue_OldValue' => '%1$s nin değeri deiştirildi, önceki değer: %2$s', 'Change:AttName_Changed' => '%1$s değiştirildi', 'Change:AttName_EntryAdded' => '%1$s değiştirilmiş, yeni giriş eklendi.', 'Change:State_Changed_NewValue_OldValue' => 'Changed from %2$s to %1$s~~', diff --git a/dictionaries/ui/layouts/navigation-menu/pt_br.dictionary.itop.navigation-menu.php b/dictionaries/ui/layouts/navigation-menu/pt_br.dictionary.itop.navigation-menu.php index dffb28180..667513067 100644 --- a/dictionaries/ui/layouts/navigation-menu/pt_br.dictionary.itop.navigation-menu.php +++ b/dictionaries/ui/layouts/navigation-menu/pt_br.dictionary.itop.navigation-menu.php @@ -31,4 +31,4 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', array( 'UI:Layout:NavigationMenu:UserMenu:Toggler:Label' => 'Abrir menu do usuário', 'UI:Layout:NavigationMenu:KeyboardShortcut:FocusFilter' => 'Filtrar entradas de menu', -)); \ No newline at end of file +)); diff --git a/pages/schema.php b/pages/schema.php index 170553188..a12fe92ee 100644 --- a/pages/schema.php +++ b/pages/schema.php @@ -290,7 +290,6 @@ function DisplayEvents(WebPage $oPage, $sClass) foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sChildClass) { if (!MetaModel::IsAbstract($sChildClass)) { $oObject = MetaModel::NewObject($sChildClass); - $aSources[] = $oObject->GetObjectUniqId(); break; } } @@ -299,7 +298,6 @@ function DisplayEvents(WebPage $oPage, $sClass) } } else { $oObject = MetaModel::NewObject($sClass); - $aSources[] = $oObject->GetObjectUniqId(); foreach (MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, false) as $sParentClass) { $aSources[] = $sParentClass; } @@ -320,12 +318,19 @@ function DisplayEvents(WebPage $oPage, $sClass) }); $aColumns = [ 'event' => ['label' => Dict::S('UI:Schema:Events:Event')], - 'listener' => ['label' => Dict::S('UI:Schema:Events:Listener')], + 'callback' => ['label' => Dict::S('UI:Schema:Events:Listener')], 'priority' => ['label' => Dict::S('UI:Schema:Events:Rank')], 'module' => ['label' => Dict::S('UI:Schema:Events:Module')], ]; + // Get the object listeners first $aRows = []; $oReflectionClass = new ReflectionClass($sClass); + if ($oReflectionClass->isInstantiable()) { + /** @var DBObject $oClass */ + $oClass = new $sClass(); + $aRows = $oClass->GetListeners(); + } + foreach ($aListeners as $aListener) { if (is_object($aListener['callback'][0])) { $sListenerClass = $sClass; @@ -343,7 +348,7 @@ function DisplayEvents(WebPage $oPage, $sClass) } $aRows[] = [ 'event' => $aListener['event'], - 'listener' => $sListener, + 'callback' => $sListener, 'priority' => $aListener['priority'], 'module' => $aListener['module'], ]; diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index b47ba1908..10aeba488 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1449,17 +1449,12 @@ EOF } $sMethods .= "\n $sCallbackFct\n\n"; } - if (strpos($sCallback, '::') === false) { - $sEventListener = '[$this, \''.$sCallback.'\']'; - } else { - $sEventListener = "'$sCallback'"; - } $sListenerRank = (float)($oListener->GetChildText('rank', '0')); $sEvents .= <<m_sObjectUniqId, [], null, $sListenerRank, '$sModuleRelativeDir'); + \$this->RegisterCRUDListener("$sEventName", '$sCallback', $sListenerRank, '$sModuleRelativeDir'); PHP; } } diff --git a/sources/Application/UI/Links/AbstractBlockLinkSetViewTable.php b/sources/Application/UI/Links/AbstractBlockLinkSetViewTable.php index ee2d91546..dfc1522e3 100644 --- a/sources/Application/UI/Links/AbstractBlockLinkSetViewTable.php +++ b/sources/Application/UI/Links/AbstractBlockLinkSetViewTable.php @@ -70,6 +70,9 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock /** @var AttributeLinkedSet $oAttDef attribute link set */ protected AttributeLinkedSet $oAttDef; + /** @var bool $bIsAttEditable Is attribute editable */ + protected bool $bIsAttEditable; + /** @var string $sTargetClass links target classname */ protected string $sTargetClass; @@ -119,11 +122,12 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock private function Init() { $this->sTargetClass = $this->GetTargetClass(); - + $this->InitIsAttEditable(); + // User rights - $this->bIsAllowCreate = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES; - $this->bIsAllowModify = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES; - $this->bIsAllowDelete = UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES; + $this->bIsAllowCreate = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_CREATE) == UR_ALLOWED_YES; + $this->bIsAllowModify = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_MODIFY) == UR_ALLOWED_YES; + $this->bIsAllowDelete = $this->bIsAttEditable && UserRights::IsActionAllowed($this->oAttDef->GetLinkedClass(), UR_ACTION_DELETE) == UR_ALLOWED_YES; } @@ -196,6 +200,26 @@ abstract class AbstractBlockLinkSetViewTable extends UIContentBlock $this->AddSubBlock($oBlock->GetRenderContent($oPage, $this->GetExtraParam(), $this->sTableId)); } + /** + * @return void + * @throws \CoreException + */ + private function InitIsAttEditable(): void + { + $iFlags = 0; + + if ($this->oDbObject->IsNew()) + { + $iFlags = $this->oDbObject->GetInitialStateAttributeFlags($this->sAttCode); + } + else + { + $iFlags = $this->oDbObject->GetAttributeFlags($this->sAttCode); + } + + $this->bIsAttEditable = !($iFlags & (OPT_ATT_READONLY | OPT_ATT_SLAVE | OPT_ATT_HIDDEN)); + } + /** * GetTableId. * diff --git a/sources/Service/Events/EventService.php b/sources/Service/Events/EventService.php index e29b56f03..c79e5a38d 100644 --- a/sources/Service/Events/EventService.php +++ b/sources/Service/Events/EventService.php @@ -11,6 +11,7 @@ use Combodo\iTop\Service\Events\Description\EventDescription; use Combodo\iTop\Service\Module\ModuleService; use ContextTag; use CoreException; +use DBObject; use Exception; use ExecutionKPI; use ReflectionClass; @@ -54,6 +55,12 @@ final class EventService /** * Register a callback for a specific event * + * **Warning** : be ultra careful on memory footprint ! each callback will be saved in {@see aEventListeners}, and a callback is + * made of the whole object instance and the method name ({@link https://www.php.net/manual/en/language.types.callable.php}). + * For example to register on DBObject instances, you should better use {@see DBObject::RegisterCRUDListener()} + * + * @uses aEventListeners + * * @api * @param string $sEvent corresponding event * @param callable $callback The callback to call @@ -62,8 +69,12 @@ final class EventService * @param array|string|null $context context filter * @param float $fPriority optional priority for callback order * - * @return string Id of the registration + * @return string registration identifier * + * @see DBObject::RegisterCRUDListener() to register in DBObject instances instead, to reduce memory footprint (callback saving) + * + * @since 3.1.0 method creation + * @since 3.1.0-3 3.1.1 3.2.0 N°6716 PHPDoc change to warn on memory footprint, and {@see DBObject::RegisterCRUDListener()} alternative */ public static function RegisterListener(string $sEvent, callable $callback, $sEventSource = null, array $aCallbackData = [], $context = null, float $fPriority = 0.0, $sModuleId = ''): string { diff --git a/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php b/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php new file mode 100644 index 000000000..8b54282f6 --- /dev/null +++ b/tests/php-unit-tests/integration-tests/DictionariesConsistencyAfterSetupTest.php @@ -0,0 +1,242 @@ + [ + 'sTemplate' => null, + 'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1', + ], + 'traduction that breaks expected nb of arguments' => [ + 'sTemplate' => 'toto %1$s titi %2$s', + 'sExpectedTraduction' => 'ITOP::DICT:FORMAT:BROKEN:KEY - 1', + ], + 'traduction ok' => [ + 'sTemplate' => 'toto %1$s titi', + 'sExpectedTraduction' => 'toto 1 titi', + ], + ]; + } + + /** + * @param $sTemplate : if null it will not create dict entry + * @since 2.7.10 N°5491 - Inconsistent dictionary entries regarding arguments to pass to Dict::Format + * @dataProvider FormatProvider + */ + public function testFormatWithOneArgumentAndCustomKey(?string $sTemplate, $sExpectedTranslation){ + //tricky way to mock GetLabelAndLangCode behavior via connected user language + $sLangCode = \Dict::GetUserLanguage(); + $aDictByLang = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData'); + $sDictKey = 'ITOP::DICT:FORMAT:BROKEN:KEY'; + + if (! is_null($sTemplate)){ + $aDictByLang[$sLangCode][$sDictKey] = $sTemplate; + } + + $this->SetNonPublicStaticProperty(\Dict::class, 'm_aData', $aDictByLang); + + $this->assertEquals($sExpectedTranslation, \Dict::Format($sDictKey, "1")); + } + + //test works after setup (no annotation @beforesetup) + //even if it does not extend ItopDataTestCase + private function ReadDictKeys($sLangCode) : array { + \Dict::InitLangIfNeeded($sLangCode); + + $aDictEntries = $this->GetNonPublicStaticProperty(\Dict::class, 'm_aData'); + return $aDictEntries[$sLangCode]; + } + + /** + * foreach dictionnary label map (key/value) it counts the number argument that should be passed to use Dict::Format + * examples: + * for "gabu zomeu" label there are no args + * for "shadok %1 %2 %3" there are 3 args + * + * limitation: there is no validation check for "%3 itop %2 combodo" which seems unconsistent + * @param $aDictEntry + * + * @return array + */ + private function GetKeyArgCountMap($aDictEntry) { + $aKeyArgsCount = []; + foreach ($aDictEntry as $sKey => $sValue){ + $aKeyArgsCount[$sKey] = $this->countArg($sValue); + } + ksort($aKeyArgsCount); + return $aKeyArgsCount; + } + + private function countArg($sLabel) { + $iMaxIndex = 0; + if (preg_match_all("/%(\d+)/", $sLabel, $aMatches)){ + $aSubMatches = $aMatches[1]; + if (is_array($aSubMatches)){ + foreach ($aSubMatches as $aCurrentMatch){ + $iIndex = $aCurrentMatch; + $iMaxIndex = ($iMaxIndex < $iIndex) ? $iIndex : $iMaxIndex; + } + } + } else if ((false !== strpos($sLabel, "%s")) + || (false !== strpos($sLabel, "%d")) + ){ + $iMaxIndex = 1; + } + + return $iMaxIndex; + } + + /** + * Warning: hardcoded list of languages + * It is hard to have it dynamically via Dict::GetLanguages as for each lang Dict::Init should be called first + **/ + public function LangCodeProvider(){ + return [ + 'cs' => [ 'CS CZ' ], + 'da' => [ 'DA DA' ], + 'de' => [ 'DE DE' ], + 'en' => [ 'EN US' ], + 'es' => [ 'ES CR' ], + 'fr' => [ 'FR FR' ], + 'hu' => [ 'HU HU' ], + 'it' => [ 'IT IT' ], + 'ja' => [ 'JA JP' ], + 'nl' => [ 'NL NL' ], + 'pt' => [ 'PT BR' ], + 'ru' => [ 'RU RU' ], + 'sk' => [ 'SK SK' ], + 'tr' => [ 'TR TR' ], + 'zh' => [ 'ZH CN' ], + ]; + } + + /** + * compare en and other dictionaries and check that for all labels there is the same number of arguments + * if not Dict::Format could raise an exception for some languages. translation should be done again... + * @dataProvider LangCodeProvider + */ + public function testDictEntryValues($sLanguageCodeToTest) + { + $sReferenceLangCode = 'EN US'; + $aReferenceLangDictEntry = $this->ReadDictKeys($sReferenceLangCode); + + $aDictEntry = $this->ReadDictKeys($sLanguageCodeToTest); + + + $aKeyArgsCountMap = []; + $aKeyArgsCountMap[$sReferenceLangCode] = $this->GetKeyArgCountMap($aReferenceLangDictEntry); + //$aKeyArgsCountMap[$sCode] = $this->GetKeyArgCountMap($aDictEntry); + + //set user language + $this->SetNonPublicStaticProperty(\Dict::class, 'm_sCurrentLanguage', $sLanguageCodeToTest); + + $aMismatchedKeys = []; + + foreach ($aKeyArgsCountMap[$sReferenceLangCode] as $sKey => $iExpectedNbOfArgs){ + if (in_array($sKey, self::$aLabelCodeNotToCheck)){ + //false positive: do not test + continue; + } + + if (array_key_exists($sKey, $aDictEntry)){ + $aPlaceHolders = []; + for ($i=0; $i<$iExpectedNbOfArgs; $i++){ + $aPlaceHolders[]=$i; + } + + $sLabelTemplate = $aDictEntry[$sKey]; + try{ + vsprintf($sLabelTemplate, $aPlaceHolders); + } catch(\Throwable $e){ + $sError = $e->getMessage(); + if (array_key_exists($sError, $aMismatchedKeys)){ + $aMismatchedKeys[$sError][$sKey] = $iExpectedNbOfArgs; + } else { + $aMismatchedKeys[$sError] = [$sKey => $iExpectedNbOfArgs]; + } + } + } + } + + $iCount = 0; + foreach ($aMismatchedKeys as $sError => $aKeys){ + var_dump($sError); + foreach ($aKeys as $sKey => $iExpectedNbOfArgs) { + $iCount++; + if ($sReferenceLangCode === $sLanguageCodeToTest) { + var_dump([ + 'key label' => $sKey, + 'expected nb of expected args' => $iExpectedNbOfArgs, + "key value in $sLanguageCodeToTest" => $aDictEntry[$sKey], + ]); + } else { + var_dump([ + 'key label' => $sKey, + 'expected nb of expected args' => $iExpectedNbOfArgs, + "key value in $sLanguageCodeToTest" => $aDictEntry[$sKey], + "key value in $sReferenceLangCode" => $aReferenceLangDictEntry[$sKey], + ]); + } + } + } + + $sErrorMsg = sprintf("%s broken propertie(s) on $sLanguageCodeToTest dictionaries! either change the dict value in $sLanguageCodeToTest or add it in ignored label list (cf aLabelCodeNotToCheck)", $iCount); + $this->assertEquals([], $aMismatchedKeys, $sErrorMsg); + } + + public function VsprintfProvider(){ + return [ + 'not enough args' => [ + "sLabelTemplate" => "$1%s", + "aPlaceHolders" => [], + ], + 'exact nb of args' => [ + "sLabelTemplate" => "$1%s", + "aPlaceHolders" => ["1"], + ], + 'too much args' => [ + "sLabelTemplate" => "$1%s", + "aPlaceHolders" => ["1", "2"], + ], + '\"% ok\" without args' => [ + "sLabelTemplate" => "% ok", + "aPlaceHolders" => [], + ], + '\"% ok $1%s\" without args' => [ + "sLabelTemplate" => "% ok", + "aPlaceHolders" => ['1'], + ], + ]; + } + + /** + * @dataProvider VsprintfProvider + public function testVsprintf($sLabelTemplate, $aPlaceHolders){ + try{ + $this->markTestSkipped("usefull to check a specific PHP version behavior"); + vsprintf($sLabelTemplate, $aPlaceHolders); + $this->assertTrue(true); + } catch(\Throwable $e) { + $this->assertTrue(false, "label \'" . $sLabelTemplate . " failed with " . var_export($aPlaceHolders, true) ); + } + } + */ +} diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php index 416057447..d7ee25386 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopTestCase.php @@ -275,9 +275,8 @@ abstract class ItopTestCase extends TestCase return $method->invokeArgs($oObject, $aArgs); } - /** - * @since 3.1.0 + * @since 2.7.10 3.1.0 */ public function GetNonPublicStaticProperty(string $sClass, string $sProperty) { @@ -304,7 +303,7 @@ abstract class ItopTestCase extends TestCase } /** - * @since 3.1.0 + * @since 2.7.10 3.1.0 */ private function GetProperty(string $sClass, string $sProperty): \ReflectionProperty { @@ -330,7 +329,7 @@ abstract class ItopTestCase extends TestCase } /** - * @since 3.1.0 + * @since 2.7.10 3.1.0 */ public function SetNonPublicStaticProperty(string $sClass, string $sProperty, $value) { @@ -410,4 +409,4 @@ abstract class ItopTestCase extends TestCase } closedir($dir); } -} \ No newline at end of file +} diff --git a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php index accbbca48..9b9c3f6b8 100644 --- a/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php +++ b/tests/php-unit-tests/unitary-tests/core/CRUDEventTest.php @@ -12,6 +12,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use ContactType; use CoreException; use DBObject; +use DBObject\MockDBObjectWithCRUDEventListener; use DBObjectSet; use DBSearch; use lnkPersonToTeam; @@ -518,6 +519,24 @@ class CRUDEventTest extends ItopDataTestCase $this->assertEquals(2, self::$aEventCalls[EVENT_DB_LINKS_CHANGED]); } + + // Tests with MockDBObject + public function testFireCRUDEvent() + { + $this->RequireOnceUnitTestFile('DBObject/MockDBObjectWithCRUDEventListener.php'); + + // For Metamodel list of classes + MockDBObjectWithCRUDEventListener::Init(); + $oDBObject = new MockDBObjectWithCRUDEventListener(); + $oDBObject2 = new MockDBObjectWithCRUDEventListener(); + + $oDBObject->FireEvent(MockDBObjectWithCRUDEventListener::TEST_EVENT); + + $this->assertNotNull($oDBObject->oEventDataReceived); + $this->assertNull($oDBObject2->oEventDataReceived); + + //echo($oDBObject->oEventDataReceived->Get('debug_info')); + } } /** diff --git a/tests/php-unit-tests/unitary-tests/core/DBObject/MockDBObjectWithCRUDEventListener.php b/tests/php-unit-tests/unitary-tests/core/DBObject/MockDBObjectWithCRUDEventListener.php new file mode 100644 index 000000000..f81ce637c --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/core/DBObject/MockDBObjectWithCRUDEventListener.php @@ -0,0 +1,44 @@ + 'bizmodel, searchable', + 'key_type' => 'autoincrement', + 'name_attcode' => '', + 'state_attcode' => '', + 'reconc_keys' => [], + 'db_table' => 'priv_unit_tests_mock', + 'db_key_field' => 'id', + 'db_finalclass_field' => '', + 'display_template' => '', + 'indexes' => [], + ); + MetaModel::Init_Params($aParams); + } + + protected function RegisterEventListeners() + { + $this->RegisterCRUDListener(self::TEST_EVENT, 'TestEventCallback', 0, 'unit-test'); + } + + public function TestEventCallback(EventData $oEventData) + { + $this->oEventDataReceived = $oEventData; + } +} \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php index 28d92a785..51b93a416 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php @@ -941,4 +941,40 @@ class DBObjectTest extends ItopDataTestCase { return $this->aReloadCount[$sClass][$sKey] ?? 0; } + + /** + * @since 3.1.0-3 3.1.1 3.2.0 N°6716 test creation + */ + public function testConstructorMemoryFootprint():void + { + $idx = 0; + $fStart = microtime(true); + $fStartLoop = $fStart; + $iInitialPeak = 0; + $iMaxAllowedMemoryIncrease = 1 * 1024 * 1024; + + for ($i = 0; $i < 5000; $i++) { + /** @noinspection PhpUnusedLocalVariableInspection We intentionally use a reference that will disappear on each loop */ + $oPerson = new \Person(); + if (0 == ($idx % 100)) { + $fDuration = microtime(true) - $fStartLoop; + $iMemoryPeakUsage = memory_get_peak_usage(); + if ($iInitialPeak === 0) { + $iInitialPeak = $iMemoryPeakUsage; + $sInitialPeak = \utils::BytesToFriendlyFormat($iInitialPeak, 4); + } + + $sCurrPeak = \utils::BytesToFriendlyFormat($iMemoryPeakUsage, 4); + echo "$idx ".sprintf('%.1f ms', $fDuration * 1000)." - Peak Memory Usage: $sCurrPeak\n"; + + $this->assertTrue(($iMemoryPeakUsage - $iInitialPeak) <= $iMaxAllowedMemoryIncrease , "Peak memory changed from $sInitialPeak to $sCurrPeak after $i loops"); + + $fStartLoop = microtime(true); + } + $idx++; + } + + $fTotalDuration = microtime(true) - $fStart; + echo 'Total duration: '.sprintf('%.3f s', $fTotalDuration)."\n\n"; + } }