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";
+ }
}