Internal: Implemented DBObject::ExecActions, enables scripting object preset/modifications

SVN:trunk[3991]
This commit is contained in:
Romain Quetiez
2016-04-08 07:34:38 +00:00
parent b991f0a6c6
commit 725c7d45d1
2 changed files with 509 additions and 0 deletions

View File

@@ -502,6 +502,11 @@ abstract class DBObject implements iDisplay
public function GetStrict($sAttCode)
{
if ($sAttCode == 'id')
{
return $this->m_iKey;
}
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
if (!$oAttDef->LoadInObject())
@@ -3063,5 +3068,355 @@ abstract class DBObject implements iDisplay
}
return $sFingerprint;
}
/**
* Execute a set of scripted actions onto the current object
* See ExecAction for the syntax and features of the scripted actions
*
* @param $aActions array of statements (e.g. "set(name, Made after $source->name$)")
* @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element
* @throws Exception
*/
public function ExecActions($aActions, $aSourceObjects)
{
foreach($aActions as $sAction)
{
try
{
if (preg_match('/^(\S*)\s*\((.*)\)$/ms', $sAction, $aMatches)) // multiline and newline matched by a dot
{
$sVerb = trim($aMatches[1]);
$sParams = $aMatches[2];
// the coma is the separator for the parameters
// comas can be escaped: \,
$sParams = str_replace(array("\\\\", "\\,"), array("__backslash__", "__coma__"), $sParams);
$sParams = trim($sParams);
if (strlen($sParams) == 0)
{
$aParams = array();
}
else
{
$aParams = explode(',', $sParams);
foreach ($aParams as &$sParam)
{
$sParam = str_replace(array("__backslash__", "__coma__"), array("\\", ","), $sParam);
$sParam = trim($sParam);
}
}
$this->ExecAction($sVerb, $aParams, $aSourceObjects);
}
else
{
throw new Exception("Invalid syntax");
}
}
catch(Exception $e)
{
throw new Exception('Action: '.$sAction.' - '.$e->getMessage());
}
}
}
/**
* Helper to copy an attribute between two objects (in memory)
* Originally designed for ExecAction()
*/
public function CopyAttribute($oSourceObject, $sSourceAttCode, $sDestAttCode)
{
if ($sSourceAttCode == 'id')
{
$oSourceAttDef = null;
}
else
{
if (!MetaModel::IsValidAttCode(get_class($this), $sDestAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sDestAttCode);
}
if (!MetaModel::IsValidAttCode(get_class($oSourceObject), $sSourceAttCode))
{
throw new Exception("Unknown attribute ".get_class($oSourceObject)."::".$sSourceAttCode);
}
$oSourceAttDef = MetaModel::GetAttributeDef(get_class($oSourceObject), $sSourceAttCode);
}
if (is_object($oSourceAttDef) && $oSourceAttDef->IsLinkSet())
{
// The copy requires that we create a new object set (the semantic of DBObject::Set is unclear about link sets)
$oDestSet = DBObjectSet::FromScratch($oSourceAttDef->GetLinkedClass());
$oSourceSet = $oSourceObject->Get($sSourceAttCode);
$oSourceSet->Rewind();
while ($oSourceLink = $oSourceSet->Fetch())
{
// Clone the link
$sLinkClass = get_class($oSourceLink);
$oLinkClone = MetaModel::NewObject($sLinkClass);
foreach(MetaModel::ListAttributeDefs($sLinkClass) as $sAttCode => $oAttDef)
{
// As of now, ignore other attribute (do not attempt to recurse!)
if ($oAttDef->IsScalar())
{
$oLinkClone->Set($sAttCode, $oSourceLink->Get($sAttCode));
}
}
// Not necessary - this will be handled by DBObject
// $oLinkClone->Set($oSourceAttDef->GetExtKeyToMe(), 0);
$oDestSet->AddObject($oLinkClone);
}
$this->Set($sDestAttCode, $oDestSet);
}
else
{
$this->Set($sDestAttCode, $oSourceObject->Get($sSourceAttCode));
}
}
/**
* Execute a scripted action onto the current object
* - clone (att1, att2, att3, ...)
* - clone_scalars ()
* - copy (source_att, dest_att)
* - reset (att)
* - nullify (att)
* - set (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
* - append (att, value (placeholders $source->att$ or $current_date$, or $current_contact_id$, ...))
* - add_to_list (source_key_att, dest_att)
* - add_to_list (source_key_att, dest_att, lnk_att, lnk_att_value)
* - apply_stimulus (stimulus)
* - call_method (method_name)
*
* @param $sVerb string Any of the verb listed above (e.g. "set")
* @param $aParams array of strings (e.g. array('name', 'copied from $source->name$')
* @param $aSourceObjects Array of Alias => Context objects (Convention: some statements require the 'source' element
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws Exception
*/
public function ExecAction($sVerb, $aParams, $aSourceObjects)
{
switch($sVerb)
{
case 'clone':
if (!array_key_exists('source', $aSourceObjects))
{
throw new Exception('Missing conventional "source" object');
}
$oObjectToRead = $aSourceObjects['source'];
foreach($aParams as $sAttCode)
{
$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
}
break;
case 'clone_scalars':
if (!array_key_exists('source', $aSourceObjects))
{
throw new Exception('Missing conventional "source" object');
}
$oObjectToRead = $aSourceObjects['source'];
foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
{
if ($oAttDef->IsScalar())
{
$this->CopyAttribute($oObjectToRead, $sAttCode, $sAttCode);
}
}
break;
case 'copy':
if (!array_key_exists('source', $aSourceObjects))
{
throw new Exception('Missing conventional "source" object');
}
$oObjectToRead = $aSourceObjects['source'];
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: source attribute');
}
$sSourceAttCode = $aParams[0];
if (!array_key_exists(1, $aParams))
{
throw new Exception('Missing argument #2: target attribute');
}
$sDestAttCode = $aParams[1];
$this->CopyAttribute($oObjectToRead, $sSourceAttCode, $sDestAttCode);
break;
case 'reset':
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: target attribute');
}
$sAttCode = $aParams[0];
if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
}
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$this->Set($sAttCode, $oAttDef->GetDefaultValue());
break;
case 'nullify':
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: target attribute');
}
$sAttCode = $aParams[0];
if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
}
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
$this->Set($sAttCode, $oAttDef->GetNullValue());
break;
case 'set':
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: target attribute');
}
$sAttCode = $aParams[0];
if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
}
if (!array_key_exists(1, $aParams))
{
throw new Exception('Missing argument #2: value to set');
}
$sRawValue = $aParams[1];
$aContext = array();
foreach ($aSourceObjects as $sAlias => $oObject)
{
$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
}
$aContext['current_contact_id'] = UserRights::GetContactId();
$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
$aContext['current_date'] = date('Y-m-d');
$aContext['current_time'] = date('H:i:s');
$sValue = MetaModel::ApplyParams($sRawValue, $aContext);
$this->Set($sAttCode, $sValue);
break;
case 'append':
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: target attribute');
}
$sAttCode = $aParams[0];
if (!MetaModel::IsValidAttCode(get_class($this), $sAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sAttCode);
}
if (!array_key_exists(1, $aParams))
{
throw new Exception('Missing argument #2: value to append');
}
$sRawAddendum = $aParams[1];
$aContext = array();
foreach ($aSourceObjects as $sAlias => $oObject)
{
$aContext = array_merge($aContext, $oObject->ToArgs($sAlias));
}
$aContext['current_contact_id'] = UserRights::GetContactId();
$aContext['current_contact_friendlyname'] = UserRights::GetUserFriendlyName();
$aContext['current_date'] = date('Y-m-d');
$aContext['current_time'] = date('H:i:s');
$sAddendum = MetaModel::ApplyParams($sRawAddendum, $aContext);
$this->Set($sAttCode, $this->Get($sAttCode).$sAddendum);
break;
case 'add_to_list':
if (!array_key_exists('source', $aSourceObjects))
{
throw new Exception('Missing conventional "source" object');
}
$oObjectToRead = $aSourceObjects['source'];
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: source attribute');
}
$sSourceKeyAttCode = $aParams[0];
if (!MetaModel::IsValidAttCode(get_class($oObjectToRead), $sSourceKeyAttCode))
{
throw new Exception("Unknown attribute ".get_class($oObjectToRead)."::".$sSourceKeyAttCode);
}
if (!array_key_exists(1, $aParams))
{
throw new Exception('Missing argument #2: target attribute (link set)');
}
$sTargetListAttCode = $aParams[1]; // indirect !!!
if (!MetaModel::IsValidAttCode(get_class($this), $sTargetListAttCode))
{
throw new Exception("Unknown attribute ".get_class($this)."::".$sTargetListAttCode);
}
if (isset($aParams[2]) && isset($aParams[3]))
{
$sRoleAttCode = $aParams[2];
$sRoleValue = $aParams[3];
}
$iObjKey = $oObjectToRead->Get($sSourceKeyAttCode);
if ($iObjKey > 0)
{
$oLinkSet = $this->Get($sTargetListAttCode);
$oListAttDef = MetaModel::GetAttributeDef(get_class($this), $sTargetListAttCode);
$oLnk = MetaModel::NewObject($oListAttDef->GetLinkedClass());
$oLnk->Set($oListAttDef->GetExtKeyToRemote(), $iObjKey);
if (isset($sRoleAttCode))
{
if (!MetaModel::IsValidAttCode(get_class($oLnk), $sRoleAttCode))
{
throw new Exception("Unknown attribute ".get_class($oLnk)."::".$sRoleAttCode);
}
$oLnk->Set($sRoleAttCode, $sRoleValue);
}
$oLinkSet->AddObject($oLnk);
$this->Set($sTargetListAttCode, $oLinkSet);
}
break;
case 'apply_stimulus':
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: stimulus');
}
$sStimulus = $aParams[0];
if (!in_array($sStimulus, MetaModel::EnumStimuli(get_class($this))))
{
throw new Exception("Unknown stimulus ".get_class($this)."::".$sStimulus);
}
$this->ApplyStimulus($sStimulus);
break;
case 'call_method':
if (!array_key_exists('source', $aSourceObjects))
{
throw new Exception('Missing conventional "source" object');
}
$oObjectToRead = $aSourceObjects['source'];
if (!array_key_exists(0, $aParams))
{
throw new Exception('Missing argument #1: method name');
}
$sMethod = $aParams[0];
$aCallSpec = array($this, $sMethod);
if (!is_callable($aCallSpec))
{
throw new Exception("Unknown method ".get_class($this)."::".$sMethod.'()');
}
// Note: $oObjectToRead has been preserved when adding $aSourceObjects, so as to remain backward compatible with methods having only 1 parameter ($oObjectToReadà
call_user_func($aCallSpec, $oObjectToRead, $aSourceObjects);
break;
default:
throw new Exception("Invalid verb");
}
}
}

