N°9553 - Helper for loading XML localized data during Setup

This commit is contained in:
v-dumas
2026-05-22 16:13:19 +02:00
parent ba2af7ed63
commit 2b9db73871
3 changed files with 198 additions and 39 deletions

View File

@@ -56,46 +56,13 @@ class TicketsInstaller extends ModuleInstallerAPI
if (!MetaModel::IsValidClass($oTrigger->Get('target_class'))) {
$oTrigger->DBDelete();
}
} catch (Exception $e) {
}
catch (Exception $e) {
utils::EnrichRaisedException($oTrigger, $e);
}
}
// It's not very clear if it make sense to test a particular version,
// as the loading mechanism checks object existence using reconc_keys
// and do not recreate them, nor update existing.
// Without test, new entries added to the data files, would be automatically loaded
if (($sPreviousVersion === '') ||
(version_compare($sPreviousVersion, $sCurrentVersion, '<')
&& version_compare($sPreviousVersion, '3.0.0', '<'))) {
$oDataLoader = new XMLDataLoader();
CMDBObject::SetTrackInfo("Initialization TicketsInstaller");
$oMyChange = CMDBObject::GetCurrentChange();
$sLang = null;
// - Try to get app. language from configuration fil (app. upgrade)
$sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE;
if (file_exists($sConfigFileName)) {
$oFileConfig = new Config($sConfigFileName);
if (is_object($oFileConfig)) {
$sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage()));
}
}
// - I still no language, get the default one
if (null === $sLang) {
$sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage()));
}
$sFileName = dirname(__FILE__)."/data/{$sLang}.data.itop-tickets.xml";
SetupLog::Info("Searching file: $sFileName");
if (!file_exists($sFileName)) {
$sFileName = dirname(__FILE__)."/data/en_us.data.itop-tickets.xml";
}
SetupLog::Info("Loading file: $sFileName");
$oDataLoader->StartSession($oMyChange);
$oDataLoader->LoadFile($sFileName, false, true);
$oDataLoader->EndSession();
}
// Load localized structural data: predefined query phrases for notifications
static::LoadLocalizedData($sPreviousVersion, $sCurrentVersion, $oConfiguration, '3.0.0', dirname(__FILE__)."/data/{{language_code}}.data.itop-tickets.xml");
}
}

View File

@@ -309,4 +309,94 @@ abstract class ModuleInstallerAPI
CMDBSource::CacheReset($sOrigTable);
}
/**
* @param string $sPreviousVersion The previous version of the module (empty string will force the loading)
* @param string $sCurrentVersion The current version of the module
* @param \Config $oConfiguration
* @param string $sFirstLoadingVersion The first module version for which the data loading should be performed (e.g. '3.0.0')
* @param string $sFilePattern The pattern of the file to load, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public static function LoadLocalizedData(string $sPreviousVersion, string $sCurrentVersion, Config $oConfiguration, string $sFirstLoadingVersion, string $sFilePattern): void
{
// It's not very clear if it makes sense to test a particular version,
// as the loading mechanism checks object existence using reconc_keys
// and do not recreate them, nor update existing.
// Without test, new entries added to the data files, would be automatically loaded
if (($sPreviousVersion === '') ||
(version_compare($sPreviousVersion, $sCurrentVersion, '<')
&& version_compare($sPreviousVersion, $sFirstLoadingVersion, '<'))) {
$sFileName = self::GetLocalizedFileName($oConfiguration, $sFilePattern);
if ($sFileName !== '') {
SetupLog::Info("Loading file: $sFileName");
self::XMLFileLoad($sFileName);
}
}
}
/**
* @param array|string $sFileName
* @param \XMLDataLoader $oDataLoader
*
* @return void
* @throws \Exception
*/
public static function XMLFileLoad(string $sFileName): void
{
if (file_exists($sFileName)) {
$oDataLoader = new XMLDataLoader();
CMDBObject::SetTrackInfo("Loading XML data from $sFileName");
$oMyChange = CMDBObject::GetCurrentChange();
SetupLog::Info("Loading file: $sFileName");
$oDataLoader->StartSession($oMyChange);
$oDataLoader->LoadFile($sFileName, false, true);
$oDataLoader->EndSession();
}
}
/**
* @param \Config $oConfiguration
* @param string $sFilePattern The full path+name of the file to localize, with {{language_code}} as placeholder for the language code (e.g. 'data.sample.{{language_code}}.xml')
*
* @return string The localized file name if found, or an empty string if not found
* @throws \ConfigException
* @throws \CoreException
*/
public static function GetLocalizedFileName(Config $oConfiguration, string $sFilePattern): string
{
$sLang = null;
if (is_object($oConfiguration)) {
$sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage()));
}
/** Old code relying on reading the file instead of using the configuration passed object
* Try to get app. language from configuration fil (app. upgrade)
$sConfigFileName = APPCONF.'production/'.ITOP_CONFIG_FILE;
if (file_exists($sConfigFileName)) {
$oFileConfig = new Config($sConfigFileName);
if (is_object($oFileConfig)) {
$sLang = str_replace(' ', '_', strtolower($oFileConfig->GetDefaultLanguage()));
}
}
**/
// - I still no language, get the default one
if (null === $sLang) {
$sLang = str_replace(' ', '_', strtolower($oConfiguration->GetDefaultLanguage()));
}
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
if (!file_exists($sFileName)) {
$sLang = 'en_us';
$sFileName = str_replace('{{language_code}}', $sLang, $sFilePattern);
}
if (file_exists($sFileName)) {
return $sFileName;
} else {
SetupLog::Warning("No data file matching the pattern $sFilePattern and language_code $sLang was found.");
return '';
}
}
}

