diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 96810b5b2..cfa71d118 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -487,6 +487,35 @@ abstract class AttributeDefinition { return $this->GetAsHTML($sValue, $oHostObject, $bLocalize); } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + if ($this->IsScalar()) + { + switch ($sVerb) + { + case '': + return $value; + + case 'html': + return $this->GetAsHtml($value, $oHostObject, $bLocalize); + + case 'label': + return $this->GetEditValue($value); + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObj)); + } + } + return null; + } public function GetAllowedValues($aArgs = array(), $sContains = '') { @@ -731,6 +760,46 @@ class AttributeLinkedSet extends AttributeDefinition return $sRes; } + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + $sRemoteName = $this->IsIndirect() ? $this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; + + $oLinkSet = clone $value; // Workaround/Safety net for Trac #887 + $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); + if ($iLimit > 0) + { + $oLinkSet->SetLimit($iLimit); + } + $aNames = $oLinkSet->GetColumnAsArray($sRemoteName); + if ($iLimit > 0) + { + $iTotal = $oLinkSet->Count(); + if ($iTotal > count($aNames)) + { + $aNames[] = '... '.Dict::Format('UI:TruncatedResults', count($aNames), $iTotal); + } + } + + switch($sVerb) + { + case '': + return implode("\n", $aNames); + + case 'html': + return ''; + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObj)); + } + } + public function DuplicatesAllowed() {return false;} // No duplicates for 1:n links, never public function GetImportColumns() @@ -2094,6 +2163,35 @@ class AttributeCaseLog extends AttributeLongText } } + + /** + * Get various representations of the value, for insertion into a template (e.g. in Notifications) + * @param $value mixed The current value of the field + * @param $sVerb string The verb specifying the representation of the value + * @param $oHostObject DBObject The object + * @param $bLocalize bool Whether or not to localize the value + */ + public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true) + { + switch($sVerb) + { + case '': + return $value->GetText(); + + case 'head': + return $value->GetLatestEntry(); + + case 'head_html': + return '
'.str_replace( array( "\r\n", "\n", "\r"), "
", htmlentities($value->GetLatestEntry(), ENT_QUOTES, 'UTF-8')).'
'; + + case 'html': + return $value->GetAsEmailHtml(); + + default: + throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObj)); + } + } + /** * Helper to get a value that will be JSON encoded * The operation is the opposite to FromJSONToValue diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 541034d28..034edacc3 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -89,8 +89,6 @@ abstract class DBObject implements iDisplay protected $m_aCheckIssues = null; protected $m_aDeleteIssues = null; - protected $m_aAsArgs = null; // The current object as a standard argument (cache) - private $m_bFullyLoaded = false; // Compound objects can be partially loaded private $m_aLoadedAtt = array(); // Compound objects can be partially loaded, array of sAttCode protected $m_aModifiedAtt = array(); // list of (potentially) modified sAttCodes @@ -413,7 +411,6 @@ abstract class DBObject implements iDisplay // The object has changed, reset caches $this->m_bCheckStatus = null; - $this->m_aAsArgs = null; // Make sure we do not reload it anymore... before saving it $this->RegisterAsDirty(); @@ -844,8 +841,6 @@ abstract class DBObject implements iDisplay throw new CoreException("Changing the key ({$this->m_iKey} to $iNewKey) on an object (class {".get_class($this).") wich already exists in the Database"); } $this->m_iKey = $iNewKey; - // Invalidate the argument cache - $this->m_aAsArgs = null; } /** * Get the icon representing this object @@ -1490,8 +1485,6 @@ abstract class DBObject implements iDisplay { // Take the autonumber $this->m_iKey = $iNewKey; - // Invalidate the argument cache - $this->m_aAsArgs = null; } return $this->m_iKey; } @@ -1574,9 +1567,6 @@ abstract class DBObject implements iDisplay $this->DBWriteLinks(); $this->m_bIsInDB = true; $this->m_bDirty = false; - - // Arg cache invalidated (in particular, it needs the object key -could be improved later) - $this->m_aAsArgs = null; $this->AfterInsert(); @@ -2274,87 +2264,94 @@ abstract class DBObject implements iDisplay - /* - * Create query parameters (SELECT ... WHERE service = :this->service_id) - * to be used with the APIs DBObjectSearch/DBObjectSet - * - * Starting 2.0.2 the parameters are computed on demand, at the lowest level, - * in VariableExpression::Render() - */ + /** + * Create query parameters (SELECT ... WHERE service = :this->service_id) + * to be used with the APIs DBObjectSearch/DBObjectSet + * + * Starting 2.0.2 the parameters are computed on demand, at the lowest level, + * in VariableExpression::Render() + */ public function ToArgsForQuery($sArgName = 'this') { return array($sArgName.'->object()' => $this); } - /* - * Create template placeholders - * An improvement could be to compute the values on demand - * (i.e. interpret the template to determine the placeholders) - */ + /** + * Create template placeholders: now equivalent to ToArgsForQuery since the actual + * template placeholders are computed on demand. + */ public function ToArgs($sArgName = 'this') { - if (is_null($this->m_aAsArgs)) - { - $this->m_aAsArgs = array(); - } - if (!array_key_exists($sArgName, $this->m_aAsArgs)) - { - $oKPI = new ExecutionKPI(); - $aScalarArgs = $this->ToArgsForQuery($sArgName); - $aScalarArgs[$sArgName] = $this->GetKey(); - $aScalarArgs[$sArgName.'->id'] = $this->GetKey(); - $aScalarArgs[$sArgName.'->hyperlink()'] = $this->GetHyperlink('iTopStandardURLMaker', false); - $aScalarArgs[$sArgName.'->hyperlink(portal)'] = $this->GetHyperlink('PortalURLMaker', false); - $aScalarArgs[$sArgName.'->name()'] = $this->GetName(); + return $this->ToArgsForQuery($sArgName); + } - $sClass = get_class($this); - foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) + public function GetForTemplate($sPlaceholderAttCode) + { + $ret = null; + if (($iPos = strpos($sPlaceholderAttCode, '->')) !== false) + { + $sExtKeyAttCode = substr($sPlaceholderAttCode, 0, $iPos); + $sRemoteAttCode = substr($sPlaceholderAttCode, $iPos + 2); + if (!MetaModel::IsValidAttCode(get_class($this), $sExtKeyAttCode)) { - if ($oAttDef instanceof AttributeCaseLog) - { - $oCaseLog = $this->Get($sAttCode); - $aScalarArgs[$sArgName.'->'.$sAttCode] = $oCaseLog->GetText(); - $sHead = $oCaseLog->GetLatestEntry(); - $aScalarArgs[$sArgName.'->head('.$sAttCode.')'] = $sHead; - $aScalarArgs[$sArgName.'->head_html('.$sAttCode.')'] = '
'.str_replace(array("\r\n", "\n", "\r"), "
", htmlentities($sHead, ENT_QUOTES, 'UTF-8')).'
'; - $aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = $oCaseLog->GetAsEmailHtml(); - } - elseif ($oAttDef->IsScalar()) - { - $aScalarArgs[$sArgName.'->'.$sAttCode] = $this->Get($sAttCode); - // #@# Note: This has been proven to be quite slow, this can slow down bulk load - $sAsHtml = $this->GetAsHtml($sAttCode); - $aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = $sAsHtml; - $aScalarArgs[$sArgName.'->label('.$sAttCode.')'] = $this->GetEditValue($sAttCode); // "Nice" display value, but without HTML tags and entities - } - elseif ($oAttDef->IsLinkSet()) - { - $sRemoteName = $oAttDef->IsIndirect() ? $oAttDef->GetExtKeyToRemote().'_friendlyname' : 'friendlyname'; - - $oLinkSet = clone $this->Get($sAttCode); // Workaround/Safety net for Trac #887 - $iLimit = MetaModel::GetConfig()->Get('max_linkset_output'); - if ($iLimit > 0) - { - $oLinkSet->SetLimit($iLimit); - } - $aNames = $oLinkSet->GetColumnAsArray($sRemoteName); - if ($iLimit > 0) - { - $iTotal = $oLinkSet->Count(); - if ($iTotal > count($aNames)) - { - $aNames[] = '... '.Dict::Format('UI:TruncatedResults', count($aNames), $iTotal); - } - } - $sNames = implode("\n", $aNames); - $aScalarArgs[$sArgName.'->'.$sAttCode] = $sNames; - $aScalarArgs[$sArgName.'->html('.$sAttCode.')'] = ''; - } + throw new CoreException("Unknown attribute '$sExtKeyAttCode' for the class ".get_class($this)); + } + + $oKeyAttDef = MetaModel::GetAttributeDef(get_class($this), $sExtKeyAttCode); + if (!$oKeyAttDef instanceof AttributeExternalKey) + { + throw new CoreException("'$sExtKeyAttCode' is not an external key of the class ".get_class($this)); + } + $sRemoteClass = $oKeyAttDef->GetTargetClass(); + $oRemoteObj = MetaModel::GetObject($sRemoteClass, $this->GetStrict($sExtKeyAttCode), false); + if (is_null($oRemoteObj)) + { + $ret = Dict::S('UI:UndefinedObject'); + } + else + { + // Recurse + $ret = $oRemoteObj->GetForTemplate($sRemoteAttCode); } - $this->m_aAsArgs[$sArgName] = $aScalarArgs; - $oKPI->ComputeStats('ToArgs', get_class($this)); } - return $this->m_aAsArgs[$sArgName]; + else + { + switch($sPlaceholderAttCode) + { + case 'id': + $ret = $this->GetKey(); + break; + + case 'hyperlink()': + $ret = $this->GetHyperlink('iTopStandardURLMaker', false); + break; + + case 'hyperlink(portal)': + $ret = $this->GetHyperlink('PortalURLMaker', false); + break; + + case 'name()': + $ret = $this->GetName(); + break; + + default: + if (preg_match('/^([^(]+)\\((.+)\\)$/', $sPlaceholderAttCode, $aMatches)) + { + $sVerb = $aMatches[1]; + $sAttCode = $aMatches[2]; + } + else + { + $sVerb = ''; + $sAttCode = $sPlaceholderAttCode; + } + + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode); + $ret = $oAttDef->GetForTemplate($this->Get($sAttCode), $sVerb, $this); + } + + } + return $ret; } // To be optionaly overloaded diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 1142170ca..919b08ddc 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -5420,7 +5420,7 @@ abstract class MetaModel /** * Replaces all the parameters by the values passed in the hash array */ - static public function ApplyParams($aInput, $aParams) + static public function ApplyParams($sInput, $aParams) { // Declare magic parameters $aParams['APP_URL'] = utils::GetAbsoluteUrlAppRoot(); @@ -5431,12 +5431,43 @@ abstract class MetaModel foreach($aParams as $sSearch => $replace) { // Some environment parameters are objects, we just need scalars - if (is_object($replace)) continue; + if (is_object($replace)) + { + $iPos = strpos($sSearch, '->object()'); + if ($iPos !== false) + { + // Expand the parameters for the object + $sName = substr($sSearch, 0, $iPos); + if (preg_match_all('/\\$'.$sName.'->([^\\$]+)\\$/', $sInput, $aMatches)) + { + foreach($aMatches[1] as $sPlaceholderAttCode) + { + try + { + $sReplacement = $replace->GetForTemplate($sPlaceholderAttCode); + if ($sReplacement !== null) + { + $aReplacements[] = $sReplacement; + $aSearches[] = '$'.$sName.'->'.$sPlaceholderAttCode.'$'; + } + } + catch(Exception $e) + { + // No replacement will occur + } + } + } + } + else + { + continue; // Ignore this non-scalar value + } + } $aSearches[] = '$'.$sSearch.'$'; $aReplacements[] = (string) $replace; } - return str_replace($aSearches, $aReplacements, $aInput); + return str_replace($aSearches, $aReplacements, $sInput); } /**