View File

@@ -4894,4 +4894,158 @@ class TestLinkSetRecording_1NAdd_Remove extends TestBizModel
sort($aRet);
return $aRet;
}
}
class TestExecActions extends TestBizModel
{
static public function GetName()
{
return 'Scripted actions API DBObject::ExecAction - syntax errors';
}
static public function GetDescription()
{
return 'Check that wrong arguments are correclty reported';
}
protected function DoExecute()
{
$oSource = new UserRequest();
$oSource->Set('title', 'Houston!');
$oSource->Set('description', 'Looks like we have a problem');
$oTarget = new Server();
////////////////////////////////////////////////////////////////////////////////
// Scenarii
//
$aScenarii = array(
array(
'action' => 'set',
'error' => 'Action: set - Invalid syntax'
),
array(
'action' => 'smurf()',
'error' => 'Action: smurf() - Invalid verb'
),
array(
'action' => ' smurf () ',
'error' => 'Action: smurf () - Invalid syntax'
),
array(
'action' => 'clone(some_att_code, another_one)',
'error' => 'Action: clone(some_att_code, another_one) - Unknown attribute Server::some_att_code'
),
array(
'action' => 'copy(toto, titi)',
'error' => 'Action: copy(toto, titi) - Unknown attribute Server::titi'
),
array(
'action' => 'copy(toto, name)',
'error' => 'Action: copy(toto, name) - Unknown attribute UserRequest::toto'
),
array(
'action' => 'copy()',
'error' => 'Action: copy() - Missing argument #1: source attribute'
),
array(
'action' => 'copy(title)',
'error' => 'Action: copy(title) - Missing argument #2: target attribute'
),
array(
'action' => 'set(toto)',
'error' => 'Action: set(toto) - Unknown attribute Server::toto'
),
array(
'action' => 'set(toto, something)',
'error' => 'Action: set(toto, something) - Unknown attribute Server::toto'
),
array(
'action' => 'set()',
'error' => 'Action: set() - Missing argument #1: target attribute'
),
array(
'action' => 'reset(toto)',
'error' => 'Action: reset(toto) - Unknown attribute Server::toto'
),
array(
'action' => 'reset()',
'error' => 'Action: reset() - Missing argument #1: target attribute'
),
array(
'action' => 'nullify(toto)',
'error' => 'Action: nullify(toto) - Unknown attribute Server::toto'
),
array(
'action' => 'nullify()',
'error' => 'Action: nullify() - Missing argument #1: target attribute'
),
array(
'action' => 'append(toto, something)',
'error' => 'Action: append(toto, something) - Unknown attribute Server::toto'
),
array(
'action' => 'append(name)',
'error' => 'Action: append(name) - Missing argument #2: value to append'
),
array(
'action' => 'append()',
'error' => 'Action: append() - Missing argument #1: target attribute'
),
array(
'action' => 'add_to_list(toto, titi)',
'error' => 'Action: add_to_list(toto, titi) - Unknown attribute UserRequest::toto'
),
array(
'action' => 'add_to_list(caller_id, titi)',
'error' => 'Action: add_to_list(caller_id, titi) - Unknown attribute Server::titi'
),
array(
'action' => 'add_to_list(caller_id)',
'error' => 'Action: add_to_list(caller_id) - Missing argument #2: target attribute (link set)'
),
array(
'action' => 'add_to_list()',
'error' => 'Action: add_to_list() - Missing argument #1: source attribute'
),
array(
'action' => 'apply_stimulus(toto)',
'error' => 'Action: apply_stimulus(toto) - Unknown stimulus Server::toto'
),
array(
'action' => 'apply_stimulus()',
'error' => 'Action: apply_stimulus() - Missing argument #1: stimulus'
),
array(
'action' => 'call_method(toto)',
'error' => 'Action: call_method(toto) - Unknown method Server::toto()'
),
array(
'action' => 'call_method()',
'error' => 'Action: call_method() - Missing argument #1: method name'
),
);
foreach ($aScenarii as $aScenario)
{
echo "<h4>".htmlentities($aScenario['action'], ENT_QUOTES, 'UTF-8')."</h4>\n";
$sMessage = '';
try
{
$oTarget->ExecActions(array($aScenario['action']), array('source' => $oSource));
$sMessage = 'Expecting an exception... none has been thrown!';
}
catch (Exception $e)
{
if ($e->getMessage() != $aScenario['error'])
{
$sMessage = 'Wrong message: expecting "'.$aScenario['error'].'" and got "'.$e->getMessage().'"';
}
}
if ($sMessage !='')
{
throw new Exception($sMessage);
}
}
}
}