You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
804 lines
28 KiB
804 lines
28 KiB
<?php |
|
|
|
/** |
|
* @file |
|
* Contains the state and data related stuff. |
|
*/ |
|
|
|
/** |
|
* The rules evaluation state. |
|
* |
|
* A rule element may clone the state, so any added variables are only visible |
|
* for elements in the current PHP-variable-scope. |
|
*/ |
|
class RulesState { |
|
|
|
/** |
|
* Globally keeps the ids of rules blocked due to recursion prevention. |
|
* |
|
* @var array |
|
*/ |
|
static protected $blocked = array(); |
|
|
|
/** |
|
* The known variables. |
|
* |
|
* @var array |
|
*/ |
|
public $variables = array(); |
|
|
|
/** |
|
* Holds info about the variables. |
|
* |
|
* @var array |
|
*/ |
|
protected $info = array(); |
|
|
|
/** |
|
* Keeps wrappers to be saved later on. |
|
*/ |
|
protected $save; |
|
|
|
/** |
|
* Holds the arguments while an element is executed. |
|
* |
|
* May be used by the element to easily access the wrapped arguments. |
|
*/ |
|
public $currentArguments; |
|
|
|
/** |
|
* Variable for saving currently blocked configs for serialization. |
|
*/ |
|
protected $currentlyBlocked; |
|
|
|
/** |
|
* Constructs a RulesState object. |
|
*/ |
|
public function __construct() { |
|
// Use an object in order to ensure any cloned states reference the same |
|
// save information. |
|
$this->save = new ArrayObject(); |
|
$this->addVariable('site', FALSE, self::defaultVariables('site')); |
|
} |
|
|
|
/** |
|
* Adds the given variable to the given execution state. |
|
*/ |
|
public function addVariable($name, $data, $info) { |
|
$this->info[$name] = $info + array( |
|
'skip save' => FALSE, |
|
'type' => 'unknown', |
|
'handler' => FALSE, |
|
); |
|
if (empty($this->info[$name]['handler'])) { |
|
$this->variables[$name] = rules_wrap_data($data, $this->info[$name]); |
|
} |
|
} |
|
|
|
/** |
|
* Runs post-evaluation tasks, such as saving variables. |
|
*/ |
|
public function cleanUp() { |
|
// Make changes permanent. |
|
foreach ($this->save->getArrayCopy() as $selector => $wrapper) { |
|
$this->saveNow($selector); |
|
} |
|
unset($this->currentArguments); |
|
} |
|
|
|
/** |
|
* Block a rules configuration from execution. |
|
*/ |
|
public function block($rules_config) { |
|
if (empty($rules_config->recursion) && $rules_config->id) { |
|
self::$blocked[$rules_config->id] = TRUE; |
|
} |
|
} |
|
|
|
/** |
|
* Unblock a rules configuration from execution. |
|
*/ |
|
public function unblock($rules_config) { |
|
if (empty($rules_config->recursion) && $rules_config->id) { |
|
unset(self::$blocked[$rules_config->id]); |
|
} |
|
} |
|
|
|
/** |
|
* Returns whether a rules configuration should be blocked from execution. |
|
*/ |
|
public function isBlocked($rule_config) { |
|
return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]); |
|
} |
|
|
|
/** |
|
* Get the info about the state variables or a single variable. |
|
*/ |
|
public function varInfo($name = NULL) { |
|
if (isset($name)) { |
|
return isset($this->info[$name]) ? $this->info[$name] : FALSE; |
|
} |
|
return $this->info; |
|
} |
|
|
|
/** |
|
* Returns whether the given wrapper is savable. |
|
*/ |
|
public function isSavable($wrapper) { |
|
return ($wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save')) || $wrapper instanceof RulesDataWrapperSavableInterface; |
|
} |
|
|
|
/** |
|
* Returns whether the variable with the given name is an entity. |
|
*/ |
|
public function isEntity($name) { |
|
$entity_info = entity_get_info(); |
|
return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]); |
|
} |
|
|
|
/** |
|
* Gets a variable. |
|
* |
|
* If necessary, the specified handler is invoked to fetch the variable. |
|
* |
|
* @param string $name |
|
* The name of the variable to return. |
|
* |
|
* @return |
|
* The variable or a EntityMetadataWrapper containing the variable. |
|
* |
|
* @throws RulesEvaluationException |
|
* Throws a RulesEvaluationException in case we have info about the |
|
* requested variable, but it is not defined. |
|
*/ |
|
public function &get($name) { |
|
if (!array_key_exists($name, $this->variables)) { |
|
// If there is handler to load the variable, do it now. |
|
if (!empty($this->info[$name]['handler'])) { |
|
$data = call_user_func($this->info[$name]['handler'], rules_unwrap_data($this->variables), $name, $this->info[$name]); |
|
$this->variables[$name] = rules_wrap_data($data, $this->info[$name]); |
|
$this->info[$name]['handler'] = FALSE; |
|
if (!isset($data)) { |
|
throw new RulesEvaluationException('Unable to load variable %name, aborting.', array('%name' => $name), NULL, RulesLog::INFO); |
|
} |
|
} |
|
else { |
|
throw new RulesEvaluationException('Unable to get variable %name, it is not defined.', array('%name' => $name), NULL, RulesLog::ERROR); |
|
} |
|
} |
|
return $this->variables[$name]; |
|
} |
|
|
|
/** |
|
* Apply permanent changes provided the wrapper's data type is savable. |
|
* |
|
* @param $selector |
|
* The data selector of the wrapper to save or just a variable name. |
|
* @param $wrapper |
|
* @param bool $immediate |
|
* Pass FALSE to postpone saving to later on. Else it's immediately saved. |
|
*/ |
|
public function saveChanges($selector, $wrapper, $immediate = FALSE) { |
|
$info = $wrapper->info(); |
|
if (empty($info['skip save']) && $this->isSavable($wrapper)) { |
|
$this->save($selector, $wrapper, $immediate); |
|
} |
|
// No entity, so try saving the parent. |
|
elseif (empty($info['skip save']) && isset($info['parent']) && !($wrapper instanceof EntityDrupalWrapper)) { |
|
// Cut of the last part of the selector. |
|
$selector = implode(':', explode(':', $selector, -1)); |
|
$this->saveChanges($selector, $info['parent'], $immediate); |
|
} |
|
return $this; |
|
} |
|
|
|
/** |
|
* Remembers to save the wrapper on cleanup or does it now. |
|
*/ |
|
protected function save($selector, EntityMetadataWrapper $wrapper, $immediate) { |
|
// Convert variable names and selectors to both use underscores. |
|
$selector = strtr($selector, '-', '_'); |
|
if (isset($this->save[$selector])) { |
|
if ($this->save[$selector][0]->getIdentifier() == $wrapper->getIdentifier()) { |
|
// The entity is already remembered. So do a combined save. |
|
$this->save[$selector][1] += self::$blocked; |
|
} |
|
else { |
|
// The wrapper is already in there, but wraps another entity. So first |
|
// save the old one, then care about the new one. |
|
$this->saveNow($selector); |
|
} |
|
} |
|
if (!isset($this->save[$selector])) { |
|
// In case of immediate saving don't clone the wrapper, so saving a new |
|
// entity immediately makes the identifier available afterwards. |
|
$this->save[$selector] = array($immediate ? $wrapper : clone $wrapper, self::$blocked); |
|
} |
|
if ($immediate) { |
|
$this->saveNow($selector); |
|
} |
|
} |
|
|
|
/** |
|
* Saves the wrapper for the given selector. |
|
*/ |
|
protected function saveNow($selector) { |
|
// Add the set of blocked elements for the recursion prevention. |
|
$previously_blocked = self::$blocked; |
|
self::$blocked += $this->save[$selector][1]; |
|
|
|
// Actually save! |
|
$wrapper = $this->save[$selector][0]; |
|
$entity = $wrapper->value(); |
|
// When operating in hook_entity_insert() $entity->is_new might be still |
|
// set. In that case remove the flag to avoid causing another insert instead |
|
// of an update. |
|
if (!empty($entity->is_new) && $wrapper->getIdentifier()) { |
|
$entity->is_new = FALSE; |
|
} |
|
rules_log('Saved %selector of type %type.', array('%selector' => $selector, '%type' => $wrapper->type())); |
|
$wrapper->save(); |
|
|
|
// Restore the state's set of blocked elements. |
|
self::$blocked = $previously_blocked; |
|
unset($this->save[$selector]); |
|
} |
|
|
|
/** |
|
* Merges info from the given state into the existing state. |
|
* |
|
* Merges the info about to-be-saved variables from the given state into the |
|
* existing state. Therefore we can aggregate saves from invoked components. |
|
* Merged-in saves are removed from the given state, but not-mergeable saves |
|
* remain there. |
|
* |
|
* @param $state |
|
* The state for which to merge the to be saved variables in. |
|
* @param $component |
|
* The component which has been invoked, thus needs to be blocked for the |
|
* merged in saves. |
|
* @param $settings |
|
* The settings of the element that invoked the component. Contains |
|
* information about variable/selector mappings between the states. |
|
*/ |
|
public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) { |
|
// For any saves that we take over, also block the component. |
|
$this->block($component); |
|
|
|
foreach ($state->save->getArrayCopy() as $selector => $data) { |
|
$parts = explode(':', $selector, 2); |
|
// Adapt the selector to fit for the parent state and move the wrapper. |
|
if (isset($settings[$parts[0] . ':select'])) { |
|
$parts[0] = $settings[$parts[0] . ':select']; |
|
$this->save(implode(':', $parts), $data[0], FALSE); |
|
unset($state->save[$selector]); |
|
} |
|
} |
|
$this->unblock($component); |
|
} |
|
|
|
/** |
|
* Returns an entity metadata wrapper as specified in the selector. |
|
* |
|
* @param string $selector |
|
* The selector string, e.g. "node:author:mail". |
|
* @param string $langcode |
|
* (optional) The language code used to get the argument value if the |
|
* argument value should be translated. Defaults to LANGUAGE_NONE. |
|
* |
|
* @return EntityMetadataWrapper |
|
* The wrapper for the given selector. |
|
* |
|
* @throws RulesEvaluationException |
|
* Throws a RulesEvaluationException in case the selector cannot be applied. |
|
*/ |
|
public function applyDataSelector($selector, $langcode = LANGUAGE_NONE) { |
|
$parts = explode(':', str_replace('-', '_', $selector), 2); |
|
$wrapper = $this->get($parts[0]); |
|
if (count($parts) == 1) { |
|
return $wrapper; |
|
} |
|
elseif (!$wrapper instanceof EntityMetadataWrapper) { |
|
throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not wrapped correctly.', array('%selector' => $selector)); |
|
} |
|
try { |
|
foreach (explode(':', $parts[1]) as $name) { |
|
if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) { |
|
// Make sure we are using the right language. Wrappers might be cached |
|
// and have previous langcodes set, so always set the right language. |
|
if ($wrapper instanceof EntityStructureWrapper) { |
|
$wrapper->language($langcode); |
|
} |
|
$wrapper = $wrapper->get($name); |
|
} |
|
else { |
|
throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not a list or a structure: %wrapper.', array('%selector' => $selector, '%wrapper' => $wrapper)); |
|
} |
|
} |
|
} |
|
catch (EntityMetadataWrapperException $e) { |
|
// In case of an exception, re-throw it. |
|
throw new RulesEvaluationException('Unable to apply data selector %selector: %error', array('%selector' => $selector, '%error' => $e->getMessage())); |
|
} |
|
return $wrapper; |
|
} |
|
|
|
/** |
|
* Magic method. Only serialize variables and their info. |
|
* |
|
* Additionally we remember currently blocked configs, so we can restore them |
|
* upon deserialization using restoreBlocks(). |
|
*/ |
|
public function __sleep() { |
|
$this->currentlyBlocked = self::$blocked; |
|
return array('info', 'variables', 'currentlyBlocked'); |
|
} |
|
|
|
/** |
|
* Magic method. Unserialize variables and their info. |
|
*/ |
|
public function __wakeup() { |
|
$this->save = new ArrayObject(); |
|
} |
|
|
|
/** |
|
* Restores the before-serialization blocked configurations. |
|
* |
|
* Warning: This overwrites any possible currently blocked configs. Thus |
|
* do not invoke this method if there might be evaluations active. |
|
*/ |
|
public function restoreBlocks() { |
|
self::$blocked = $this->currentlyBlocked; |
|
} |
|
|
|
/** |
|
* Defines always-available variables. |
|
* |
|
* @param $key |
|
* (optional) |
|
*/ |
|
public static function defaultVariables($key = NULL) { |
|
// Add a variable for accessing site-wide data properties. |
|
$vars['site'] = array( |
|
'type' => 'site', |
|
'label' => t('Site information'), |
|
'description' => t("Site-wide settings and other global information."), |
|
// Add the property info via a callback making use of the cached info. |
|
'property info alter' => array('RulesData', 'addSiteMetadata'), |
|
'property info' => array(), |
|
'optional' => TRUE, |
|
); |
|
return isset($key) ? $vars[$key] : $vars; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* A class holding static methods related to data. |
|
*/ |
|
class RulesData { |
|
|
|
/** |
|
* Returns whether the type match. They match if type1 is compatible to type2. |
|
* |
|
* @param $var_info |
|
* The name of the type to check for whether it is compatible to type2. |
|
* @param $param_info |
|
* The type expression to check for. |
|
* @param bool $ancestors |
|
* (optional) Whether sub-type relationships for checking type compatibility |
|
* should be taken into account. Defaults to TRUE. |
|
* |
|
* @return bool |
|
* Whether the types match. |
|
*/ |
|
public static function typesMatch($var_info, $param_info, $ancestors = TRUE) { |
|
$var_type = $var_info['type']; |
|
$param_type = $param_info['type']; |
|
|
|
if ($param_type == '*' || $param_type == 'unknown') { |
|
return TRUE; |
|
} |
|
|
|
if ($var_type == $param_type) { |
|
// Make sure the bundle matches, if specified by the parameter. |
|
return !isset($param_info['bundles']) || isset($var_info['bundle']) && in_array($var_info['bundle'], $param_info['bundles']); |
|
} |
|
|
|
// Parameters may specify multiple types using an array. |
|
$valid_types = is_array($param_type) ? $param_type : array($param_type); |
|
if (in_array($var_type, $valid_types)) { |
|
return TRUE; |
|
} |
|
|
|
// Check for sub-type relationships. |
|
if ($ancestors && !isset($param_info['bundles'])) { |
|
$cache = &rules_get_cache(); |
|
self::typeCalcAncestors($cache, $var_type); |
|
// If one of the types is an ancestor return TRUE. |
|
return (bool) array_intersect_key($cache['data_info'][$var_type]['ancestors'], array_flip($valid_types)); |
|
} |
|
return FALSE; |
|
} |
|
|
|
protected static function typeCalcAncestors(&$cache, $type) { |
|
if (!isset($cache['data_info'][$type]['ancestors'])) { |
|
$cache['data_info'][$type]['ancestors'] = array(); |
|
if (isset($cache['data_info'][$type]['parent']) && $parent = $cache['data_info'][$type]['parent']) { |
|
$cache['data_info'][$type]['ancestors'][$parent] = TRUE; |
|
self::typeCalcAncestors($cache, $parent); |
|
// Add all parent ancestors to our own ancestors. |
|
$cache['data_info'][$type]['ancestors'] += $cache['data_info'][$parent]['ancestors']; |
|
} |
|
// For special lists like list<node> add in "list" as valid parent. |
|
if (entity_property_list_extract_type($type)) { |
|
$cache['data_info'][$type]['ancestors']['list'] = TRUE; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Returns data for the given info and the to-be-configured parameter. |
|
* |
|
* Returns matching data variables or properties for the given info and the |
|
* to-be-configured parameter. |
|
* |
|
* @param $source |
|
* Either an array of info about available variables or a entity metadata |
|
* wrapper. |
|
* @param $param_info |
|
* The information array about the to be configured parameter. |
|
* @param string $prefix |
|
* An optional prefix for the data selectors. |
|
* @param int $recursions |
|
* The number of recursions used to go down the tree. Defaults to 2. |
|
* @param bool $suggestions |
|
* Whether possibilities to recurse are suggested as soon as the deepest |
|
* level of recursions is reached. Defaults to TRUE. |
|
* |
|
* @return array |
|
* An array of info about matching variables or properties that match, keyed |
|
* with the data selector. |
|
*/ |
|
public static function matchingDataSelector($source, $param_info, $prefix = '', $recursions = 2, $suggestions = TRUE) { |
|
// If an array of info is given, get entity metadata wrappers first. |
|
$data = NULL; |
|
if (is_array($source)) { |
|
foreach ($source as $name => $info) { |
|
$source[$name] = rules_wrap_data($data, $info, TRUE); |
|
} |
|
} |
|
|
|
$matches = array(); |
|
foreach ($source as $name => $wrapper) { |
|
$info = $wrapper->info(); |
|
$name = str_replace('_', '-', $name); |
|
|
|
if (self::typesMatch($info, $param_info)) { |
|
$matches[$prefix . $name] = $info; |
|
if (!is_array($source) && $source instanceof EntityListWrapper) { |
|
// Add some more possible list items. |
|
for ($i = 1; $i < 4; $i++) { |
|
$matches[$prefix . $i] = $info; |
|
} |
|
} |
|
} |
|
// Recurse later on to get an improved ordering of the results. |
|
if ($wrapper instanceof EntityStructureWrapper || $wrapper instanceof EntityListWrapper) { |
|
$recurse[$prefix . $name] = $wrapper; |
|
if ($recursions > 0) { |
|
$matches += self::matchingDataSelector($wrapper, $param_info, $prefix . $name . ':', $recursions - 1, $suggestions); |
|
} |
|
elseif ($suggestions) { |
|
// We may not recurse any more, |
|
// but indicate the possibility to recurse. |
|
$matches[$prefix . $name . ':'] = $wrapper->info(); |
|
if (!is_array($source) && $source instanceof EntityListWrapper) { |
|
// Add some more possible list items. |
|
for ($i = 1; $i < 4; $i++) { |
|
$matches[$prefix . $i . ':'] = $wrapper->info(); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return $matches; |
|
} |
|
|
|
/** |
|
* Adds asserted metadata to the variable info. |
|
* |
|
* In case there are already assertions for a variable, the assertions are |
|
* merged such that both apply. |
|
* |
|
* @see RulesData::applyMetadataAssertions() |
|
*/ |
|
public static function addMetadataAssertions($var_info, $assertions) { |
|
foreach ($assertions as $selector => $assertion) { |
|
// Convert the selector back to underscores, such it matches the varname. |
|
$selector = str_replace('-', '_', $selector); |
|
|
|
$parts = explode(':', $selector); |
|
if (isset($var_info[$parts[0]])) { |
|
// Apply the selector to determine the right target array. We build an |
|
// array like |
|
// $var_info['rules assertion']['property1']['property2']['#info'] = .. |
|
$target = &$var_info[$parts[0]]['rules assertion']; |
|
foreach (array_slice($parts, 1) as $part) { |
|
$target = &$target[$part]; |
|
} |
|
|
|
// In case the assertion is directly for a variable, we have to modify |
|
// the variable info directly. In case the asserted property is nested |
|
// the info-has to be altered by RulesData::applyMetadataAssertions() |
|
// before the child-wrapper is created. |
|
if (count($parts) == 1) { |
|
// Support asserting a type in case of generic entity references only. |
|
$var_type = &$var_info[$parts[0]]['type']; |
|
if (isset($assertion['type']) && ($var_type == 'entity' || $var_type == 'list<entity>')) { |
|
$var_type = $assertion['type']; |
|
unset($assertion['type']); |
|
} |
|
// Add any single bundle directly to the variable info, so the |
|
// variable fits as argument for parameters requiring the bundle. |
|
if (isset($assertion['bundle']) && count($bundles = (array) $assertion['bundle']) == 1) { |
|
$var_info[$parts[0]]['bundle'] = reset($bundles); |
|
} |
|
} |
|
|
|
// Add the assertions, but merge them with any previously added |
|
// assertions if necessary. |
|
$target['#info'] = isset($target['#info']) ? rules_update_array($target['#info'], $assertion) : $assertion; |
|
|
|
// Add in a callback that the entity metadata wrapper pick up for |
|
// altering the property info, such that we can add in the assertions. |
|
$var_info[$parts[0]] += array('property info alter' => array('RulesData', 'applyMetadataAssertions')); |
|
|
|
// In case there is a VARNAME_unchanged variable as it is used in update |
|
// hooks, assume the assertions are valid for the unchanged variable |
|
// too. |
|
if (isset($var_info[$parts[0] . '_unchanged'])) { |
|
$name = $parts[0] . '_unchanged'; |
|
$var_info[$name]['rules assertion'] = $var_info[$parts[0]]['rules assertion']; |
|
$var_info[$name]['property info alter'] = array('RulesData', 'applyMetadataAssertions'); |
|
|
|
if (isset($var_info[$parts[0]]['bundle']) && !isset($var_info[$name]['bundle'])) { |
|
$var_info[$name]['bundle'] = $var_info[$parts[0]]['bundle']; |
|
} |
|
} |
|
} |
|
} |
|
return $var_info; |
|
} |
|
|
|
/** |
|
* Property info alter callback for the entity metadata wrapper. |
|
* |
|
* Used for applying the rules metadata assertions. |
|
* |
|
* @see RulesData::addMetadataAssertions() |
|
*/ |
|
public static function applyMetadataAssertions(EntityMetadataWrapper $wrapper, $property_info) { |
|
$info = $wrapper->info(); |
|
|
|
if (!empty($info['rules assertion'])) { |
|
$assertion = $info['rules assertion']; |
|
|
|
// In case there are list-wrappers pass through the assertions of the item |
|
// but make sure we only apply the assertions for the list items for |
|
// which the conditions are executed. |
|
if (isset($info['parent']) && $info['parent'] instanceof EntityListWrapper) { |
|
$assertion = isset($assertion[$info['name']]) ? $assertion[$info['name']] : array(); |
|
} |
|
|
|
// Support specifying multiple bundles, whereas the added properties are |
|
// the intersection of the bundle properties. |
|
if (isset($assertion['#info']['bundle'])) { |
|
$bundles = (array) $assertion['#info']['bundle']; |
|
foreach ($bundles as $bundle) { |
|
$properties[] = isset($property_info['bundles'][$bundle]['properties']) ? $property_info['bundles'][$bundle]['properties'] : array(); |
|
} |
|
// Add the intersection. |
|
$property_info['properties'] += count($properties) > 1 ? call_user_func_array('array_intersect_key', $properties) : reset($properties); |
|
} |
|
// Support adding directly asserted property info. |
|
if (isset($assertion['#info']['property info'])) { |
|
$property_info['properties'] += $assertion['#info']['property info']; |
|
} |
|
|
|
// Pass through any rules assertion of properties to their info, so any |
|
// derived wrappers apply it. |
|
foreach (element_children($assertion) as $key) { |
|
$property_info['properties'][$key]['rules assertion'] = $assertion[$key]; |
|
$property_info['properties'][$key]['property info alter'] = array('RulesData', 'applyMetadataAssertions'); |
|
|
|
// Apply any 'type' and 'bundle' assertion directly to the property |
|
// info. |
|
if (isset($assertion[$key]['#info']['type'])) { |
|
$type = $assertion[$key]['#info']['type']; |
|
// Support asserting a type in case of generic entity references only. |
|
if ($property_info['properties'][$key]['type'] == 'entity' && entity_get_info($type)) { |
|
$property_info['properties'][$key]['type'] = $type; |
|
} |
|
} |
|
if (isset($assertion[$key]['#info']['bundle'])) { |
|
$bundle = (array) $assertion[$key]['#info']['bundle']; |
|
// Add any single bundle directly to the variable info, so the |
|
// property fits as argument for parameters requiring the bundle. |
|
if (count($bundle) == 1) { |
|
$property_info['properties'][$key]['bundle'] = reset($bundle); |
|
} |
|
} |
|
} |
|
} |
|
return $property_info; |
|
} |
|
|
|
/** |
|
* Property info alter callback for the entity metadata wrapper. |
|
* |
|
* Used to inject metadata for the 'site' variable. In contrast to doing this |
|
* via hook_rules_data_info() this callback makes use of the already existing |
|
* property info cache for site information of entity metadata. |
|
* |
|
* @see RulesPlugin::availableVariables() |
|
*/ |
|
public static function addSiteMetadata(EntityMetadataWrapper $wrapper, $property_info) { |
|
$site_info = entity_get_property_info('site'); |
|
$property_info['properties'] += $site_info['properties']; |
|
// Also invoke the usual callback for altering metadata, in case actions |
|
// have specified further metadata. |
|
return RulesData::applyMetadataAssertions($wrapper, $property_info); |
|
} |
|
|
|
} |
|
|
|
/** |
|
* A wrapper class similar to the EntityDrupalWrapper, but for non-entities. |
|
* |
|
* This class is intended to serve as base for a custom wrapper classes of |
|
* identifiable data types, which are non-entities. By extending this class only |
|
* the extractIdentifier() and load() methods have to be defined. |
|
* In order to make the data type savable implement the |
|
* RulesDataWrapperSavableInterface. |
|
* |
|
* That way it is possible for non-entity data types to be work with Rules, i.e. |
|
* one can implement a 'ui class' with a direct input form returning the |
|
* identifier of the data. However, instead of that it is suggested to implement |
|
* an entity type, such that the same is achieved via general API functions like |
|
* entity_load(). |
|
*/ |
|
abstract class RulesIdentifiableDataWrapper extends EntityStructureWrapper { |
|
|
|
/** |
|
* Contains the id. |
|
*/ |
|
protected $id = FALSE; |
|
|
|
/** |
|
* Construct a new wrapper object. |
|
* |
|
* @param $type |
|
* The type of the passed data. |
|
* @param $data |
|
* (optional) The data to wrap or its identifier. |
|
* @param array $info |
|
* (optional) Used internally to pass info about properties down the tree. |
|
*/ |
|
public function __construct($type, $data = NULL, $info = array()) { |
|
parent::__construct($type, $data, $info); |
|
$this->setData($data); |
|
} |
|
|
|
/** |
|
* Sets the data internally accepting both the data id and object. |
|
*/ |
|
protected function setData($data) { |
|
if (isset($data) && $data !== FALSE && !is_object($data)) { |
|
$this->id = $data; |
|
$this->data = FALSE; |
|
} |
|
elseif (is_object($data)) { |
|
// We got the data object passed. |
|
$this->data = $data; |
|
$id = $this->extractIdentifier($data); |
|
$this->id = isset($id) ? $id : FALSE; |
|
} |
|
} |
|
|
|
/** |
|
* Returns the identifier of the wrapped data. |
|
*/ |
|
public function getIdentifier() { |
|
return $this->dataAvailable() && $this->value() ? $this->id : NULL; |
|
} |
|
|
|
/** |
|
* Overridden. |
|
*/ |
|
public function value(array $options = array()) { |
|
$this->setData(parent::value()); |
|
if (!$this->data && !empty($this->id)) { |
|
// Lazy load the data if necessary. |
|
$this->data = $this->load($this->id); |
|
if (!$this->data) { |
|
throw new EntityMetadataWrapperException('Unable to load the ' . check_plain($this->type) . ' with the id ' . check_plain($this->id) . '.'); |
|
} |
|
} |
|
return $this->data; |
|
} |
|
|
|
/** |
|
* Overridden to support setting the data by either the object or the id. |
|
*/ |
|
public function set($value) { |
|
if (!$this->validate($value)) { |
|
throw new EntityMetadataWrapperException('Invalid data value given. Be sure it matches the required data type and format.'); |
|
} |
|
// As custom wrapper classes can only appear for Rules variables, but not |
|
// as properties we don't have to care about updating the parent. |
|
$this->clear(); |
|
$this->setData($value); |
|
return $this; |
|
} |
|
|
|
/** |
|
* Overridden. |
|
*/ |
|
public function clear() { |
|
$this->id = NULL; |
|
parent::clear(); |
|
} |
|
|
|
/** |
|
* Prepare for serialization. |
|
*/ |
|
public function __sleep() { |
|
$vars = parent::__sleep(); |
|
// Don't serialize the loaded data, except for the case the data is not |
|
// saved yet. |
|
if (!empty($this->id)) { |
|
unset($vars['data']); |
|
} |
|
return $vars; |
|
} |
|
|
|
/** |
|
* Prepare for unserialization. |
|
*/ |
|
public function __wakeup() { |
|
if ($this->id !== FALSE) { |
|
// Make sure data is set, so the data will be loaded when needed. |
|
$this->data = FALSE; |
|
} |
|
} |
|
|
|
/** |
|
* Extract the identifier of the given data object. |
|
* |
|
* @return |
|
* The extracted identifier. |
|
*/ |
|
abstract protected function extractIdentifier($data); |
|
|
|
/** |
|
* Load a data object given an identifier. |
|
* |
|
* @return |
|
* The loaded data object, or FALSE if loading failed. |
|
*/ |
|
abstract protected function load($id); |
|
|
|
} |
|
|
|
/** |
|
* Used to declare custom wrapper classes as savable. |
|
*/ |
|
interface RulesDataWrapperSavableInterface { |
|
|
|
/** |
|
* Save the currently wrapped data. |
|
*/ |
|
public function save(); |
|
|
|
}
|
|
|