getLineNo().')'; } parent::__construct($message, $code, $previous); } } /** * Compiler class */ class MFCompiler { const DATA_PRECOMPILED_FOLDER = 'data'.DIRECTORY_SEPARATOR.'precompiled_styles'.DIRECTORY_SEPARATOR; /** * @var string * @since 3.1.0 */ protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_CODE = 'code'; /** * @var string * @since 3.1.0 */ protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_LABEL = 'label'; /** * @var string * @since 3.1.0 */ protected const ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK = 'rank'; /** * @var string * @see self::GenerateStyleDataFromNode * @internal * @since 3.0.0 */ public const ENUM_STYLE_HOST_ELEMENT_TYPE_CLASS = 'class'; /** * @var string * @see self::GenerateStyleDataFromNode * @internal * @since 3.0.0 */ public const ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM = 'enum'; /** * Path to the "use symlinks" file * If this file is present, then we will compile to symlink ! * * @var string * * @since 3.0.0 N°4092 */ public const USE_SYMBOLIC_LINKS_FILE_PATH = APPROOT.'data/.compilation-symlinks'; /** @var \ThemeHandlerService */ protected static $oThemeHandlerService; /** * Path to the "calculate hKeys" file * If this file is present, then we don't recalculate hkeys * * @var string * @since 2.7.5 3.0.0 N°4020 */ const REBUILD_HKEYS_NEVER = APPROOT.'data/.setup-rebuild-hkeys-never'; /** @var \ModelFactory */ protected $oFactory; protected $aRootClasses; /** * @var array $aCustomListsCodes Codes of the custom zlists * @since 3.1.0 */ protected $aCustomListsCodes; protected $aLog; protected $sMainPHPCode; // Code that goes into core/main.php protected $aSnippets; protected $aRelations; /** * @var array Strings containing dynamic CSS definitions for DM classes * @since 3.0.0 */ protected $aClassesCSSRules; protected $sEnvironment; protected $sCompilationTimeStamp; /** @var array dynamic attributes definition * @since 3.1.0 */ protected array $aDynamicAttributeDefinitions; /** @var array dynamic attribute property definition */ protected array $aDynamicPropertyDefinitions; public function __construct($oModelFactory, $sEnvironment) { $this->oFactory = $oModelFactory; $this->sEnvironment = $sEnvironment; $this->oFactory->ApplyChanges(); $this->aCustomListsCodes = []; $this->aLog = []; $this->aDynamicAttributeDefinitions = []; $this->sMainPHPCode = '<'.'?'."php\n"; $this->sMainPHPCode .= "/**\n"; $this->sMainPHPCode .= " * This file was automatically generated by the compiler on ".date('Y-m-d H:i:s')." -- DO NOT EDIT\n"; $this->sMainPHPCode .= " */\n"; $this->sMainPHPCode .= "\n"; $this->sCompilationTimeStamp = "".microtime(true); $this->sMainPHPCode .= "define('COMPILATION_TIMESTAMP', '".$this->sCompilationTimeStamp."');\n"; $this->aSnippets = []; $this->aRelations = []; $this->aClassesCSSRules = []; } protected function Log($sText) { $this->aLog[] = $sText; } protected function DumpLog($oPage) { foreach ($this->aLog as $sText) { $oPage->p($sText); } } public function GetLog() { return $this->aLog; } /** * @return bool if flag is present true, false otherwise * * @uses \file_exists() * @uses USE_SYMBOLIC_LINKS_FILE_PATH * * @since 3.0.0 N°4092 */ public static function IsUseSymbolicLinksFlagPresent(): bool { return (file_exists(static::USE_SYMBOLIC_LINKS_FILE_PATH)); } /** * This is to check if the functionality can be used. As this is really only useful for developers, * this is strictly limited and not available on any iTop instance ! * * @return bool Check that the symlinks flag can be used * * always false if not in dev env * * `symlink` function non-existent : false * * true otherwise * * @uses utils::IsDevelopmentEnvironment() * @uses \function_exists() * * @since 3.0.0 N°4092 */ public static function CanUseSymbolicLinksFlagBeUsed(): bool { if (false === utils::IsDevelopmentEnvironment()) { return false; } if (false === function_exists('symlink')) { return false; } return true; } /** * @param bool $bUseSymbolicLinks * * @uses USE_SYMBOLIC_LINKS_FILE_PATH * * @since 3.0.0 N°4092 */ public static function SetUseSymbolicLinksFlag(bool $bUseSymbolicLinks): void { $bIsUseSymlinksFlagPresent = (static::IsUseSymbolicLinksFlagPresent()); if ($bUseSymbolicLinks) { if ($bIsUseSymlinksFlagPresent) { return; } touch(static::USE_SYMBOLIC_LINKS_FILE_PATH); return; } if (!$bIsUseSymlinksFlagPresent) { return; } unlink(static::USE_SYMBOLIC_LINKS_FILE_PATH); } /** * @return bool possible return values : * * if flag is present true, false otherwise * * @uses \file_exists() * @uses REBUILD_HKEYS_NEVER * * @since 2.7.5 3.0.0 */ public static function SkipRebuildHKeys() { return (file_exists(static::REBUILD_HKEYS_NEVER)); } /** * Compile the data model into PHP files and data structures * * @param string $sTargetDir The target directory where to put the resulting files * @param Page $oP For some output... * @param bool $bUseSymbolicLinks * @param bool $bSkipTempDir * * @return void * @throws Exception */ public function Compile($sTargetDir, $oP = null, $bUseSymbolicLinks = null, $bSkipTempDir = false) { if (is_null($bUseSymbolicLinks)) { $bUseSymbolicLinks = false; if (self::CanUseSymbolicLinksFlagBeUsed() && self::IsUseSymbolicLinksFlagPresent()) { // We are only overriding the useSymLinks option if the consumer didn't specify anything // The toolkit always send this parameter for example, but not the Designer Connector $bUseSymbolicLinks = true; } } $sFinalTargetDir = $sTargetDir; $bIsAlreadyInMaintenanceMode = SetupUtils::IsInMaintenanceMode(); $sConfigFilePath = utils::GetConfigFilePath($this->sEnvironment); if (is_file($sConfigFilePath)) { $oConfig = new Config($sConfigFilePath); } else { $oConfig = null; } if (($this->sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) { SetupUtils::EnterMaintenanceMode($oConfig); } if ($bUseSymbolicLinks || $bSkipTempDir) { // Skip the creation of a temporary dictionary, not compatible with symbolic links $sTempTargetDir = $sFinalTargetDir; SetupUtils::rrmdir($sFinalTargetDir); SetupUtils::builddir($sFinalTargetDir); // Here is the directory } else { // Create a temporary directory // Once the compilation is 100% successful, then move the results into the target directory $sTempTargetDir = tempnam(SetupUtils::GetTmpDir(), 'itop-'); unlink($sTempTargetDir); // I need a directory, not a file... SetupUtils::builddir($sTempTargetDir); // Here is the directory } try { $this->DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks); } catch (Exception $e) { if ($sTempTargetDir != $sFinalTargetDir) { // Cleanup the temporary directory SetupUtils::rrmdir($sTempTargetDir); } if (($this->sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) { SetupUtils::ExitMaintenanceMode(); } throw $e; } if ($sTempTargetDir != $sFinalTargetDir) { // Move the results to the target directory SetupUtils::movedir($sTempTargetDir, $sFinalTargetDir); } if (($this->sEnvironment == 'production') && !$bIsAlreadyInMaintenanceMode) { SetupUtils::ExitMaintenanceMode(); } // Reset the opcache since otherwise the PHP "model" files may still be cached !! // In case of bad luck (this happens **sometimes** - see N. 550), we may analyze the database structure // with the previous datamodel still loaded (in opcode cache) and thus fail to create the new fields // Finally the application crashes (because of the missing field) when the cache gets updated if (function_exists('opcache_reset')) { // Zend opcode cache opcache_reset(); } if (function_exists('apc_clear_cache')) { // old style APC apc_clear_cache(); } } /** * Perform the actual "Compilation" of all modules * @param string $sTempTargetDir * @param string $sFinalTargetDir * @param Page $oP * @param bool $bUseSymbolicLinks * @throws Exception */ protected function DoCompile($sTempTargetDir, $sFinalTargetDir, $oP = null, $bUseSymbolicLinks = false) { $aAllClasses = array(); // flat list of classes $aModulesInfo = array(); // Hash array of module_name => array('version' => string, 'root_dir' => string) // Determine the target modules for the MENUS // $aMenuNodes = array(); $aMenusByModule = array(); foreach ($this->oFactory->GetNodes('menus/menu') as $oMenuNode) { $sMenuId = $oMenuNode->getAttribute('id'); $aMenuNodes[$sMenuId] = $oMenuNode; $sModuleMenu = $oMenuNode->getAttribute('_created_in'); $aMenusByModule[$sModuleMenu][] = $sMenuId; } // Determine the target module (exactly one!) for USER RIGHTS // This used to be based solely on the module which created the user_rights node first // Unfortunately, our sample extension was delivered with the xml structure, resulting in the new module to be the recipient of the compilation // Then model.itop-profiles-itil would not exist... resulting in an error after the compilation (and the actual product of the compiler would never be included // The bullet proof implementation would be to compile in a separate directory as it has been done with the dictionaries... that's another story $aModules = $this->oFactory->GetLoadedModules(); $sUserRightsModule = ''; foreach ($aModules as $foo => $oModule) { if ($oModule->GetName() == 'itop-profiles-itil') { $sUserRightsModule = 'itop-profiles-itil'; break; } } $oUserRightsNode = $this->oFactory->GetNodes('user_rights')->item(0); if ($oUserRightsNode && ($sUserRightsModule == '')) { // Legacy algorithm (itop <= 2.0.3) $sUserRightsModule = $oUserRightsNode->getAttribute('_created_in'); } $this->Log("User Rights module found: '$sUserRightsModule'"); // List root classes // $this->aRootClasses = array(); foreach ($this->oFactory->ListRootClasses() as $oClass) { $this->Log("Root class (with child classes): ".$oClass->getAttribute('id')); $this->aRootClasses[$oClass->getAttribute('id')] = $oClass; } $this->LoadSnippets(); $this->LoadGlobalEventListeners(); $this->LoadDynamicAttributeDefinitions(); // Compile, module by module // $aModules = $this->oFactory->GetLoadedModules(); $aDataModelFiles = array(); $aWebservicesFiles = array(); $iStart = strlen(realpath(APPROOT)); $sRelFinalTargetDir = substr($sFinalTargetDir, strlen(APPROOT)); $this->WriteStaticOnlyHtaccess($sTempTargetDir); $this->WriteStaticOnlyWebConfig($sTempTargetDir); static::SetUseSymbolicLinksFlag($bUseSymbolicLinks); foreach ($aModules as $foo => $oModule) { $sModuleName = $oModule->GetName(); $sModuleVersion = $oModule->GetVersion(); $sModuleRootDir = $oModule->GetRootDir(); if ($sModuleRootDir != '') { $sModuleRootDir = realpath($sModuleRootDir); $sRelativeDir = basename($sModuleRootDir); if ($bUseSymbolicLinks) { $sRealRelativeDir = substr($sModuleRootDir, $iStart); } else { $sRealRelativeDir = $sRelFinalTargetDir.'/'.$sRelativeDir; } // Push the other module files SetupUtils::copydir($sModuleRootDir, $sTempTargetDir.'/'.$sRelativeDir, $bUseSymbolicLinks); } else { $sRelativeDir = $sModuleName; $sRealRelativeDir = $sModuleName; } $aModulesInfo[$sModuleName] = array('root_dir' => $sRealRelativeDir, 'version' => $sModuleVersion); $sCompiledCode = ''; $oConstants = $this->oFactory->ListConstants($sModuleName); if ($oConstants->length > 0) { foreach($oConstants as $oConstant) { $sCompiledCode .= $this->CompileConstant($oConstant)."\n"; } } $oEvents = $this->oFactory->ListEvents($sModuleName); if ($oEvents->length > 0) { foreach($oEvents as $oEvent) { $sCompiledCode .= $this->CompileEvent($oEvent, $sModuleName)."\n"; } } if (array_key_exists($sModuleName, $this->aSnippets)) { foreach( $this->aSnippets[$sModuleName]['before'] as $aSnippet) { $sCompiledCode .= "\n"; $sCompiledCode .= "/**\n"; $sCompiledCode .= " * Snippet: {$aSnippet['snippet_id']}\n"; $sCompiledCode .= " */\n"; $sCompiledCode .= $aSnippet['content']."\n"; } } $oClasses = $this->oFactory->ListClasses($sModuleName); $iClassCount = $oClasses->length; if ($iClassCount == 0) { $this->Log("Found module without classes declared: $sModuleName"); } else { /** @var \MFElement $oClass */ foreach($oClasses as $oClass) { $sClass = $oClass->getAttribute("id"); $aAllClasses[] = $sClass; try { $sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir); } catch (DOMFormatException $e) { $sMessage = "Failed to process class '$sClass', "; if (!empty($sModuleRootDir)) { $sMessage .= "from '$sModuleRootDir': "; } $sMessage .= $e->getMessage(); throw new Exception($sMessage); } } } if (!array_key_exists($sModuleName, $aMenusByModule)) { $this->Log("Found module without menus declared: $sModuleName"); } else { $sMenuCreationClass = 'MenuCreation_'.preg_replace('/[^A-Za-z0-9_]/', '_', $sModuleName); $sCompiledCode .= <<GetChildText('parent', null)) { array_unshift($aParentIdHierarchy, $sParent); $oMenuNode = $aMenuNodes[$sParent]; } $aMenusToLoad = array_merge($aMenusToLoad, $aParentIdHierarchy); $aParentMenus = array_merge($aParentMenus, $aParentIdHierarchy); // Note: the order matters: the parents must be defined BEFORE $aMenusToLoad[] = $sMenuId; } $aMenusToLoad = array_unique($aMenusToLoad); $aMenuLinesForAll = array(); $aMenuLinesForAdmins = array(); $aAdminMenus = array(); foreach($aMenusToLoad as $sMenuId) { $oMenuNode = $aMenuNodes[$sMenuId]; if (is_null($oMenuNode)) { throw new Exception("Module '{$oModule->GetId()}' (location : '$sModuleRootDir') contains an unknown menuId : '$sMenuId'"); } if ($oMenuNode->getAttribute("xsi:type") == 'MenuGroup') { // Note: this algorithm is wrong // 1 - the module may appear empty in the current module, while children are defined in other modules // 2 - check recursively that child nodes are not empty themselves // Future algorithm: // a- browse the modules and build the menu tree // b- browse the tree and blacklist empty menus // c- before compiling, discard if blacklisted if (!in_array($oMenuNode->getAttribute("id"), $aParentMenus)) { // Discard empty menu groups continue; } } try { /** @var iTopWebPage $oP */ $aMenuLines = $this->CompileMenu($oMenuNode, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir, $oP); } catch (DOMFormatException $e) { throw new Exception("Failed to process menu '$sMenuId', from '$sModuleRootDir': ".$e->getMessage()); } $sParent = $oMenuNode->GetChildText('parent', null); if (($oMenuNode->GetChildText('enable_admin_only') == '1') || isset($aAdminMenus[$sParent])) { $aMenuLinesForAdmins = array_merge($aMenuLinesForAdmins, $aMenuLines); $aAdminMenus[$oMenuNode->getAttribute("id")] = true; } else { $aMenuLinesForAll = array_merge($aMenuLinesForAll, $aMenuLines); } } $sIndent = "\t\t"; foreach ($aMenuLinesForAll as $sPHPLine) { $sCompiledCode .= $sIndent.$sPHPLine."\n"; } if (count($aMenuLinesForAdmins) > 0) { $sCompiledCode .= $sIndent."if (UserRights::IsAdministrator())\n"; $sCompiledCode .= $sIndent."{\n"; foreach ($aMenuLinesForAdmins as $sPHPLine) { $sCompiledCode .= $sIndent."\t".$sPHPLine."\n"; } $sCompiledCode .= $sIndent."}\n"; } $sCompiledCode .= <<CompileUserRights($oUserRightsNode); } if (array_key_exists($sModuleName, $this->aSnippets)) { foreach( $this->aSnippets[$sModuleName]['after'] as $aSnippet) { $sCompiledCode .= "\n"; $sCompiledCode .= "/**\n"; $sCompiledCode .= " * Snippet: {$aSnippet['snippet_id']}\n"; $sCompiledCode .= " */\n"; $sCompiledCode .= $aSnippet['content']."\n"; } } // Create (overwrite if existing) the compiled file // if (strlen($sCompiledCode) > 0) { // We have compiled something: write the code somewhere // if (strlen($sModuleRootDir) > 0) { // Write the code into the given module as model..php // $sModelFileName = 'model.'.$sModuleName.'.php'; $sResultFile = $sTempTargetDir.'/'.$sRelativeDir.'/'.$sModelFileName; $this->WritePHPFile($sResultFile, $sModuleName, $sModuleVersion, $sCompiledCode); // In case the model file wasn't present in the module file, we're adding it ! (N°4875) $oModule->AddFileToInclude('business', $sModelFileName); } else { // Write the code into core/main.php // $this->sMainPHPCode .= <<sMainPHPCode .= $sCompiledCode; } } else { $this->Log("Compilation of module $sModuleName in version $sModuleVersion produced not code at all. No file written."); } // files to include (PHP datamodels) foreach($oModule->GetFilesToInclude('business') as $sRelFileName) { if (file_exists("{$sTempTargetDir}/{$sRelativeDir}/{$sRelFileName}")) { $aDataModelFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');"; } else { /** @noinspection NestedPositiveIfStatementsInspection */ if (utils::IsDevelopmentEnvironment()) { $sMissingBusinessFileMessage = 'A module embeds a non existing file: Check the module.php "datamodel" key!'; $aContext = [ 'moduleId' => $oModule->GetId(), 'moduleLocation' => $oModule->GetRootDir(), 'includedFile' => $sRelFileName, ]; SetupLog::Error($sMissingBusinessFileMessage, null, $aContext); throw new CoreException($sMissingBusinessFileMessage, $aContext); } } } // files to include (PHP webservices providers) foreach ($oModule->GetFilesToInclude('webservices') as $sRelFileName) { $aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');"; } } // foreach module // Register custom zlists if (is_array($this->aCustomListsCodes)) { $this->sMainPHPCode .= <<aCustomListsCodes as $sCustomListCode) { // Note: HEREDOC used to ease finding of \MetaModel::RegisterZList() method usages $this->sMainPHPCode .= << 'Custom zlist $sCustomListCode', 'type' => 'attributes']); PHP; } $this->sMainPHPCode .= "\n"; } // Compile the dictionaries -out of the modules // $sDictDir = $sTempTargetDir.'/dictionaries'; if (!is_dir($sDictDir)) { $this->Log("Creating directory $sDictDir"); mkdir($sDictDir, 0777, true); } $oDictionaries = $this->oFactory->GetNodes('dictionaries/dictionary'); $this->CompileDictionaries($oDictionaries, $sTempTargetDir, $sFinalTargetDir); // Compile the branding // /** @var \MFElement $oBrandingNode */ $oBrandingNode = $this->oFactory->GetNodes('branding')->item(0); $this->CompileBranding($oBrandingNode, $sTempTargetDir, $sFinalTargetDir); if (array_key_exists('_core_', $this->aSnippets)) { foreach( $this->aSnippets['_core_']['before'] as $aSnippet) { $this->sMainPHPCode .= "\n"; $this->sMainPHPCode .= "/**\n"; $this->sMainPHPCode .= " * Snippet: {$aSnippet['snippet_id']}\n"; $this->sMainPHPCode .= " */\n"; $this->sMainPHPCode .= $aSnippet['content']."\n"; } } // Compile the portals /** @var \MFElement $oPortalsNode */ $oPortalsNode = $this->oFactory->GetNodes('/itop_design/portals')->item(0); $this->CompilePortals($oPortalsNode, $sTempTargetDir, $sFinalTargetDir); // Create module design XML files $oModuleDesignsNode = $this->oFactory->GetNodes('/itop_design/module_designs')->item(0); $this->CompileModuleDesigns($oModuleDesignsNode, $sTempTargetDir, $sFinalTargetDir); // Compile the XML parameters /** @var \MFElement $oParametersNode */ $oParametersNode = $this->oFactory->GetNodes('/itop_design/module_parameters')->item(0); $this->CompileParameters($oParametersNode, $sTempTargetDir, $sFinalTargetDir); if (array_key_exists('_core_', $this->aSnippets)) { foreach( $this->aSnippets['_core_']['after'] as $aSnippet) { $this->sMainPHPCode .= "\n"; $this->sMainPHPCode .= "/**\n"; $this->sMainPHPCode .= " * Snippet: {$aSnippet['snippet_id']}\n"; $this->sMainPHPCode .= " */\n"; $this->sMainPHPCode .= $aSnippet['content']."\n"; } } if (count($this->aRelations) > 0) { $this->sMainPHPCode .= "\n"; $this->sMainPHPCode .= "/**\n"; $this->sMainPHPCode .= " * Relations\n"; $this->sMainPHPCode .= " */\n"; foreach($this->aRelations as $sRelationCode => $aData) { $sRelCodeSafe = addslashes($sRelationCode); $this->sMainPHPCode .= "MetaModel::RegisterRelation('$sRelCodeSafe');\n"; } } // Write core/main.php SetupUtils::builddir($sTempTargetDir.'/core'); $sPHPFile = $sTempTargetDir.'/core/main.php'; file_put_contents($sPHPFile, $this->sMainPHPCode); $sCurrDate = date(DATE_ISO8601); // Autoload $sPHPFile = $sTempTargetDir.'/autoload.php'; $sPHPFileContent = << $value) { if (is_null($value)) { $aTransformed[] = $key; } else { if (is_array($value)) { $this->ArrayOfItemsToZList($value); } $aTransformed[$key] = $value; } } $aItems = $aTransformed; } /** * Helper to format the flags for an attribute, in a given state * @param object $oAttNode DOM node containing the information to build the flags * Returns string PHP flags, based on the OPT_ATT_ constants, or empty (meaning 0, can be omitted) */ protected function FlagsToPHP($oAttNode) { static $aNodeAttributeToFlag = array( 'mandatory' => 'OPT_ATT_MANDATORY', 'read_only' => 'OPT_ATT_READONLY', 'must_prompt' => 'OPT_ATT_MUSTPROMPT', 'must_change' => 'OPT_ATT_MUSTCHANGE', 'hidden' => 'OPT_ATT_HIDDEN', ); $aFlags = array(); foreach ($aNodeAttributeToFlag as $sNodeAttribute => $sFlag) { $bFlag = ($oAttNode->GetOptionalElement($sNodeAttribute) != null); if ($bFlag) { $aFlags[] = $sFlag; } } if (empty($aFlags)) { $aFlags[] = 'OPT_ATT_NORMAL'; // When no flag is defined, reset the state to "normal" } $sRes = implode(' | ', $aFlags); return $sRes; } /** * Helper to format the tracking level for linkset (direct or indirect attributes) * * @param string $sTrackingLevel Value set from within the XML * Returns string PHP flag * * @throws \DOMFormatException */ protected function TrackingLevelToPHP($sAttType, $sTrackingLevel) { static $aXmlToPHP_Links = array( 'none' => 'LINKSET_TRACKING_NONE', 'list' => 'LINKSET_TRACKING_LIST', 'details' => 'LINKSET_TRACKING_DETAILS', 'all' => 'LINKSET_TRACKING_ALL', ); static $aXmlToPHP_Others = array( 'none' => 'ATTRIBUTE_TRACKING_NONE', 'all' => 'ATTRIBUTE_TRACKING_ALL', ); switch ($sAttType) { case 'AttributeLinkedSetIndirect': case 'AttributeLinkedSet': $aXmlToPHP = $aXmlToPHP_Links; break; default: $aXmlToPHP = $aXmlToPHP_Others; } if (!array_key_exists($sTrackingLevel, $aXmlToPHP)) { throw new DOMFormatException("Tracking level: unknown value '$sTrackingLevel', expecting a value in {".implode(', ', array_keys($aXmlToPHP))."}"); } return $aXmlToPHP[$sTrackingLevel]; } /** * Helper to format the edit-mode for direct linkset * * @param string $sEditMode Value set from within the XML * Returns string PHP flag * * @throws \DOMFormatException */ protected function EditModeToPHP($sEditMode) { static $aXmlToPHP = array( 'none' => 'LINKSET_EDITMODE_NONE', 'add_only' => 'LINKSET_EDITMODE_ADDONLY', 'actions' => 'LINKSET_EDITMODE_ACTIONS', 'in_place' => 'LINKSET_EDITMODE_INPLACE', 'add_remove' => 'LINKSET_EDITMODE_ADDREMOVE', ); if (!array_key_exists($sEditMode, $aXmlToPHP)) { throw new DOMFormatException("Edit mode: unknown value '$sEditMode'"); } return $aXmlToPHP[$sEditMode]; } /** * Helper to format the edit-when for direct linkset * * @param string $sEditWhen Value set from within the XML * @return string PHP flag * * @throws \DOMFormatException */ protected function EditWhenToPHP($sEditWhen): string { static $aXmlToPHP = array( 'never' => 'LINKSET_EDITWHEN_NEVER', 'on_host_edition' => 'LINKSET_EDITWHEN_ON_HOST_EDITION', 'on_host_display' => 'LINKSET_EDITWHEN_ON_HOST_DISPLAY', 'always' => 'LINKSET_EDITWHEN_ALWAYS', ); if (!array_key_exists($sEditWhen, $aXmlToPHP)) { throw new DOMFormatException("Edit mode: unknown value '$sEditWhen'"); } return $aXmlToPHP[$sEditWhen]; } /** * Format a path (file or url) as an absolute path or relative to the module or the app */ protected function PathToPHP($sPath, $sModuleRelativeDir, $bIsUrl = false) { if ($sPath == '') { $sPHP = "''"; } elseif (substr($sPath, 0, 2) == '$$') { // Absolute $sPHP = self::QuoteForPHP(substr($sPath, 2)); } elseif (substr($sPath, 0, 1) == '$') { // Relative to the application if ($bIsUrl) { $sPHP = "utils::GetAbsoluteUrlAppRoot().".self::QuoteForPHP(substr($sPath, 1)); } else { $sPHP = "APPROOT.".self::QuoteForPHP(substr($sPath, 1)); } } else { // Relative to the module if ($bIsUrl) { $sPHP = "utils::GetAbsoluteUrlModulePage('$sModuleRelativeDir', ".self::QuoteForPHP($sPath).")"; } else { $sPHP = "__DIR__.'/$sPath'"; } } return $sPHP; } /** * @param $oNode * @param string $sTag * @param string|null $sDefault * @param bool $bAddQuotes If true, surrounds property value with single quotes and escapes existing single quotes * * @return string|null * * @since 3.0.0 Add param. $bAddQuotes to be equivalent to {@see self::GetMandatoryPropString} and allow retrieving property without surrounding single quotes */ protected function GetPropString($oNode, string $sTag, string $sDefault = null, bool $bAddQuotes = true) { $val = $oNode->GetChildText($sTag); if (is_null($val)) { if (is_null($sDefault)) { return null; } else { $val = $sDefault; } } if ($bAddQuotes) { $val = "'".str_replace("'", "\\'", $val)."'"; } return $val; } /** * @param $oNode * @param string $sTag * @param bool $bAddQuotes * * @return string * @throws \DOMFormatException */ protected function GetMandatoryPropString($oNode, string $sTag, bool $bAddQuotes = true) { $val = $oNode->GetChildText($sTag); if (!is_null($val) && ($val !== '')) { if ($bAddQuotes) { return "'".$val."'"; } else { return $val; } } else { throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'"); } } /** * @param $oNode * @param $sTag * @param bool|null $bDefault * * @return bool|null */ private function GetPropBooleanConverted($oNode, $sTag, $bDefault = null) { $sValue = $this->GetPropBoolean($oNode, $sTag, $bDefault); if ($sValue == null) { return null; } if ($sValue == 'true') { return true; } return false; } /** * @param $oNode * @param $sTag * @param bool|null $bDefault * * @return null|string * @see GetPropBooleanConverted() to get boolean value */ protected function GetPropBoolean($oNode, $sTag, $bDefault = null) { $val = $oNode->GetChildText($sTag); if (is_null($val)) { if (is_null($bDefault)) { return null; } else { return $bDefault ? 'true' : 'false'; } } return (($val == 'true') || ($val == '1')) ? 'true' : 'false'; } /** * @param $oNode * @param $sTag * * @return string * @throws \DOMFormatException */ protected function GetMandatoryPropBoolean($oNode, $sTag) { $val = $oNode->GetChildText($sTag); if (is_null($val)) { throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'"); } return $val == 'true' ? 'true' : 'false'; } protected function GetPropNumber($oNode, $sTag, $nDefault = null) { $val = $oNode->GetChildText($sTag); if (is_null($val)) { if (is_null($nDefault)) { return null; } else { $val = $nDefault; } } return (string)$val; } /** * @param $oNode * @param $sTag * * @return string * @throws \DOMFormatException */ protected function GetMandatoryPropNumber($oNode, $sTag) { $val = $oNode->GetChildText($sTag); if (is_null($val)) { throw new DOMFormatException("missing (or empty) mandatory tag '$sTag' under the tag '".$oNode->nodeName."'"); } return (string)$val; } /** * Adds quotes and escape characters */ protected function QuoteForPHP($sStr, $bSimpleQuotes = false) { $sStr = $sStr ?? ''; if ($bSimpleQuotes) { $sEscaped = str_replace(array('\\', "'"), array('\\\\', "\\'"), $sStr); $sRet = "'$sEscaped'"; } else { $sEscaped = str_replace(array('\\', '"', "\n"), array('\\\\', '\\"', '\\n'), $sStr); $sRet = '"'.$sEscaped.'"'; } return $sRet; } protected function CompileEvent(DesignElement $oEvent, string $sModuleName) { $sName = $oEvent->getAttribute('id'); $aEventDescription = DesignElement::ToArray($oEvent); // array ( // 'description' => 'An object insert in the database has been requested. All changes to the object will be persisted automatically.', // 'sources' => // array ( // 'cmdbAbstractObject' => 'cmdbAbstractObject', // ), // 'replaces' => 'DBObject::OnInsert', // 'event_data' => // array ( // 'object' => // array ( // 'description' => 'The object inserted', // 'type' => 'DBObject', // ), // 'debug_info' => // array ( // 'description' => 'Debug string', // 'type' => 'string', // ), // ), // ) $sConstant = $sName; $sOutput = "const $sConstant = '$sName';\n"; $sOutput .= "\Combodo\iTop\Service\Events\EventService::RegisterEvent(\n"; $sOutput .= " new \Combodo\iTop\Service\Events\Description\EventDescription(\n"; //$sEventName $sOutput .= " '$sName',\n"; //$mEventSources $sOutput .= " [\n"; if (isset($aEventDescription['sources'])) { foreach ($aEventDescription['sources'] as $sSourceId => $sSourceName) { $sOutput .= " '$sSourceId' => '$sSourceName',\n"; } } $sOutput .= " ],\n"; // $sDescription $sOutput .= " '{$aEventDescription['description']}',\n"; // $sReplaces if (isset($aEventDescription['replaces'])) { $sOutput .= " '{$aEventDescription['replaces']}',\n"; } else { $sOutput .= " '',\n"; } // $aEventDataDescription $sOutput .= " [\n"; if (isset($aEventDescription['event_data'])) { foreach ($aEventDescription['event_data'] as $sEventDataName => $aEventDataDescription) { $sEventDataDesc = $aEventDataDescription['description']; $sEventDataType = $aEventDataDescription['type']; $sOutput .= " new \Combodo\iTop\Service\Events\Description\EventDataDescription(\n"; $sOutput .= " '$sEventDataName',\n"; $sOutput .= " '$sEventDataDesc',\n"; $sOutput .= " '$sEventDataType',\n"; $sOutput .= " ),\n"; } } $sOutput .= " ],\n"; // $sModule $sOutput .= " '$sModuleName'\n"; $sOutput .= " )\n"; $sOutput .= ");\n"; return $sOutput; } protected function CompileConstant($oConstant) { $sName = $oConstant->getAttribute('id'); $sType = $oConstant->getAttribute('xsi:type'); $sText = $oConstant->GetText(null); switch ($sType) { case 'integer': if (is_null($sText)) { // No data given => null $sScalar = 'null'; } else { $sScalar = (string)(int)$sText; } break; case 'float': if (is_null($sText)) { // No data given => null $sScalar = 'null'; } else { $sScalar = (string)(float)$sText; } break; case 'bool': if (is_null($sText)) { // No data given => null $sScalar = 'null'; } else { $sScalar = ($sText == 'true') ? 'true' : 'false'; } break; case 'string': default: $sScalar = $this->QuoteForPHP($sText, true); } $sPHPDefine = "define('$sName', $sScalar);"; return $sPHPDefine; } /** * @param \MFElement $oClass * @param string $sTempTargetDir * @param string $sFinalTargetDir * @param string $sModuleRelativeDir * * @return string * @throws \DOMFormatException */ protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir) { $sClass = $oClass->getAttribute('id'); $oProperties = $oClass->GetUniqueElement('properties'); $sPHP = ''; /* Contains dynamic CSS class definitions */ $sCss = ''; // Class characteristics // $aClassParams = array(); $aClassParams['category'] = $this->GetPropString($oProperties, 'category', ''); $aClassParams['key_type'] = "'autoincrement'"; if ((bool)$this->GetPropNumber($oProperties, 'is_link', 0)) { $aClassParams['is_link'] = 'true'; } // Naming $sComplementaryNameAttCode = ""; if ($oNaming = $oProperties->GetOptionalElement('naming')) { $oNameAttributes = $oNaming->GetUniqueElement('attributes'); /** @var \DOMNodeList $oAttributes */ $oAttributes = $oNameAttributes->getElementsByTagName('attribute'); $aNameAttCodes = array(); /** @var \MFElement $oAttribute */ foreach ($oAttributes as $oAttribute) { $aNameAttCodes[] = $oAttribute->getAttribute('id'); } if (count($aNameAttCodes) > 0) { // New style... $sNameAttCode = "array('".implode("', '", $aNameAttCodes)."')"; } else { $sNameAttCode = "''"; } if ($oComplementaryNameAttributes = $oNaming->GetOptionalElement('complementary_attributes')) { /** @var \DOMNodeList $oAttributes */ $oComplementaryAttributes = $oComplementaryNameAttributes->getElementsByTagName('attribute'); $aComplementaryNameAttCodes = array(); /** @var \MFElement $oAttribute */ foreach ($oComplementaryAttributes as $oComplementaryAttribute) { $aComplementaryNameAttCodes[] = $oComplementaryAttribute->getAttribute('id'); } if (count($aComplementaryNameAttCodes) > 0) { $sComplementaryNameAttCode = "array('".implode("', '", $aComplementaryNameAttCodes)."')"; } $aClassParams['complementary_name_attcode'] = $sComplementaryNameAttCode; } } else { $sNameAttCode = "''"; } $aClassParams['name_attcode'] = $sNameAttCode; // Semantic // - Default attributes code $sImageAttCode = ""; $sStateAttCode = ""; // - Parse optional fields semantic node $oFieldsSemantic = $oProperties->GetOptionalElement('fields_semantic'); if ($oFieldsSemantic) { // Image attribute $oImageAttribute = $oFieldsSemantic->GetOptionalElement('image_attribute'); if ($oImageAttribute) { $sImageAttCode = $oImageAttribute->GetText(); } // State attribute (for XML v1.7- the lifecycle/attribute node should have been migrated in this one) $oStateAttribute = $oFieldsSemantic->GetOptionalElement('state_attribute'); if ($oStateAttribute) { $sStateAttCode = $oStateAttribute->GetText(); } } $aClassParams['image_attcode'] = "'$sImageAttCode'"; $aClassParams['state_attcode'] = "'$sStateAttCode'"; // Reconcialiation if ($oReconciliation = $oProperties->GetOptionalElement('reconciliation')) { $oReconcAttributes = $oReconciliation->getElementsByTagName('attribute'); $aReconcAttCodes = array(); foreach ($oReconcAttributes as $oAttribute) { $aReconcAttCodes[] = $oAttribute->getAttribute('id'); } if (empty($aReconcAttCodes)) { $sReconcKeys = "array()"; } else { $sReconcKeys = "array('".implode("', '", $aReconcAttCodes)."')"; } } else { $sReconcKeys = "array()"; } $aClassParams['reconc_keys'] = $sReconcKeys; $aClassParams['db_table'] = $this->GetPropString($oProperties, 'db_table', ''); $aClassParams['db_key_field'] = $this->GetPropString($oProperties, 'db_key_field', 'id'); if (array_key_exists($sClass, $this->aRootClasses)) { $sDefaultFinalClass = 'finalclass'; } else { $sDefaultFinalClass = ''; } $aClassParams['db_finalclass_field'] = $this->GetPropString($oProperties, 'db_final_class_field', $sDefaultFinalClass); $this->CompileFiles($oProperties, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, ''); // Style if ($oStyle = $oProperties->GetOptionalElement('style')) { $aClassStyleData = $this->GenerateStyleDataFromNode($oStyle, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_CLASS, $sClass); $aClassParams['style'] = $aClassStyleData['orm_style_instantiation']; $sCss .= $aClassStyleData['scss']; } $oOrder = $oProperties->GetOptionalElement('order'); if ($oOrder) { $oColumnsNode = $oOrder->GetUniqueElement('columns'); $oColumns = $oColumnsNode->getElementsByTagName('column'); $aSortColumns = array(); foreach ($oColumns as $oColumn) { $aSortColumns[] = "'".$oColumn->getAttribute('id')."' => ".(($oColumn->getAttribute('ascending') == 'true') ? 'true' : 'false'); } if (count($aSortColumns) > 0) { $aClassParams['order_by_default'] = "array(".implode(", ", $aSortColumns).")"; } } if ($oIndexes = $oProperties->GetOptionalElement('indexes')) { $aIndexes = array(); foreach($oIndexes->getElementsByTagName('index') as $oIndex) { $sIndexId = $oIndex->getAttribute('id'); /** @var DesignElement $oAttributes */ $oAttributes = $oIndex->GetUniqueElement('attributes'); foreach ($oAttributes->getElementsByTagName('attribute') as $oAttribute) { $aIndexes[$sIndexId][] = $oAttribute->getAttribute('id'); } } $aClassParams['indexes'] = var_export($aIndexes, true); } $sEvents = ''; $sMethods = ''; $oHooks = $oClass->GetOptionalElement('event_listeners'); if ($oHooks) { foreach ($oHooks->getElementsByTagName('event_listener') as $oListener) { /** @var DesignElement $oListener */ $oEventNode = $oListener->GetUniqueElement('event'); /** @var DesignElement $oEventNode $oEventNode */ $sEventName = $oEventNode->GetText(); $sListenerId = $oListener->getAttribute('id'); $oCallback = $oListener->GetUniqueElement('callback', false); if (is_object($oCallback)) { $sCallback = $oCallback->GetText(); } else { $oCode = $oListener->GetUniqueElement('code'); $sCode = trim($oCode->GetText()); $sCallback = "EventHook_{$sEventName}_$sListenerId"; $sCallbackFct = preg_replace('@^function\s*\(@', "public function $sCallback(", $sCode); if ($sCode == $sCallbackFct) { throw new DOMFormatException("Malformed tag in class: $sClass hook: $sEventName listener: $sListenerId"); } $sMethods .= "\n $sCallbackFct\n\n"; } $sListenerRank = (float)($oListener->GetChildText('rank', '0')); $sEvents .= <<RegisterCRUDListener("$sEventName", '$sCallback', $sListenerRank, '$sModuleRelativeDir'); PHP; } } if (!empty($sEvents)) { $sMethods .= <<GetOptionalElement('archive')) { $bEnabled = $this->GetPropBoolean($oArchive, 'enabled', false); $aClassParams['archive'] = $bEnabled; } if ($oObsolescence = $oProperties->GetOptionalElement('obsolescence')) { $sCondition = trim($this->GetPropString($oObsolescence, 'condition', '')); if ($sCondition != "''") { $aClassParams['obsolescence_expression'] = $sCondition; } } if ($oUniquenessRules = $oProperties->GetOptionalElement('uniqueness_rules')) { $aUniquenessRules = array(); /** @var \MFElement $oUniquenessSingleRule */ foreach ($oUniquenessRules->GetElementsByTagName('rule') as $oUniquenessSingleRule) { $sCurrentRuleId = $oUniquenessSingleRule->getAttribute('id'); $oAttributes = $oUniquenessSingleRule->GetUniqueElement('attributes', false); if ($oAttributes) { $aUniquenessAttributes = array(); foreach ($oAttributes->getElementsByTagName('attribute') as $oAttribute) { $aUniquenessAttributes[] = $oAttribute->getAttribute('id'); } $aUniquenessRules[$sCurrentRuleId]['attributes'] = $aUniquenessAttributes; } else { $aUniquenessRules[$sCurrentRuleId]['attributes'] = null; } $aUniquenessRules[$sCurrentRuleId]['filter'] = $oUniquenessSingleRule->GetChildText('filter'); $aUniquenessRules[$sCurrentRuleId]['disabled'] = $this->GetPropBooleanConverted($oUniquenessSingleRule, 'disabled', null); $aUniquenessRules[$sCurrentRuleId]['is_blocking'] = $this->GetPropBooleanConverted($oUniquenessSingleRule, 'is_blocking', null); } // we will check for rules validity later as for now we don't have objects hierarchy (see \MetaModel::InitClasses) $aClassParams['uniqueness_rules'] = var_export($aUniquenessRules, true); } // Finalize class params declaration // $sClassParams = $this->GetAssociativeArrayAsPhpCode($aClassParams); // Comment on top of the class declaration // $sCodeComment = $oProperties->GetChildText('comment'); // Fields // $oFields = $oClass->GetOptionalElement('fields'); if ($oFields) { $this->CompileFiles($oFields, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, ''); } $sAttributes = ''; $aTagFieldsInfo = array(); /** @var \DOMElement $oField */ foreach($this->oFactory->ListFields($oClass) as $oField) { try { // $oField $sAttCode = $oField->getAttribute('id'); $sAttType = $oField->getAttribute('xsi:type'); $aParameters = $this->CompileAttribute($sAttType, $oField, $sModuleRelativeDir, $sClass, $sAttCode, $sCss, $aTagFieldsInfo, $sTempTargetDir); $aParams = array(); foreach($aParameters as $sKey => $sValue) { if (!is_null($sValue)) { $aParams[] = '"'.$sKey.'"=>'.$sValue; } } $sParams = implode(', ', $aParams); $sAttributes .= " MetaModel::Init_AddAttribute(new $sAttType(\"$sAttCode\", array($sParams)));\n"; } catch(Exception $e) { throw new DOMFormatException("Field: '$sAttCode', (type: $sAttType), ".$e->getMessage()); } } // Lifecycle // $sLifecycle = ''; $sHighlightScale = ''; $oLifecycle = $oClass->GetOptionalElement('lifecycle'); if ($oLifecycle) { $sLifecycle .= "\t\t// Lifecycle (status attribute: $sStateAttCode)\n"; $sLifecycle .= "\t\t//\n"; $oStimuli = $oLifecycle->GetUniqueElement('stimuli'); foreach ($oStimuli->getElementsByTagName('stimulus') as $oStimulus) { $sStimulus = $oStimulus->getAttribute('id'); $sStimulusClass = $oStimulus->getAttribute('xsi:type'); $sLifecycle .= " MetaModel::Init_DefineStimulus(new ".$sStimulusClass."(\"".$sStimulus."\", array()));\n"; } $oHighlightScale = $oLifecycle->GetUniqueElement('highlight_scale', false); if ($oHighlightScale) { $sHighlightScale = "\t\t// Higlight Scale\n"; $sHighlightScale .= " MetaModel::Init_DefineHighlightScale( array(\n"; $this->CompileFiles($oHighlightScale, $sTempTargetDir.'/'.$sModuleRelativeDir, $sFinalTargetDir.'/'.$sModuleRelativeDir, ''); foreach ($oHighlightScale->getElementsByTagName('item') as $oItem) { $sItemCode = $oItem->getAttribute('id'); $fRank = (float)$oItem->GetChildText('rank'); $sColor = $oItem->GetChildText('color'); if (($sIcon = $oItem->GetChildText('icon')) && (strlen($sIcon) > 0)) { $sIcon = $sModuleRelativeDir.'/'.$sIcon; $sIcon = "utils::GetAbsoluteUrlModulesRoot().'$sIcon'"; } else { $sIcon = "''"; } switch($sColor) { // Known PHP constants: keep the literal value as-is case 'HILIGHT_CLASS_CRITICAL': case 'HIGHLIGHT_CLASS_CRITICAL': $sColor = 'HILIGHT_CLASS_CRITICAL'; break; case 'HILIGHT_CLASS_OK': case 'HIGHLIGHT_CLASS_OK': $sColor = 'HILIGHT_CLASS_OK'; break; case 'HIGHLIGHT_CLASS_WARNING': case 'HILIGHT_CLASS_WARNING': $sColor = 'HILIGHT_CLASS_WARNING'; break; case 'HIGHLIGHT_CLASS_NONE': case 'HILIGHT_CLASS_NONE': $sColor = 'HILIGHT_CLASS_NONE'; break; default: // Future extension, specify your own color?? $sColor = "'".addslashes($sColor)."'"; } $sHighlightScale .= " '$sItemCode' => array('rank' => $fRank, 'color' => $sColor, 'icon' => $sIcon),\n"; } $sHighlightScale .= " ));\n"; } $oStates = $oLifecycle->GetUniqueElement('states'); $aStatesDependencies = array(); $aStates = array(); foreach ($oStates->getElementsByTagName('state') as $oState) { $aStatesDependencies[$oState->getAttribute('id')] = $oState->GetChildText('inherit_flags_from', ''); $aStates[$oState->getAttribute('id')] = $oState; } $aStatesOrder = array(); while (count($aStatesOrder) < count($aStatesDependencies)) { $iResolved = 0; foreach($aStatesDependencies as $sState => $sInheritFrom) { if (is_null($sInheritFrom)) { // Already recorded as resolved continue; } elseif ($sInheritFrom == '') { // Resolved $aStatesOrder[$sState] = $sInheritFrom; $aStatesDependencies[$sState] = null; $iResolved++; } elseif (isset($aStatesOrder[$sInheritFrom])) { // Resolved $aStatesOrder[$sState] = $sInheritFrom; $aStatesDependencies[$sState] = null; $iResolved++; } } if ($iResolved == 0) { // No change on this loop -> there are unmet dependencies $aRemainingDeps = array(); foreach($aStatesDependencies as $sState => $sParentState) { if (strlen($sParentState) > 0) { $aRemainingDeps[] = $sState.' ('.$sParentState.')'; } } throw new DOMFormatException("Could not solve inheritance for states: ".implode(', ', $aRemainingDeps)); } } foreach ($aStatesOrder as $sState => $foo) { $oState = $aStates[$sState]; $oInitialStatePath = $oState->GetOptionalElement('initial_state_path'); if ($oInitialStatePath) { $aInitialStatePath = array(); foreach ($oInitialStatePath->getElementsByTagName('state_ref') as $oIntermediateState) { $aInitialStatePath[] = "'".$oIntermediateState->GetText()."'"; } $sInitialStatePath = 'Array('.implode(', ', $aInitialStatePath).')'; } $sLifecycle .= " MetaModel::Init_DefineState(\n"; $sLifecycle .= " \"".$sState."\",\n"; $sLifecycle .= " array(\n"; $sAttributeInherit = $oState->GetChildText('inherit_flags_from', ''); $sLifecycle .= " \"attribute_inherit\" => '$sAttributeInherit',\n"; $oHighlight = $oState->GetUniqueElement('highlight', false); if ($oHighlight) { $sCode = $oHighlight->GetChildText('code', ''); if ($sCode != '') { $sLifecycle .= " 'highlight' => array('code' => '$sCode'),\n"; } } $sLifecycle .= " \"attribute_list\" => array(\n"; $oFlags = $oState->GetUniqueElement('flags'); foreach ($oFlags->getElementsByTagName('attribute') as $oAttributeNode) { $sFlags = $this->FlagsToPHP($oAttributeNode); if (strlen($sFlags) > 0) { $sAttCode = $oAttributeNode->GetAttribute('id'); $sLifecycle .= " '$sAttCode' => $sFlags,\n"; } } $sLifecycle .= " ),\n"; if (!is_null($oInitialStatePath)) { $sLifecycle .= " \"initial_state_path\" => $sInitialStatePath,\n"; } $sLifecycle .= " )\n"; $sLifecycle .= " );\n"; $oTransitions = $oState->GetUniqueElement('transitions'); foreach ($oTransitions->getElementsByTagName('transition') as $oTransition) { $sStimulus = $oTransition->getAttribute('id'); $sTargetState = $oTransition->GetChildText('target'); $oActions = $oTransition->GetUniqueElement('actions'); $aVerbs = array(); foreach ($oActions->getElementsByTagName('action') as $oAction) { $sVerb = $oAction->GetChildText('verb'); $oParams = $oAction->GetOptionalElement('params'); $aActionParams = array(); if ($oParams) { $oParamNodes = $oParams->getElementsByTagName('param'); foreach($oParamNodes as $oParam) { $sParamType = $oParam->getAttribute('xsi:type'); if ($sParamType == '') { $sParamType = 'string'; } $aActionParams[] = "array('type' => '$sParamType', 'value' => ".self::QuoteForPHP($oParam->textContent).")"; } } else { // Old (pre 2.1.0) format, when no parameter is specified, assume 1 parameter: reference sStimulusCode $aActionParams[] = "array('type' => 'reference', 'value' => 'sStimulusCode')"; } $sActionParams = 'array('.implode(', ', $aActionParams).')'; $aVerbs[] = "array('verb' => '$sVerb', 'params' => $sActionParams)"; } $sActions = implode(', ', $aVerbs); $sLifecycle .= " MetaModel::Init_DefineTransition(\"$sState\", \"$sStimulus\", array(\n"; $sLifecycle .= " \"target_state\"=>\"$sTargetState\",\n"; $sLifecycle .= " \"actions\"=>array($sActions),\n"; $sLifecycle .= " \"user_restriction\"=>null,\n"; $sLifecycle .= " \"attribute_list\"=>array(\n"; $oFlags = $oTransition->GetOptionalElement('flags'); if($oFlags !== null) { foreach ($oFlags->getElementsByTagName('attribute') as $oAttributeNode) { $sFlags = $this->FlagsToPHP($oAttributeNode); if (strlen($sFlags) > 0) { $sAttCode = $oAttributeNode->GetAttribute('id'); $sLifecycle .= " '$sAttCode' => $sFlags,\n"; } } } $sLifecycle .= " )\n"; $sLifecycle .= " ));\n"; } } } // No "real" lifecycle with stimuli and such but still a state attribute, we need to define states from the enum. values elseif ($oFieldsSemantic && $oStateAttribute) { $sLifecycle .= "\t\t// States but no lifecycle declared in XML (status attribute: $sStateAttCode)\n"; $sLifecycle .= "\t\t//\n"; // Note: We can't use ModelFactory::GetField() as the current clas doesn't seem to be loaded yet. $oField = $this->oFactory->GetNodes('field[@id="'.$sStateAttCode.'"]', $oFields)->item(0); if ($oField == null) { // Search field in parent class $oField = $this->GetFieldInParentClasses($oClass, $sStateAttCode); if ($oField == null) { throw new DOMFormatException("Non existing attribute '$sStateAttCode'", null, null, $oStateAttribute); } } $oCodeNodes = $this->oFactory->GetNodes('values/value/code', $oField); foreach ($oCodeNodes as $oCode) { $sLifecycle .= " MetaModel::Init_DefineState(\n"; $sLifecycle .= " \"".$oCode->GetText()."\",\n"; $sLifecycle .= " array(\n"; $sLifecycle .= " \"attribute_inherit\" => '',\n"; $sLifecycle .= " \"attribute_list\" => array()\n"; $sLifecycle .= " )\n"; $sLifecycle .= " );\n"; } } // ZLists // $oPresentation = $oClass->GetUniqueElement('presentation'); $sZlists = ''; // - Standard zlists $aListRef = [ 'details' => 'details', 'summary' => 'summary', 'standard_search' => 'search', 'default_search' => 'default_search', 'list' => 'list', ]; foreach ($aListRef as $sListCode => $sListTag) { $oListNode = $oPresentation->GetOptionalElement($sListTag); if ($oListNode) { $sZlists .= $this->GeneratePhpCodeForZlist($sListCode, $oListNode); } } // - Custom zlists foreach ($oPresentation->GetNodes('custom_presentations/custom_presentation') as $oListNode) { $sListCode = $oListNode->getAttribute('id'); // Cannot have a custom zlist with an ID that is among the reserved ones {@see $aListRef} if (array_key_exists($sListCode, $aListRef)) { throw new DOMFormatException('Custom zlist "'.$sListCode.'" cannot be compiled as it is using a reserved identifier ('.implode(', ', array_keys($aListRef)).')'); } // Store custom zlist code for further registration if (false === in_array($sListCode, $this->aCustomListsCodes)) { $this->aCustomListsCodes[] = $sListCode; } $sZlists .= "\n".$this->GeneratePhpCodeForZlist($sListCode, $oListNode); } // Methods $oMethods = $oClass->GetUniqueElement('methods'); foreach ($oMethods->getElementsByTagName('method') as $oMethod) { $sMethodCode = $oMethod->GetChildText('code'); if ($sMethodComment = $oMethod->GetChildText('comment', null)) { $sMethods .= "\n\t$sMethodComment\n".$sMethodCode."\n"; } else { $sMethods .= "\n\n".$sMethodCode."\n"; } } // Relations // $oRelations = $oClass->GetOptionalElement('relations'); if ($oRelations) { $aRelations = array(); foreach($oRelations->getElementsByTagName('relation') as $oRelation) { $sRelationId = $oRelation->getAttribute('id'); $this->aRelations[$sRelationId] = array('id' => $sRelationId); $oNeighbours = $oRelation->GetUniqueElement('neighbours'); foreach($oNeighbours->getElementsByTagName('neighbour') as $oNeighbour) { $sNeighbourId = $oNeighbour->getAttribute('id'); $sDirection = $oNeighbour->GetChildText('direction', 'both'); $sAttribute = $oNeighbour->GetChildText('attribute'); $sQueryDown = $oNeighbour->GetChildText('query_down'); $sQueryUp = $oNeighbour->GetChildText('query_up'); if (($sQueryDown == '') && ($sAttribute == '')) { throw new DOMFormatException("Relation '$sRelationId/$sNeighbourId': either a query or an attribute must be specified"); } if (($sQueryDown != '') && ($sAttribute != '')) { throw new DOMFormatException("Relation '$sRelationId/$sNeighbourId': both a query and and attribute have been specified... which one should be used?"); } if ($sDirection == 'both') { if (($sAttribute == '') && ($sQueryUp == '')) { throw new DOMFormatException("Relation '$sRelationId/$sNeighbourId': missing the query_up specification"); } } elseif ($sDirection != 'down') { throw new DOMFormatException("Relation '$sRelationId/$sNeighbourId': unknown direction ($sDirection), expecting 'both' or 'down'"); } $aRelations[$sRelationId][$sNeighbourId] = array( '_legacy_' => false, 'sDirection' => $sDirection, 'sDefinedInClass' => $sClass, 'sNeighbour' => $sNeighbourId, 'sQueryDown' => $sQueryDown, 'sQueryUp' => $sQueryUp, 'sAttribute' => $sAttribute, ); } } $sMethods .= "\tpublic static function GetRelationQueriesEx(\$sRelCode)\n"; $sMethods .= "\t{\n"; $sMethods .= "\t\tswitch (\$sRelCode)\n"; $sMethods .= "\t\t{\n"; foreach ($aRelations as $sRelationId => $aRelationData) { $sMethods .= "\t\tcase '$sRelationId':\n"; $sMethods .= "\t\t\t\$aRels = array(\n"; foreach ($aRelationData as $sNeighbourId => $aData) { //$sData = str_replace("\n", "\n\t\t\t\t", var_export($aData, true)); $sData = var_export($aData, true); $sMethods .= "\t\t\t\t'$sNeighbourId' => $sData,\n"; } $sMethods .= "\t\t\t);\n"; $sMethods .= "\t\t\treturn array_merge(\$aRels, parent::GetRelationQueriesEx(\$sRelCode));\n\n"; } $sMethods .= "\t\tdefault:\n"; $sMethods .= "\t\t\treturn parent::GetRelationQueriesEx(\$sRelCode);\n"; $sMethods .= "\t\t}\n"; $sMethods .= "\t}\n"; } // Let's make the whole class declaration // $sClassName = $oClass->getAttribute('id'); $bIsAbstractClass = ($oProperties->GetChildText('abstract') == 'true'); $oPhpParent = $oClass->GetUniqueElement('php_parent', false); $aRequiredFiles = []; if ($oPhpParent) { $sParentClass = $oPhpParent->GetChildText('name', ''); if ($sParentClass == '') { throw new Exception("Failed to process class '".$oClass->getAttribute('id')."', missing required tag 'name' under 'php_parent'."); } $sIncludeFile = $oPhpParent->GetChildText('file', ''); if ($sIncludeFile != '') { $aRequiredFiles[] = $sIncludeFile; } } else { $sParentClass = $oClass->GetChildText('parent', 'DBObject'); } $sInitMethodCalls = <<GeneratePhpCodeForClass($sClassName, $sParentClass, $sClassParams, $sInitMethodCalls, $bIsAbstractClass, $sMethods, $aRequiredFiles, $sCodeComment); // N°931 generates TagFieldData classes for AttributeTag fields if (!empty($aTagFieldsInfo)) { $sTagClassParentClass = "TagSetFieldData"; $aTagClassParams = array ( 'category' => 'bizmodel', 'key_type' => 'autoincrement', 'name_attcode' => array('label'), 'state_attcode' => '', 'reconc_keys' => array('code'), 'db_table' => '', // no need to have a corresponding table : this class exists only for rights, no additional field 'db_key_field' => 'id', 'db_finalclass_field' => 'finalclass', ); $sTagInitMethodCalls = << 'code', 1 => 'label', )); EOF ; foreach ($aTagFieldsInfo as $sTagFieldName) { $sTagClassName = static::GetTagDataClassName($sClassName, $sTagFieldName); $sTagClassParams = var_export($aTagClassParams, true); $sPHP .= $this->GeneratePhpCodeForClass($sTagClassName, $sTagClassParentClass, $sTagClassParams, $sTagInitMethodCalls); } } if (strlen($sCss) > 0) { if (array_key_exists($sClass, $this->aClassesCSSRules) === false) { $this->aClassesCSSRules[$sClass] = ''; } $this->aClassesCSSRules[$sClass] .= $sCss; } return $sPHP; } /** * @param string $sAttType * @param \DOMElement $oField * @param string $sModuleRelativeDir * @param string $sClass * @param string $sAttCode * @param string $sCss * @param array $aTagFieldsInfo * @param string $sTempTargetDir * * @return array * @throws \DOMException * @throws \DOMFormatException * @since 3.1.0 N°6040 */ protected function CompileAttribute(string $sAttType, DOMElement $oField, string $sModuleRelativeDir, string $sClass, string $sAttCode, string &$sCss, array &$aTagFieldsInfo, string $sTempTargetDir): array { $aParameters = []; $aDependencies = array(); $oDependencies = $oField->GetOptionalElement('dependencies'); if (!is_null($oDependencies)) { $oDepNodes = $oDependencies->getElementsByTagName('attribute'); foreach($oDepNodes as $oDepAttribute) { $aDependencies[] = "'".$oDepAttribute->getAttribute('id')."'"; } } $sDependencies = 'array('.implode(', ', $aDependencies).')'; // Check dynamic attribute definition first if ($this->HasDynamicAttributeDefinition($sAttType)) { $this->CompileDynamicAttribute($sAttType, $oField, $aParameters, $sModuleRelativeDir, $sDependencies); } elseif ($sAttType == 'AttributeLinkedSetIndirect') { $this->CompileCommonProperty('linked_class', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('ext_key_to_me', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('ext_key_to_remote', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('count_min', $oField, $aParameters, $sModuleRelativeDir, 0); $this->CompileCommonProperty('count_max', $oField, $aParameters, $sModuleRelativeDir, 0); $this->CompileCommonProperty('duplicates', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeLinkedSet') { $this->CompileCommonProperty('linked_class', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('ext_key_to_me', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('count_min', $oField, $aParameters, $sModuleRelativeDir, 0); $this->CompileCommonProperty('count_max', $oField, $aParameters, $sModuleRelativeDir, 0); $this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('edit_mode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('edit_when', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('with_php_constraint', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('with_php_computation', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeExternalKey') { $this->CompileCommonProperty('target_class', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('on_target_delete', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('max_combo_length', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('min_autocomplete_chars', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('allow_target_creation', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir, 'select'); $this->CompileCommonProperty('create_temporary_object', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeObjectKey') { $this->CompileCommonProperty('class_attcode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeHierarchicalKey') { $this->CompileCommonProperty('filter', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('on_target_delete', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('max_combo_length', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('min_autocomplete_chars', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('allow_target_creation', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeExternalField') { $this->CompileCommonProperty('extkey_attcode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('target_attcode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeURL') { $this->CompileCommonProperty('target', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeEnum') { $this->CompileAttributeEnumValues($sModuleRelativeDir, $sClass, $sAttCode, $oField, $aParameters, $sCss); $this->CompileCommonProperty('display_style', $oField, $aParameters, $sModuleRelativeDir, 'list'); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeMetaEnum') { $this->CompileAttributeEnumValues($sModuleRelativeDir, $sClass, $sAttCode, $oField, $aParameters, $sCss); $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('mappings', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeBlob') { $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeImage') { $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('display_max_width', $oField, $aParameters, $sModuleRelativeDir, 128); $this->CompileCommonProperty('display_max_height', $oField, $aParameters, $sModuleRelativeDir, 128); $this->CompileCommonProperty('storage_max_width', $oField, $aParameters, $sModuleRelativeDir, 256); $this->CompileCommonProperty('storage_max_height', $oField, $aParameters, $sModuleRelativeDir, 256); $this->CompileCommonProperty('default_image', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeStopWatch') { $this->CompileCommonProperty('states', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('goal', $oField, $aParameters, $sModuleRelativeDir, 'DefaultMetricComputer'); $this->CompileCommonProperty('working_time', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('thresholds', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeSubItem') { $this->CompileCommonProperty('target_attcode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('item_code', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeRedundancySettings') { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('relation_code', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('from_class', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('neighbour_id', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('enabled', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('enabled_mode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('min_up', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('min_up_mode', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('min_up_type', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeCustomFields') { $this->CompileCommonProperty('handler_class', $oField, $aParameters, $sModuleRelativeDir); } elseif ($sAttType == 'AttributeTagSet') { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('max_items', $oField, $aParameters, $sModuleRelativeDir, 12); $this->CompileCommonProperty('tag_code_max_len', $oField, $aParameters, $sModuleRelativeDir, 20); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); if ($aParameters['tag_code_max_len'] > 255) { $aParameters['tag_code_max_len'] = 255; } $aTagFieldsInfo[] = $sAttCode; $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeClassAttCodeSet') { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('max_items', $oField, $aParameters, $sModuleRelativeDir, 12); $this->CompileCommonProperty('class_field', $oField, $aParameters, $sModuleRelativeDir); // List of AttributeDefinition Classes to filter class_field (empty means all) $this->CompileCommonProperty('attribute_definition_list', $oField, $aParameters, $sModuleRelativeDir, ''); // Exclusion list of AttributeDefinition Classes to filter class_field (empty means no exclusion) $this->CompileCommonProperty('attribute_definition_exclusion_list', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeEnumSet') { $oValues = $oField->GetUniqueElement('values'); $oValueNodes = $oValues->getElementsByTagName('value'); $aValues = []; foreach ($oValueNodes as $oValue) { // New in 3.0 the format of values changed $sCode = $this->GetMandatoryPropString($oValue, 'code', false); $aValues[] = $sCode; } $sValues = '"'.implode(',', $aValues).'"'; $aParameters['possible_values'] = "new ValueSetEnumPadded($sValues)"; $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('max_items', $oField, $aParameters, $sModuleRelativeDir, 12); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeQueryAttCodeSet') { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('max_items', $oField, $aParameters, $sModuleRelativeDir, 12); $this->CompileCommonProperty('query_field', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeClassState') { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('class_field', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } elseif ($sAttType == 'AttributeDashboard') { $this->CompileCommonProperty('is_user_editable', $oField, $aParameters, $sModuleRelativeDir, true); $aParameters['definition_file'] = $this->GetPropString($oField, 'definition_file'); if ($aParameters['definition_file'] == null) { $oDashboardDefinition = $oField->GetOptionalElement('definition'); if ($oDashboardDefinition == null) { throw(new DOMFormatException('Missing definition for Dashboard Attribute "'.$sAttCode.'" expecting either a tag "definition_file" or "definition".')); } $sFileName = strtolower($sClass).'__'.strtolower($sAttCode).'_dashboard.xml'; $oXMLDoc = new DOMDocument('1.0', 'UTF-8'); $oXMLDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS) $oXMLDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect $oRootNode = $oXMLDoc->createElement('dashboard'); // make sure that the document is not empty $oRootNode->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $oXMLDoc->appendChild($oRootNode); foreach ($oDashboardDefinition->childNodes as $oNode) { $oDefNode = $oXMLDoc->importNode($oNode, true); // layout, cells, etc Nodes and below $oRootNode->appendChild($oDefNode); } $sFileName = $sModuleRelativeDir.'/'.$sFileName; $oXMLDoc->save($sTempTargetDir.'/'.$sFileName); $aParameters['definition_file'] = "'".str_replace("'", "\\'", $sFileName)."'"; } } else if($sAttType == 'AttributeClass'){ $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['class_category'] = $this->GetPropString($oField, 'class_category', ''); $aParameters['more_values'] = $this->GetPropString($oField, 'more_values', ''); $aParameters['depends_on'] = $sDependencies; }else { $this->CompileCommonProperty('sql', $oField, $aParameters, $sModuleRelativeDir); $this->CompileCommonProperty('is_null_allowed', $oField, $aParameters, $sModuleRelativeDir, false); $this->CompileCommonProperty('default_value', $oField, $aParameters, $sModuleRelativeDir, ''); $this->CompileCommonProperty('allowed_values', $oField, $aParameters, $sModuleRelativeDir); $aParameters['depends_on'] = $sDependencies; } // Optional parameters (more for historical reasons) // Added if present... // $aParameters['validation_pattern'] = $this->GetPropString($oField, 'validation_pattern'); $aParameters['format'] = $this->GetPropString($oField, 'format'); $aParameters['width'] = $this->GetPropString($oField, 'width'); $aParameters['height'] = $this->GetPropString($oField, 'height'); $aParameters['digits'] = $this->GetPropNumber($oField, 'digits'); $aParameters['decimals'] = $this->GetPropNumber($oField, 'decimals'); $aParameters['always_load_in_tables'] = $this->GetPropBoolean($oField, 'always_load_in_tables', false); $sTrackingLevel = $oField->GetChildText('tracking_level'); if (!is_null($sTrackingLevel)) { $aParameters['tracking_level'] = $this->TrackingLevelToPHP($sAttType, $sTrackingLevel); } return $aParameters; } /** * @param string $sPropertyName * @param \DOMElement $oField * @param array $aParameters * @param string $sModuleRelativeDir * * @param mixed $default * * @return bool true if the property was found and compiled * @throws \DOMFormatException */ protected function CompileCommonProperty(string $sPropertyName, DOMElement $oField, array &$aParameters, string $sModuleRelativeDir, $default = null): bool { if ($this->HasDynamicPropertyDefinition($sPropertyName)) { $aProperty = $this->aDynamicPropertyDefinitions[$sPropertyName]; if (!is_null($default)) { $aProperty['default'] = $default; } $aParameters = $this->CompileDynamicProperty($sPropertyName, $aProperty, $oField, $aParameters); } else { /* Properties too specific to be defined in XML */ switch ($sPropertyName) { case 'allowed_values': $aParameters[$sPropertyName] = 'null'; break; case 'filter': if ($sOql = $oField->GetChildText('filter')) { $sEscapedOql = self::QuoteForPHP($sOql); $aParameters['allowed_values'] = "new ValueSetObjects($sEscapedOql)"; // or "new ValueSetObjects('SELECT xxxx')" } else { $aParameters['allowed_values'] = 'null'; } break; case 'edit_mode': $sEditMode = $oField->GetChildText('edit_mode'); if (!is_null($sEditMode)) { $aParameters['edit_mode'] = $this->EditModeToPHP($sEditMode); } break; case 'edit_when': $sEditWhen = $oField->GetChildText('edit_when'); if(!is_null($sEditWhen)){ $aParameters['edit_when'] = $this->EditWhenToPHP($sEditWhen); } break; case 'mappings': $oMappings = $oField->GetUniqueElement('mappings'); $oMappingNodes = $oMappings->getElementsByTagName('mapping'); $aMapping = array(); foreach ($oMappingNodes as $oMapping) { $sMappingId = $oMapping->getAttribute('id'); $sMappingAttCode = $oMapping->GetChildText('attcode'); $aMapping[$sMappingId]['attcode'] = $sMappingAttCode; $aMapping[$sMappingId]['values'] = array(); $oMetaValues = $oMapping->GetUniqueElement('metavalues'); foreach ($oMetaValues->getElementsByTagName('metavalue') as $oMetaValue) { $sMetaValue = $oMetaValue->getAttribute('id'); $oValues = $oMetaValue->GetUniqueElement('values'); foreach ($oValues->getElementsByTagName('value') as $oValue) { $sValue = $oValue->getAttribute('id'); $aMapping[$sMappingId]['values'][$sValue] = $sMetaValue; } } } $aParameters['mapping'] = var_export($aMapping, true); break; case 'default_image': if (($sDefault = $oField->GetChildText('default_image')) && (strlen($sDefault) > 0)) { $aParameters['default_image'] = "'$sModuleRelativeDir/$sDefault'"; } else { $aParameters['default_image'] = 'null'; } break; case 'states': $oStates = $oField->GetUniqueElement('states'); $oStateNodes = $oStates->getElementsByTagName('state'); $aStates = array(); foreach ($oStateNodes as $oState) { $aStates[] = '"'.$oState->GetAttribute('id').'"'; } $aParameters['states'] = 'array('.implode(', ', $aStates).')'; break; case 'thresholds': $oThresholds = $oField->GetUniqueElement('thresholds'); $oThresholdNodes = $oThresholds->getElementsByTagName('threshold'); $aThresholds = array(); foreach ($oThresholdNodes as $oThreshold) { $iPercent = (int)$oThreshold->getAttribute('id'); $oHighlight = $oThreshold->GetUniqueElement('highlight', false); $sHighlight = ''; if ($oHighlight) { $sCode = $oHighlight->GetChildText('code'); $sPersistent = $this->GetPropBoolean($oHighlight, 'persistent', false); $sHighlight = "'highlight' => array('code' => '$sCode', 'persistent' => $sPersistent), "; } $oActions = $oThreshold->GetUniqueElement('actions'); $oActionNodes = $oActions->getElementsByTagName('action'); $aActions = array(); foreach ($oActionNodes as $oAction) { $oParams = $oAction->GetOptionalElement('params'); $aActionParams = array(); if ($oParams) { $oParamNodes = $oParams->getElementsByTagName('param'); foreach ($oParamNodes as $oParam) { $sParamType = $oParam->getAttribute('xsi:type'); if ($sParamType == '') { $sParamType = 'string'; } $aActionParams[] = "array('type' => '$sParamType', 'value' => ".self::QuoteForPHP($oParam->textContent).')'; } } $sActionParams = 'array('.implode(', ', $aActionParams).')'; $sVerb = $this->GetPropString($oAction, 'verb'); $aActions[] = "array('verb' => $sVerb, 'params' => $sActionParams)"; } $sActions = 'array('.implode(', ', $aActions).')'; $aThresholds[] = $iPercent." => array('percent' => $iPercent, $sHighlight 'actions' => $sActions)"; } $aParameters['thresholds'] = 'array('.implode(', ', $aThresholds).')'; break; default: return false; } } return true; } /** * @param string $sPropertyName * @param array $aProperty * @param \DOMElement $oField * @param array $aParameters * * @return array * @throws \DOMFormatException * @since 3.1.0 N°6040 */ protected function CompileDynamicProperty(string $sPropertyName, array $aProperty, DOMElement $oField, array $aParameters): array { $sPHPParam = $aProperty['php_param'] ?? $sPropertyName; $bMandatory = $aProperty['mandatory'] ?? false; $sType = $aProperty['type'] ?? 'string'; $sDefault = $aProperty['default'] ?? null; switch ($sType) { case 'string': if ($bMandatory) { $aParameters[$sPHPParam] = $this->GetMandatoryPropString($oField, $sPropertyName); } else { $aParameters[$sPHPParam] = $this->GetPropString($oField, $sPropertyName, $sDefault); } break; case 'boolean': if ($bMandatory) { $aParameters[$sPHPParam] = $this->GetMandatoryPropBoolean($oField, $sPropertyName); } else { $aParameters[$sPHPParam] = $this->GetPropBoolean($oField, $sPropertyName, is_null($sDefault) ? null : $sDefault === 'true'); } break; case 'number': if ($bMandatory) { $aParameters[$sPHPParam] = $this->GetMandatoryPropNumber($oField, $sPropertyName); } else { $aParameters[$sPHPParam] = $this->GetPropNumber($oField, $sPropertyName, is_null($sDefault) ? null : (int)$sDefault); } break; case 'php': $sValue = $oField->GetChildText($sPropertyName); if ($bMandatory && is_null($sValue)) { throw new DOMFormatException("missing (or empty) mandatory tag '$sPropertyName' under the tag '".$oField->nodeName."'"); } $aParameters[$sPHPParam] = $sValue ?? 'null'; break; case 'oql': if ($sOql = $oField->GetChildText($sPropertyName)) { $sEscapedOql = self::QuoteForPHP($sOql); $aParameters[$sPHPParam] = "$sEscapedOql"; } else { $aParameters[$sPHPParam] = 'null'; } break; case 'null': $aParameters[$sPHPParam] = 'null'; break; } return $aParameters; } /** * @param string $sAttType * @param \DOMElement $oField * @param array $aParameters * @param string $sModuleRelativeDir * @param string $sDependencies * * @throws \DOMFormatException * @since 3.1.0 */ protected function CompileDynamicAttribute(string $sAttType, DOMElement $oField, array &$aParameters, string $sModuleRelativeDir, string $sDependencies): void { foreach ($this->GetPropertiesForDynamicAttributeDefinition($sAttType) as $sPropertyName => $aProperty) { if ($this->HasDynamicPropertyDefinition($sPropertyName)) { // Attribute can rewrite common properties definition $aProperty = array_merge($this->aDynamicPropertyDefinitions[$sPropertyName], $aProperty); $aParameters = $this->CompileDynamicProperty($sPropertyName, $aProperty, $oField, $aParameters); } else { if (!$this->CompileCommonProperty($sPropertyName, $oField, $aParameters, $sModuleRelativeDir)) { /* new property specific to that attribute */ $aParameters = $this->CompileDynamicProperty($sPropertyName, $aProperty, $oField, $aParameters); } } } $aParameters['depends_on'] = $sDependencies; } protected function CompileAttributeEnumValues(string $sModuleRelativeDir, string $sClass, string $sAttCode, DOMElement $oField, array &$aParameters, string &$sCss): void { $sSortType = $oField->GetChildText('sort_type'); if (utils::IsNullOrEmptyString($sSortType)) { $sSortType = static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_CODE; } $oValues = $oField->GetUniqueElement('values'); $oValueNodes = $oValues->getElementsByTagName('value'); $aValues = []; $aValuesWithRank = []; $aValuesWithoutRank = []; $aStyledValues = []; foreach ($oValueNodes as $oValue) { // Value's code $sCode = $this->GetMandatoryPropString($oValue, 'code', false); $sRankAsString = $this->GetPropNumber($oValue, 'rank'); // Consider value as ranked only if it is the desired sort type, this is to avoid issues if a node is left when sort type isn't "rank" if (utils::IsNotNullOrEmptyString($sRankAsString) && ($sSortType === static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK)){ $aValuesWithRank[$sCode] = (float) $sRankAsString; } else { $aValuesWithoutRank[$sCode] = true; } // Value's style $oStyleNode = $oValue->GetOptionalElement('style'); if ($oStyleNode) { $aEnumStyleData = $this->GenerateStyleDataFromNode($oStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode, $sCode); $aStyledValues[] = $aEnumStyleData['orm_style_instantiation']; $sCss .= $aEnumStyleData['scss']; } } // Order values $aSortedValues = []; $sLocalizedSortAsPHPParam = ''; switch ($sSortType) { case static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_RANK: // Sort ranked values then append unranked values sorted by their code asort($aValuesWithRank); ksort($aValuesWithoutRank); $aSortedValues = array_merge(array_keys($aValuesWithRank), array_keys($aValuesWithoutRank)); break; case static::ENUM_ATTRIBUTE_ENUM_SORT_TYPE_LABEL: // Sort by labels is delegated at runtime for one main reason: // Default language (fallback -eg. english- if no dict entry for the current language -eg. italian-) can change at anytime in the configuration file -eg. from english to french- // if that was to happen, users would not understand why they have labels from in english instead of french, which would cause support questions / investigations. $sLocalizedSortAsPHPParam = ', true'; default: // Sort values by their code ksort($aValuesWithoutRank); $aSortedValues = array_keys($aValuesWithoutRank); break; } $sValuesAsPHPParam = '"'.implode(',', $aSortedValues).'"'; $aParameters['allowed_values'] = "new ValueSetEnum($sValuesAsPHPParam $sLocalizedSortAsPHPParam)"; if (count($aStyledValues) > 0) { $sStyledValues = '['.implode(',', $aStyledValues).']'; $aParameters['styled_values'] = "$sStyledValues"; } // Default style for values $oDefaultStyleNode = $oField->GetOptionalElement('default_style'); if ($oDefaultStyleNode) { $aEnumStyleData = $this->GenerateStyleDataFromNode($oDefaultStyleNode, $sModuleRelativeDir, self::ENUM_STYLE_HOST_ELEMENT_TYPE_ENUM, $sClass, $sAttCode); $aParameters['default_style'] = $aEnumStyleData['orm_style_instantiation']; $sCss .= $aEnumStyleData['scss']; } } /** * @internal This method is public in order to be used in the tests * * @param \MFElement $oNode Style node, can be either a