View File

@@ -4,11 +4,12 @@ namespace Combodo\iTop\Test\UnitTest\Setup;
use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Config;
use MetaModel;
use ModuleInstallerAPI;
/**
* Class ModuleInstallerAPITest
* Class ModuleInstallerAPI
*
* @covers ModuleInstallerAPI
*
@@ -282,4 +283,105 @@ SQL
$this->assertEquals($sOrigValue, $sDstValue, "Data was not moved as expected");
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_LoadsOnFirstInstall(): void
{
// Given
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_FirstInstall_', 'fr_fr');
$this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
$this->CreateLocalizedDataFile($sTmpDir, "fr_fr", $sOrgName);
// When no previous version, and current version higher than the first loading version
ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern);
// Then data loaded
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 1);
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_DoesNotLoadWhenVersionConditionIsNotMet(): void
{
// Given
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_NoLoad_', 'en_us');
$this->CreateLocalizedDataFile($sTmpDir, "en_us", $sOrgName);
// When a previous version that is lower than the first loading version, but higher or equal to the current version
ModuleInstallerAPI::LoadLocalizedData('3.0.0', '3.1.0', $oConfig, '3.0.0', $sPattern);
// Then no data loaded
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 0);
}
/**
* @covers \ModuleInstallerAPI::LoadLocalizedData
*/
public function testLoadLocalizedData_FallbacksToEnUsWhenLanguageFileIsMissing(): void
{
[$oConfig, $sOrgName, $sTmpDir, $sPattern] = $this->PrepareLocalizedDataTestContext('XML_Load_Fallback_', 'fr_fr');
// Intentionally create ONLY en_us file
$this->CreateLocalizedDataFile($sTmpDir, 'en_us', $sOrgName);
// When loading localized data in fr_fr, but only en_us file exists
ModuleInstallerAPI::LoadLocalizedData('', '3.3.0', $oConfig, '3.0.0', $sPattern);
$this->AssertOrganizationCountByName($sOrgName, 'fr_fr', 0);
$this->AssertOrganizationCountByName($sOrgName, 'en_us', 1);
}
/**
* Prepare common context for LoadLocalizedData tests.
*
* @return array{0: Config, 1: string, 2: string, 3: string, 4: string}
*/
private function PrepareLocalizedDataTestContext(string $sOrgNamePrefix, string $sLanguage): array
{
$oConfig = MetaModel::GetConfig();
$oConfig->SetDefaultLanguage($sLanguage);
$this->assertNotNull($oConfig);
$sOrgName = $sOrgNamePrefix.uniqid();
$sTmpDir = static::CreateTmpdir();
$this->aFileToClean[] = $sTmpDir;
$sPattern = $sTmpDir.DIRECTORY_SEPARATOR.'data.{{language_code}}.xml';
return [$oConfig, $sOrgName, $sTmpDir, $sPattern];
}
private function CreateLocalizedDataFile(string $sDir, string $sLang, string $sOrgName): string
{
$sFilePath = $sDir.DIRECTORY_SEPARATOR.'data.'.$sLang.'.xml';
file_put_contents($sFilePath, $this->BuildOrganizationXml($sOrgName, $sLang));
return $sFilePath;
}
private function BuildOrganizationXml(string $sOrgName, string $sLang): string
{
$iId = random_int(100000, 999999);
$sOrgNameXml = htmlspecialchars($sOrgName, ENT_XML1);
return <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<Organization alias="Organization" id="{$iId}">
<name>{$sOrgNameXml}</name>
<code>{$sLang}</code>
<status>active</status>
</Organization>
</Set>
XML;
}
private function AssertOrganizationCountByName(string $sOrgName, string $sLanguage, int $iExpectedCount): void
{
$sOrgTable = MetaModel::DBGetTable('Organization');
$iCount = (int) CMDBSource::QueryToScalar(
"SELECT COUNT(*) FROM `{$sOrgTable}` WHERE `name` = ".CMDBSource::Quote($sOrgName)." AND `code` = ".CMDBSource::Quote($sLanguage)
);
$this->assertEquals($iExpectedCount, $iCount);
}
}