diff --git a/core/attributedef/AttributeEnum.php b/core/attributedef/AttributeEnum.php new file mode 100644 index 0000000000..9a2add6728 --- /dev/null +++ b/core/attributedef/AttributeEnum.php @@ -0,0 +1,399 @@ +IsParam('styled_values')) { + $aStyles = $this->Get('styled_values'); + if (array_key_exists($sValue, $aStyles)) { + return $aStyles[$sValue]; + } + } + + if ($this->IsParam('default_style')) { + return $this->Get('default_style'); + } + + return null; + } + + protected function GetSQLCol($bFullSpec = false) + { + // Get the definition of the column, including the actual values present in the table + return $this->GetSQLColHelper($bFullSpec, true); + } + + /** + * A more versatile version of GetSQLCol + * @param bool $bFullSpec + * @param bool $bIncludeActualValues + * @param string $sSQLTableName The table where to look for the actual values (may be useful for data synchro tables) + * @return string + * @since 3.0.0 + */ + protected function GetSQLColHelper($bFullSpec = false, $bIncludeActualValues = false, $sSQLTableName = null) + { + $oValDef = $this->GetValuesDef(); + if ($oValDef) { + $aValues = CMDBSource::Quote(array_keys($oValDef->GetValues(array(), "")), true); + } else { + $aValues = array(); + } + + // Preserve the values already present in the database to ease migrations + if ($bIncludeActualValues) { + if ($sSQLTableName == null) { + // No SQL table given, use the one of the attribute + $sHostClass = $this->GetHostClass(); + $sSQLTableName = MetaModel::DBGetTable($sHostClass, $this->GetCode()); + } + $aValues = array_unique(array_merge($aValues, $this->GetActualValuesInDB($sSQLTableName))); + } + + if (count($aValues) > 0) { + // The syntax used here do matters + // In particular, I had to remove unnecessary spaces to + // make sure that this string will match the field type returned by the DB + // (used to perform a comparison between the current DB format and the data model) + return "ENUM(" . implode(",", $aValues) . ")" + . CMDBSource::GetSqlStringColumnDefinition() + . ($bFullSpec ? $this->GetSQLColSpec() : ''); + } else { + return "VARCHAR(255)" + . CMDBSource::GetSqlStringColumnDefinition() + . ($bFullSpec ? " DEFAULT ''" : ""); // ENUM() is not an allowed syntax! + } + } + + /** + * @since 3.0.0 + * {@inheritDoc} + * @see AttributeDefinition::GetImportColumns() + */ + public function GetImportColumns() + { + // Note: this is used by the Data Synchro to build the "data" table + // Right now the function is not passed the "target" SQL table, but if we improve this in the future + // we may call $this->GetSQLColHelper(true, true, $sDBTable); to take into account the actual 'enum' values + // in this table + return array($this->GetCode() => $this->GetSQLColHelper(false, false)); + } + + /** + * Get the list of the actual 'enum' values present in the database + * @return string[] + * @since 3.0.0 + */ + protected function GetActualValuesInDB(string $sDBTable) + { + $aValues = array(); + try { + $sSQL = "SELECT DISTINCT `" . $this->GetSQLExpr() . "` AS value FROM `$sDBTable`;"; + $aValuesInDB = CMDBSource::QueryToArray($sSQL); + foreach ($aValuesInDB as $aRow) { + if ($aRow['value'] !== null) { + $aValues[] = $aRow['value']; + } + } + } catch (MySQLException $e) { + // Never mind, maybe the table does not exist yet (new installation from scratch) + // It seems more efficient to try and ignore errors than to test if the table & column really exists + } + return CMDBSource::Quote($aValues); + } + + protected function GetSQLColSpec() + { + $default = $this->ScalarToSQL($this->GetDefaultValue()); + if (is_null($default)) { + $sRet = ''; + } else { + // ENUMs values are strings so the default value must be a string as well, + // otherwise MySQL interprets the number as the zero-based index of the value in the list (i.e. the nth value in the list) + $sRet = " DEFAULT " . CMDBSource::Quote($default); + } + + return $sRet; + } + + public function ScalarToSQL($value) + { + // Note: for strings, the null value is an empty string and it is recorded as such in the DB + // but that wasn't working for enums, because '' is NOT one of the allowed values + // that's why a null value must be forced to a real null + $value = parent::ScalarToSQL($value); + if ($this->IsNull($value)) { + return null; + } else { + return $value; + } + } + + public function RequiresIndex() + { + return false; + } + + public function GetBasicFilterOperators() + { + return parent::GetBasicFilterOperators(); + } + + public function GetBasicFilterLooseOperator() + { + return '='; + } + + public function GetBasicFilterSQLExpr($sOpCode, $value) + { + return parent::GetBasicFilterSQLExpr($sOpCode, $value); + } + + public function GetValueLabel($sValue) + { + if (is_null($sValue)) { + // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label + $sLabel = Dict::S('Class:' . $this->GetHostClass() . '/Attribute:' . $this->GetCode() . '/Value:' . $sValue, + Dict::S('Enum:Undefined')); + } else { + $sLabel = $this->SearchLabel('/Attribute:' . $this->m_sCode . '/Value:' . $sValue, null, true /*user lang*/); + if (is_null($sLabel)) { + $sDefault = str_replace('_', ' ', $sValue); + // Browse the hierarchy again, accepting default (english) translations + $sLabel = $this->SearchLabel('/Attribute:' . $this->m_sCode . '/Value:' . $sValue, $sDefault, false); + } + } + + return $sLabel; + } + + public function GetValueDescription($sValue) + { + if (is_null($sValue)) { + // Unless a specific label is defined for the null value of this enum, use a generic "undefined" label + $sDescription = Dict::S('Class:' . $this->GetHostClass() . '/Attribute:' . $this->GetCode() . '/Value:' . $sValue . '+', + Dict::S('Enum:Undefined')); + } else { + $sDescription = Dict::S('Class:' . $this->GetHostClass() . '/Attribute:' . $this->GetCode() . '/Value:' . $sValue . '+', + '', true /* user language only */); + if (strlen($sDescription) == 0) { + $sParentClass = MetaModel::GetParentClass($this->m_sHostClass); + if ($sParentClass) { + if (MetaModel::IsValidAttCode($sParentClass, $this->m_sCode)) { + $oAttDef = MetaModel::GetAttributeDef($sParentClass, $this->m_sCode); + $sDescription = $oAttDef->GetValueDescription($sValue); + } + } + } + } + + return $sDescription; + } + + public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + { + if ($bLocalize) { + $sLabel = $this->GetValueLabel($sValue); + // $sDescription = $this->GetValueDescription($sValue); + $oStyle = $this->GetStyle($sValue); + // later, we could imagine a detailed description in the title + // $sRes = "".parent::GetAsHtml($sLabel).""; + $oBadge = \Combodo\iTop\Application\UI\Base\Component\FieldBadge\FieldBadgeUIBlockFactory::MakeForField($sLabel, $oStyle); + $oRenderer = new \Combodo\iTop\Renderer\BlockRenderer($oBadge); + $sRes = $oRenderer->RenderHtml(); + } else { + $sRes = parent::GetAsHtml($sValue, $oHostObject, $bLocalize); + } + + return $sRes; + } + + public function GetAsXML($value, $oHostObject = null, $bLocalize = true) + { + if (is_null($value)) { + $sFinalValue = ''; + } elseif ($bLocalize) { + $sFinalValue = $this->GetValueLabel($value); + } else { + $sFinalValue = $value; + } + $sRes = parent::GetAsXML($sFinalValue, $oHostObject, $bLocalize); + + return $sRes; + } + + public function GetAsCSV( + $sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, + $bConvertToPlainText = false + ) + { + if (is_null($sValue)) { + $sFinalValue = ''; + } elseif ($bLocalize) { + $sFinalValue = $this->GetValueLabel($sValue); + } else { + $sFinalValue = $sValue; + } + $sRes = parent::GetAsCSV($sFinalValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); + + return $sRes; + } + + public static function GetFormFieldClass() + { + return '\\Combodo\\iTop\\Form\\Field\\SelectField'; + } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) { + // Later : We should check $this->Get('display_style') and create a Radio / Select / ... regarding its value + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + $oFormField->SetChoices($this->GetAllowedValues($oObject->ToArgsForQuery())); + parent::MakeFormField($oObject, $oFormField); + + return $oFormField; + } + + public function GetEditValue($sValue, $oHostObj = null) + { + if (is_null($sValue)) { + return ''; + } else { + return $this->GetValueLabel($sValue); + } + } + + public function GetForJSON($value) + { + return $value; + } + + public function GetAllowedValues($aArgs = array(), $sContains = '') + { + $aRawValues = parent::GetAllowedValues($aArgs, $sContains); + if (is_null($aRawValues)) { + return null; + } + $aLocalizedValues = array(); + foreach ($aRawValues as $sKey => $sValue) { + $aLocalizedValues[$sKey] = $this->GetValueLabel($sKey); + } + + // Sort by label only if necessary + // See N°1646 and {@see \MFCompiler::CompileAttributeEnumValues()} for complete information as for why sort on labels is done at runtime while other sorting are done at compile time + /** @var \ValueSetEnum $oValueSetDef */ + $oValueSetDef = $this->GetValuesDef(); + if ($oValueSetDef->IsSortedByValues()) { + asort($aLocalizedValues); + } + + return $aLocalizedValues; + } + + public function GetMaxSize() + { + return null; + } + + /** + * An enum can be localized + */ + public function MakeValueFromString( + $sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null, + $sAttributeQualifier = null + ) + { + if ($bLocalizedValue) { + // Lookup for the value matching the input + // + $sFoundValue = null; + $aRawValues = parent::GetAllowedValues(); + if (!is_null($aRawValues)) { + foreach ($aRawValues as $sKey => $sValue) { + $sRefValue = $this->GetValueLabel($sKey); + if ($sProposedValue == $sRefValue) { + $sFoundValue = $sKey; + break; + } + } + } + if (is_null($sFoundValue)) { + return null; + } + + return $this->MakeRealValue($sFoundValue, null); + } else { + return parent::MakeValueFromString($sProposedValue, $bLocalizedValue, $sSepItem, $sSepAttribute, $sSepValue, + $sAttributeQualifier); + } + } + + /** + * Processes the input value to align it with the values supported + * by this type of attribute. In this case: turns empty strings into nulls + * + * @param mixed $proposedValue The value to be set for the attribute + * + * @return mixed The actual value that will be set + */ + public function MakeRealValue($proposedValue, $oHostObj) + { + if ($proposedValue == '') { + return null; + } + + return parent::MakeRealValue($proposedValue, $oHostObj); + } + + public function GetOrderByHint() + { + $aValues = $this->GetAllowedValues(); + + return Dict::Format('UI:OrderByHint_Values', implode(', ', $aValues)); + } +} \ No newline at end of file