N°8760: code legacy refactoring around module computation (AnalyzeInstallation)

AnalyzeInstallation refactoring

fix ci call to ModuleInstallationService

ci: fix code style

get rid of itop_version

refactoring: make code simpler

get rid of unused name_db

refactoring setup: rename version_db by installed_version

refactoring setup: rename version_code by available_version

code cleanup: avoid useless runtimeenv instanciation
This commit is contained in:
odain
2025-11-10 16:22:16 +01:00
parent 73f868ac83
commit 76178c16b8
13 changed files with 12464 additions and 151 deletions

View File

@@ -0,0 +1,130 @@
<?php
require_once __DIR__.'/ModuleInstallationService.php';
class AnalyzeInstallation
{
private static AnalyzeInstallation $oInstance;
private ?array $aAvailableModules = null;
private ?array $aSelectInstall = null;
protected function __construct()
{
}
final public static function GetInstance(): AnalyzeInstallation
{
if (!isset(self::$oInstance)) {
self::$oInstance = new AnalyzeInstallation();
}
return self::$oInstance;
}
final public static function SetInstance(?AnalyzeInstallation $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* Analyzes the current installation and the possibilities
*
* @param null|Config $oConfig Defines the target environment (DB)
* @param mixed $modulesPath Either a single string or an array of absolute paths
* @param bool $bAbortOnMissingDependency ...
* @param array $aModulesToLoad List of modules to search for, defaults to all if omitted
*
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'uninstall' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
* )
* 'label' => ...
* 'dependencies' => array(<module1>, <module2>, ...)
* 'visible' => true | false
* )
* )
* @throws \Exception
*/
public function AnalyzeInstallation(?Config $oConfig, mixed $modulesPath, bool $bAbortOnMissingDependency = false, ?array $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'installed_version' => '',
'available_version' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
if (! is_null($this->aAvailableModules)) {
//test only
$aAvailableModules = $this->aAvailableModules;
} else {
$aAvailableModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
}
foreach ($aAvailableModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
$aModuleInfo['installed_version'] = '';
$aModuleInfo['available_version'] = $sModuleVersion;
if ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
$aCurrentlyInstalledModules = ModuleInstallationService::GetInstance()->ReadFromDB($oConfig);
// Adjust the list of proposed modules
foreach ($aCurrentlyInstalledModules as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
continue;
}
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['installed_version'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['mandatory']) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
}
}

View File

@@ -0,0 +1,159 @@
<?php
class ModuleInstallationService
{
private static ModuleInstallationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): ModuleInstallationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new ModuleInstallationService();
}
return self::$oInstance;
}
final public static function SetInstance(?ModuleInstallationService $oInstance): void
{
static::$oInstance = $oInstance;
}
private ?array $aSelectInstall = null;
public function ReadFromDB(?Config $oConfig): array
{
try {
$aSelectInstall = [];
if (! is_null($oConfig)) {
if (! is_null($this->aSelectInstall)) {
//test only
$aSelectInstall = $this->aSelectInstall;
} else {
CMDBSource::InitFromConfig($oConfig);
//read db module installations
$aSelectInstallOld = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install");
//file_put_contents(APPROOT."/tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules.json", json_encode($aSelectInstallOld, JSON_PRETTY_PRINT));
$iRootId = CMDBSource::QueryToScalar("SELECT max(parent_id) FROM ".$oConfig->Get('db_subname')."priv_module_install");
$sDbSubName = $oConfig->Get('db_subname');
// Get the latest installed modules, without the "root" ones (iTop version and datamodel version)
$sSQL = <<<SQL
SELECT * FROM $sDbSubName.priv_module_install
WHERE
parent_id='$iRootId'
OR id='$iRootId'
SQL;
$aSelectInstall = CMDBSource::QueryToArray($sSQL);
//file_put_contents(APPROOT."/tests/php-unit-tests/unitary-tests/setup/ressources/priv_modules2.json", json_encode($aSelectInstall, JSON_PRETTY_PRINT));
}
}
} catch (MySQLException $e) {
// No database or erroneous information
}
return $this->ComputeInstalledModules($aSelectInstall);
}
private function ComputeInstalledModulesLegacy(array $aSelectInstall): array
{
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
$iRootId = 0;
foreach ($aSelectInstall as $aInstall) {
if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) {
// Root module, what is its ID ?
$iId = (int) $aInstall['id'];
if ($iId > $iRootId) {
$iRootId = $iId;
}
}
}
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$iInstalled = strtotime($aInstall['installed']);
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$sModuleName = ROOT_MODULE;
} elseif ($aInstall['parent_id'] != $iRootId) {
// Skip all modules belonging to previous installations
continue;
}
if (array_key_exists($sModuleName, $aInstallByModule)) {
if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) {
continue;
}
}
if ($aInstall['parent_id'] == 0) {
$aInstallByModule[$sModuleName]['installed_version'] = $sModuleVersion;
}
$aInstallByModule[$sModuleName]['installed'] = $iInstalled;
$aInstallByModule[$sModuleName]['version'] = $sModuleVersion;
}
return $aInstallByModule;
}
private function ComputeInstalledModules(array $aSelectInstall): array
{
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
//module installation datetime is mostly the same for all modules
//unless there was issue recording things in DB
$sFirstDatetime = null;
$iFirstTime = -1;
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$sDatetime = $aInstall['installed'];
if (is_null($sFirstDatetime)) {
$sFirstDatetime = $sDatetime;
$iFirstTime = strtotime($sDatetime);
$iInstalled = $iFirstTime;
} elseif ($sDatetime === $sFirstDatetime) {
$iInstalled = $iFirstTime;
} else {
$sDatetime = $aInstall['installed'];
$iInstalled = strtotime($sDatetime);
}
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$aInstallByModule[ROOT_MODULE] = [
'installed_version' => $sModuleVersion,
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
} else {
$aInstallByModule[$sModuleName] = [
'installed' => $iInstalled,
'version' => $sModuleVersion,
];
}
}
return $aInstallByModule;
}
}

View File

@@ -120,10 +120,6 @@ class ModuleDiscovery
if (is_null($aArgs) || ! is_array($aArgs)) {
throw new ModuleFileReaderException("Error parsing module file args", 0, null, $sFilePath);
}
if (!array_key_exists('itop_version', $aArgs)) {
// Assume 1.0.2
$aArgs['itop_version'] = '1.0.2';
}
foreach (array_keys(self::$m_aModuleArgs) as $sArgName) {
if (!array_key_exists($sArgName, $aArgs)) {
throw new Exception("Module '$sId': missing argument '$sArgName'");

View File

@@ -32,6 +32,7 @@ require_once APPROOT."setup/modulediscovery.class.inc.php";
require_once APPROOT.'setup/modelfactory.class.inc.php';
require_once APPROOT.'setup/compiler.class.inc.php';
require_once APPROOT.'setup/extensionsmap.class.inc.php';
require_once APPROOT.'setup/AnalyzeInstallation.php';
define('MODULE_ACTION_OPTIONAL', 1);
define('MODULE_ACTION_MANDATORY', 2);
@@ -145,12 +146,12 @@ class RunTimeEnvironment
* @return array Array with the following format:
* array =>
* 'iTop' => array(
* 'version_db' => ... (could be empty in case of a fresh install)
* 'version_code => ...
* 'installed_version' => ... (could be empty in case of a fresh install)
* 'available_version => ...
* )
* <module_name> => array(
* 'version_db' => ...
* 'version_code' => ...
* 'installed_version' => ...
* 'available_version' => ...
* 'install' => array(
* 'flag' => SETUP_NEVER | SETUP_OPTIONAL | SETUP_MANDATORY
* 'message' => ...
@@ -168,137 +169,7 @@ class RunTimeEnvironment
*/
public function AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency = false, $aModulesToLoad = null)
{
$aRes = [
ROOT_MODULE => [
'version_db' => '',
'name_db' => '',
'version_code' => ITOP_VERSION_FULL,
'name_code' => ITOP_APPLICATION,
],
];
$aDirs = is_array($modulesPath) ? $modulesPath : [$modulesPath];
$aModules = ModuleDiscovery::GetAvailableModules($aDirs, $bAbortOnMissingDependency, $aModulesToLoad);
foreach ($aModules as $sModuleId => $aModuleInfo) {
list($sModuleName, $sModuleVersion) = ModuleDiscovery::GetModuleName($sModuleId);
if ($sModuleName == '') {
throw new Exception("Missing name for the module: '$sModuleId'");
}
if ($sModuleVersion == '') {
// The version must not be empty (it will be used as a criteria to determine wether a module has been installed or not)
//throw new Exception("Missing version for the module: '$sModuleId'");
$sModuleVersion = '1.0.0';
}
$sModuleAppVersion = $aModuleInfo['itop_version'];
$aModuleInfo['version_db'] = '';
$aModuleInfo['version_code'] = $sModuleVersion;
if (!in_array($sModuleAppVersion, ['1.0.0', '1.0.1', '1.0.2'])) {
// This module is NOT compatible with the current version
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is not compatible with the current version of the application',
];
} elseif ($aModuleInfo['mandatory']) {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_MANDATORY,
'message' => 'the module is part of the application',
];
} else {
$aModuleInfo['install'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
$aRes[$sModuleName] = $aModuleInfo;
}
try {
$aSelectInstall = [];
if (! is_null($oConfig)) {
CMDBSource::InitFromConfig($oConfig);
$aSelectInstall = CMDBSource::QueryToArray("SELECT * FROM ".$oConfig->Get('db_subname')."priv_module_install");
}
} catch (MySQLException $e) {
// No database or erroneous information
}
// Build the list of installed module (get the latest installation)
//
$aInstallByModule = []; // array of <module> => array ('installed' => timestamp, 'version' => <version>)
$iRootId = 0;
foreach ($aSelectInstall as $aInstall) {
if (($aInstall['parent_id'] == 0) && ($aInstall['name'] != 'datamodel')) {
// Root module, what is its ID ?
$iId = (int) $aInstall['id'];
if ($iId > $iRootId) {
$iRootId = $iId;
}
}
}
foreach ($aSelectInstall as $aInstall) {
//$aInstall['comment']; // unsused
$iInstalled = strtotime($aInstall['installed']);
$sModuleName = $aInstall['name'];
$sModuleVersion = $aInstall['version'];
if ($sModuleVersion == '') {
// Though the version cannot be empty in iTop 2.0, it used to be possible
// therefore we have to put something here or the module will not be considered
// as being installed
$sModuleVersion = '0.0.0';
}
if ($aInstall['parent_id'] == 0) {
$sModuleName = ROOT_MODULE;
} elseif ($aInstall['parent_id'] != $iRootId) {
// Skip all modules belonging to previous installations
continue;
}
if (array_key_exists($sModuleName, $aInstallByModule)) {
if ($iInstalled < $aInstallByModule[$sModuleName]['installed']) {
continue;
}
}
if ($aInstall['parent_id'] == 0) {
$aRes[$sModuleName]['version_db'] = $sModuleVersion;
$aRes[$sModuleName]['name_db'] = $aInstall['name'];
}
$aInstallByModule[$sModuleName]['installed'] = $iInstalled;
$aInstallByModule[$sModuleName]['version'] = $sModuleVersion;
}
// Adjust the list of proposed modules
//
foreach ($aInstallByModule as $sModuleName => $aModuleDB) {
if ($sModuleName == ROOT_MODULE) {
continue;
} // Skip the main module
if (!array_key_exists($sModuleName, $aRes)) {
// A module was installed, it is not proposed in the new build... skip
continue;
}
$aRes[$sModuleName]['version_db'] = $aModuleDB['version'];
if ($aRes[$sModuleName]['install']['flag'] == MODULE_ACTION_MANDATORY) {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_IMPOSSIBLE,
'message' => 'the module is part of the application',
];
} else {
$aRes[$sModuleName]['uninstall'] = [
'flag' => MODULE_ACTION_OPTIONAL,
'message' => '',
];
}
}
return $aRes;
return AnalyzeInstallation::GetInstance()->AnalyzeInstallation($oConfig, $modulesPath, $bAbortOnMissingDependency, $aModulesToLoad);
}
/**
@@ -365,8 +236,7 @@ class RunTimeEnvironment
// Determine the installed modules and extensions
//
$oSourceConfig = new Config(APPCONF.$sSourceEnv.'/'.ITOP_CONFIG_FILE);
$oSourceEnv = new RunTimeEnvironment($sSourceEnv);
$aAvailableModules = $oSourceEnv->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
$aAvailableModules = $this->AnalyzeInstallation($oSourceConfig, $aDirsToCompile);
// Actually read the modules available for the target environment,
// but get the selection from the source environment and finally
@@ -404,7 +274,7 @@ class RunTimeEnvironment
$sModuleRootDir = $oModule->GetRootDir();
$bIsExtra = $this->oExtensionsMap->ModuleIsChosenAsPartOfAnExtension($sModule, iTopExtension::SOURCE_REMOTE);
if (array_key_exists($sModule, $aAvailableModules)) {
if (($aAvailableModules[$sModule]['version_db'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
if (($aAvailableModules[$sModule]['installed_version'] != '') || $bIsExtra && !$oModule->IsAutoSelect()) { //Extra modules are always unless they are 'AutoSelect'
$aRet[$oModule->GetName()] = $oModule;
}
}
@@ -648,7 +518,7 @@ class RunTimeEnvironment
$oInstallRec->Set('comment', json_encode($aData));
$oInstallRec->Set('parent_id', 0); // root module
$oInstallRec->Set('installed', $iInstallationTime);
$iMainItopRecord = $oInstallRec->DBInsertNoReload();
$oInstallRec->DBInsertNoReload();
// Record main installation
$oInstallRec = new ModuleInstallation();
@@ -669,7 +539,7 @@ class RunTimeEnvironment
}
$aModuleData = $aAvailableModules[$sModuleId];
$sName = $sModuleId;
$sVersion = $aModuleData['version_code'];
$sVersion = $aModuleData['available_version'];
$sUninstallable = $aModuleData['uninstallable'] ?? 'yes';
$aComments = [];
$aComments[] = $sShortComment;
@@ -975,7 +845,7 @@ class RunTimeEnvironment
{
foreach ($aAvailableModules as $sModuleId => $aModule) {
if (($sModuleId != ROOT_MODULE) && in_array($sModuleId, $aSelectedModules)) {
$aArgs = [MetaModel::GetConfig(), $aModule['version_db'], $aModule['version_code']];
$aArgs = [MetaModel::GetConfig(), $aModule['installed_version'], $aModule['available_version']];
RunTimeEnvironment::CallInstallerHandler($aAvailableModules[$sModuleId], $sHandlerName, $aArgs);
}
}
@@ -1039,7 +909,7 @@ class RunTimeEnvironment
$sRelativePath = 'env-'.$this->sTargetEnv.'/'.basename($aModule['root_dir']);
// Load data only for selected AND newly installed modules
if (in_array($sModuleId, $aSelectedModules)) {
if ($aModule['version_db'] != '') {
if ($aModule['installed_version'] != '') {
// Simulate the load of the previously loaded XML files to get the mapping of the keys
if ($bSampleData) {
$aPreviouslyLoadedFiles = static::MergeWithRelativeDir($aPreviouslyLoadedFiles, $sRelativePath, $aAvailableModules[$sModuleId]['data.struct']);

View File

@@ -1605,7 +1605,7 @@ EOF
if ($this->bUpgrade) {
// In upgrade mode, the defaults are the installed modules
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed
$aScores[$sChoiceId][$sModuleId] = true;
}
@@ -1663,7 +1663,7 @@ EOF
}
if (array_key_exists('modules', $aChoice)) {
foreach ($aChoice['modules'] as $sModuleId) {
if ($aModules[$sModuleId]['version_db'] != '') {
if ($aModules[$sModuleId]['installed_version'] != '') {
// A module corresponding to this choice is installed, increase the score of this choice
if (!isset($aScores[$sChoiceId])) {
$aScores[$sChoiceId] = [];