diff --git a/.travis.yml b/.travis.yml
index 498f2dc8..80eb8f54 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,7 @@ php:
env:
- DRUPAL_CORE=8.0.x
- DRUPAL_CORE=8.1.x
+ - DRUPAL_CORE=8.2.x
matrix:
allow_failures:
@@ -59,7 +60,9 @@ before_script:
# Run composer install for Drupal 8.1. We need an up-to-date composer when
# installing Drupal 8.1.
- - if [ "$DRUPAL_CORE" = "8.1.x" ]; then composer self-update && composer install; fi
+ # Disabled for now because core still includes the vendor directory, see
+ # https://www.drupal.org/node/1475510
+ # - if [ "$DRUPAL_CORE" = "8.1.x" ]; then composer self-update && composer install; fi
# Start a web server on port 8888, run in the background.
- php -S localhost:8888 &
diff --git a/config/schema/rules.action.schema.yml b/config/schema/rules.action.schema.yml
index ac585450..bbefa6aa 100644
--- a/config/schema/rules.action.schema.yml
+++ b/config/schema/rules.action.schema.yml
@@ -1,3 +1,10 @@
+# Per default the schema of arbitrary context values of an action cannot be
+# typed. Actions that need translatability or other features of the config
+# system must specify their context value schema explicitly, see examples below.
+rules.action.context_values.*:
+ type: ignore
+ label: Context values
+
rules.action.context_values.rules_system_message:
type: mapping
label: Message action context values
diff --git a/config/schema/rules.context.schema.yml b/config/schema/rules.context.schema.yml
index 45ee7071..3f6126e0 100644
--- a/config/schema/rules.context.schema.yml
+++ b/config/schema/rules.context.schema.yml
@@ -25,12 +25,6 @@ rules.context.values:
# The entries depend on the plugin here. Plugins have to extend this and
# provide a suiting schema.
-rules.context.mapping:
- type: sequence
- label: 'Context mapping'
- sequence:
- - type: string
-
rules.context.processors:
type: sequence
label: 'Context processors'
diff --git a/config/schema/rules.data_types.schema.yml b/config/schema/rules.data_types.schema.yml
index c40fee14..bbc9c351 100644
--- a/config/schema/rules.data_types.schema.yml
+++ b/config/schema/rules.data_types.schema.yml
@@ -7,11 +7,11 @@ rules_component:
label: 'Context definitions'
sequence:
- type: rules.context.definition
- provided_context:
+ provided_context_definitions:
type: sequence
- label: 'Names of provided context'
+ label: 'Provided context definitions'
sequence:
- - type: string
+ - type: rules.context.definition
expression:
type: rules_expression.[id]
label: 'Expression configuration'
diff --git a/config/schema/rules.schema.yml b/config/schema/rules.schema.yml
index 7b1b7225..9230169d 100644
--- a/config/schema/rules.schema.yml
+++ b/config/schema/rules.schema.yml
@@ -35,9 +35,21 @@ rules.reaction.*:
label:
type: label
label: 'Label'
- event:
- type: string
- label: 'Event'
+ events:
+ type: sequence
+ label: 'Events'
+ sequence:
+ type: mapping
+ label: 'Event'
+ mapping:
+ event_name:
+ type: string
+ label: 'Name'
+ configuration:
+ type: sequence
+ label: 'Configuration'
+ sequence:
+ type: mapping
module:
type: string
label: 'Module'
diff --git a/src/Context/ContextAwarePluginInterface.php b/src/Context/ContextAwarePluginInterface.php
index dd5c37a3..97806c18 100644
--- a/src/Context/ContextAwarePluginInterface.php
+++ b/src/Context/ContextAwarePluginInterface.php
@@ -20,7 +20,46 @@ interface ContextAwarePluginInterface extends CoreContextAwarePluginInterface {
* When a plugin is configured half-way or even fully, some context values are
* already available upon which the definition of subsequent or provided
* context can be refined.
+ *
+ * Implement this method, when the plugin's context definitions need to be
+ * refined. When the selected data definitions should be refined, implement
+ * ::assertMetadata() instead.
+ *
+ * Note that context gets refined at configuration and execution time of the
+ * plugin.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $selected_data
+ * An array of data definitions for context that is mapped using a data
+ * selector, keyed by context name.
+ */
+ public function refineContextDefinitions(array $selected_data);
+
+ /**
+ * Asserts additional metadata for the selected data.
+ *
+ * Allows the plugin to assert additional metadata that is in place when the
+ * plugin has been successfully executed. A typical use-case would be
+ * asserting the node type for a "Node is of type" condition. By doing so,
+ * sub-sequent executed plugins are aware of the metadata and can build upon
+ * it.
+ *
+ * Implement this method, when the selected data definitions need to be
+ * refined. When the plugin's context definitions should be refined, implement
+ * ::refineContextDefinitions() instead.
+ *
+ * Note that metadata is only asserted on configuration time. The plugin has
+ * to ensure that the run-time data matches the asserted configuration if it
+ * has been executed successfully.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $selected_data
+ * An array of data definitions for context that is mapped using a data
+ * selector, keyed by context name.
+ *
+ * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
+ * An array of modified data definitions, keyed as the passed array. Note
+ * data definitions need to be cloned *before* they are modified, such that
+ * the changes do not propagate unintentionally.
*/
- public function refineContextDefinitions();
+ public function assertMetadata(array $selected_data);
}
diff --git a/src/Engine/IntegrityCheckTrait.php b/src/Context/ContextHandlerIntegrityTrait.php
similarity index 63%
rename from src/Engine/IntegrityCheckTrait.php
rename to src/Context/ContextHandlerIntegrityTrait.php
index 19b473b6..0f5135d6 100644
--- a/src/Engine/IntegrityCheckTrait.php
+++ b/src/Context/ContextHandlerIntegrityTrait.php
@@ -2,25 +2,26 @@
/**
* @file
- * Contains \Drupal\rules\Engine\IntegrityCheckTrait.
+ * Contains \Drupal\rules\Engine\ContextHandlerIntegrityTrait.
*/
-namespace Drupal\rules\Engine;
+namespace Drupal\rules\Context;
-use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
+use Drupal\Core\Plugin\Context\ContextDefinitionInterface as CoreContextDefinitionInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface as CoreContextAwarePluginInterface;
-use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
-use Drupal\Core\TypedData\ListInterface;
-use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\rules\Context\ContextDefinitionInterface as RulesContextDefinitionInterface;
-use Drupal\rules\Context\ContextProviderInterface;
+use Drupal\rules\Engine\ExecutionMetadataStateInterface;
+use Drupal\rules\Engine\IntegrityViolation;
+use Drupal\rules\Engine\IntegrityViolationList;
use Drupal\rules\Exception\RulesIntegrityException;
/**
- * Provides shared integrity checking methods for conditions and actions.
+ * Extends the context handler trait with support for checking integrity.
*/
-trait IntegrityCheckTrait {
+trait ContextHandlerIntegrityTrait {
+
+ use ContextHandlerTrait;
/**
* Performs the integrity check.
@@ -34,16 +35,19 @@ trait IntegrityCheckTrait {
* @return \Drupal\rules\Engine\IntegrityViolationList
* The list of integrity violations.
*/
- protected function doCheckIntegrity(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
+ protected function checkContextConfigIntegrity(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
$violation_list = new IntegrityViolationList();
$context_definitions = $plugin->getContextDefinitions();
+ // Make sure that all provided variables by this plugin are added to the
+ // execution metadata state.
+ $this->addProvidedContextDefinitions($plugin, $metadata_state);
+
foreach ($context_definitions as $name => $context_definition) {
// Check if a data selector is configured that maps to the state.
if (isset($this->configuration['context_mapping'][$name])) {
try {
- $data_definition = $metadata_state->fetchDefinitionByPropertyPath($this->configuration['context_mapping'][$name]);
-
+ $data_definition = $this->getMappedDefinition($name, $metadata_state);
$this->checkDataTypeCompatible($context_definition, $data_definition, $name, $violation_list);
}
catch (RulesIntegrityException $e) {
@@ -83,7 +87,7 @@ protected function doCheckIntegrity(CoreContextAwarePluginInterface $plugin, Exe
$violation_list->add($violation);
}
}
- elseif ($context_definition->isRequired()) {
+ elseif ($context_definition->isRequired() && $context_definition->getDefaultValue() === NULL) {
$violation = new IntegrityViolation();
$violation->setMessage($this->t('The required context %context_name is missing.', [
'%context_name' => $context_definition->getLabel(),
@@ -112,10 +116,6 @@ protected function doCheckIntegrity(CoreContextAwarePluginInterface $plugin, Exe
}
}
- // Make sure that all provided variables by this plugin are added to the
- // execution metadata state.
- $this->addProvidedVariablesToExecutionMetadataState($plugin, $metadata_state);
-
return $violation_list;
}
@@ -131,28 +131,17 @@ protected function doCheckIntegrity(CoreContextAwarePluginInterface $plugin, Exe
* @param \Drupal\rules\Engine\IntegrityViolationList $violation_list
* The list of violations where new ones will be added.
*/
- protected function checkDataTypeCompatible(ContextDefinitionInterface $context_definition, DataDefinitionInterface $provided, $context_name, IntegrityViolationList $violation_list) {
- $expected_class = $context_definition->getDataDefinition()->getClass();
- $provided_class = $provided->getClass();
- $expected_type_problem = NULL;
-
- if (is_subclass_of($expected_class, PrimitiveInterface::class)
- && !is_subclass_of($provided_class, PrimitiveInterface::class)
- ) {
- $expected_type_problem = $this->t('primitive');
- }
- elseif (is_subclass_of($expected_class, ListInterface::class)
- && !is_subclass_of($provided_class, ListInterface::class)
- ) {
- $expected_type_problem = $this->t('list');
- }
- elseif (is_subclass_of($expected_class, ComplexDataInterface::class)
- && !is_subclass_of($provided_class, ComplexDataInterface::class)
- ) {
- $expected_type_problem = $this->t('complex');
+ protected function checkDataTypeCompatible(CoreContextDefinitionInterface $context_definition, DataDefinitionInterface $provided, $context_name, IntegrityViolationList $violation_list) {
+ // Compare data types. For now, fail if they are not equal.
+ // @todo: Add support for matching based upon type-inheritance.
+ $target_type = $context_definition->getDataDefinition()->getDataType();
+
+ // Special case any and entity target types for now.
+ if ($target_type == 'any' || ($target_type == 'entity' && strpos($provided->getDataType(), 'entity:') !== FALSE)) {
+ return;
}
-
- if ($expected_type_problem) {
+ if ($target_type != $provided->getDataType()) {
+ $expected_type_problem = $context_definition->getDataDefinition()->getDataType();
$violation = new IntegrityViolation();
$violation->setMessage($this->t('Expected a @expected_type data type for context %context_name but got a @provided_type data type instead.', [
'@expected_type' => $expected_type_problem,
@@ -165,33 +154,4 @@ protected function checkDataTypeCompatible(ContextDefinitionInterface $context_d
}
}
- /**
- * Adds provided variables to the execution metadata state.
- *
- * @param CoreContextAwarePluginInterface $plugin
- * The action or condition plugin that may provide variables.
- * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
- * The excution metadata state to add variables to.
- */
- public function addProvidedVariablesToExecutionMetadataState(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
- if ($plugin instanceof ContextProviderInterface) {
- $provided_context_definitions = $plugin->getProvidedContextDefinitions();
-
- foreach ($provided_context_definitions as $name => $context_definition) {
- if (isset($this->configuration['provides_mapping'][$name])) {
- // Populate the state with the new variable that is provided by this
- // plugin. That is necessary so that the integrity check in subsequent
- // actions knows about the variable and does not throw violations.
- $metadata_state->setDataDefinition(
- $this->configuration['provides_mapping'][$name],
- $context_definition->getDataDefinition()
- );
- }
- else {
- $metadata_state->setDataDefinition($name, $context_definition->getDataDefinition());
- }
- }
- }
- }
-
}
diff --git a/src/Context/ContextHandlerTrait.php b/src/Context/ContextHandlerTrait.php
index d9d77664..462e27e2 100644
--- a/src/Context/ContextHandlerTrait.php
+++ b/src/Context/ContextHandlerTrait.php
@@ -7,10 +7,12 @@
namespace Drupal\rules\Context;
-use Drupal\Core\Plugin\Context\Context;
+use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Plugin\ContextAwarePluginInterface as CoreContextAwarePluginInterface;
+use Drupal\rules\Engine\ExecutionMetadataStateInterface;
use Drupal\rules\Engine\ExecutionStateInterface;
use Drupal\rules\Exception\RulesEvaluationException;
+use Drupal\rules\Exception\RulesIntegrityException;
/**
* Provides methods for handling context based on the plugin configuration.
@@ -30,61 +32,188 @@ trait ContextHandlerTrait {
protected $processorManager;
/**
- * Maps variables from rules state into the plugin context.
+ * Prepares plugin context based upon the set context configuration.
+ *
+ * The plugin is prepared for execution by mapping the variables from the
+ * execution state into the plugin context and applying data processors.
+ * In addition, it is ensured that all required context is basically
+ * available as defined. This include the following checks:
+ * - Required context must have a value set.
+ * - Context may not have NULL values unless the plugin allows it.
*
* @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
* The plugin that is populated with context values.
* @param \Drupal\rules\Engine\ExecutionStateInterface $state
- * The Rules state containing available variables.
+ * The execution state containing available variables.
*
* @throws \Drupal\rules\Exception\RulesEvaluationException
- * In case a required context is missing for the plugin.
+ * Thrown if some context is not satisfied; e.g. a required context is
+ * missing.
+ *
+ * @see ::prepareContextWithMetadata()
*/
- protected function mapContext(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $state) {
- $context_definitions = $plugin->getContextDefinitions();
- foreach ($context_definitions as $name => $definition) {
- // Check if a data selector is configured that maps to the state.
- if (isset($this->configuration['context_mapping'][$name])) {
- $typed_data = $state->fetchDataByPropertyPath($this->configuration['context_mapping'][$name]);
-
- if ($typed_data->getValue() === NULL && !$definition->isAllowedNull()) {
- throw new RulesEvaluationException('The value of data selector '
- . $this->configuration['context_mapping'][$name] . " is NULL, but the context $name in "
- . $plugin->getPluginId() . ' requires a value.');
+ protected function prepareContext(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $state) {
+ if (isset($this->configuration['context_values'])) {
+ foreach ($this->configuration['context_values'] as $name => $value) {
+ $plugin->setContextValue($name, $value);
+ }
+ }
+
+ $selected_data = [];
+ // Map context by applying data selectors and collected the definitions of
+ // selected data for refining context definitions later. Note, that we must
+ // refine context definitions on execution time also, such that provided
+ // context gets the right metadata attached.
+ if (isset($this->configuration['context_mapping'])) {
+ foreach ($this->configuration['context_mapping'] as $name => $selector) {
+ $typed_data = $state->fetchDataByPropertyPath($selector);
+ $plugin->setContextValue($name, $typed_data);
+ $selected_data[$name] = $typed_data->getDataDefinition();
+ }
+ }
+
+ if ($plugin instanceof ContextAwarePluginInterface) {
+ // Getting context values may lead to undocumented exceptions if context
+ // is not set right now. So catch those exceptions.
+ // @todo: Remove ones https://www.drupal.org/node/2677162 got fixed.
+ try {
+ $plugin->refineContextDefinitions($selected_data);
+ }
+ catch (ContextException $e) {
+ if (strpos($e->getMessage(), 'context is required') === FALSE) {
+ throw new RulesEvaluationException($e->getMessage());
}
- $context = $plugin->getContext($name);
- $new_context = Context::createFromContext($context, $typed_data);
- $plugin->setContext($name, $new_context);
}
- elseif (isset($this->configuration['context_values'])
- && array_key_exists($name, $this->configuration['context_values'])
- ) {
+ }
- if ($this->configuration['context_values'][$name] === NULL && !$definition->isAllowedNull()) {
- throw new RulesEvaluationException("The context value for $name is NULL, but the context $name in "
+ // Apply data processors.
+ $this->processData($plugin, $state);
+
+ // Finally, ensure all contexts are set as expected now.
+ foreach ($plugin->getContextDefinitions() as $name => $definition) {
+ if ($plugin->getContextValue($name) === NULL && $definition->isRequired()) {
+ // If a context mapping has been specified, the value might end up NULL
+ // but valid (e.g. a reference on an empty property). In that case
+ // isAllowedNull determines whether the context is conform.
+ if (!isset($this->configuration['context_mapping'][$name])) {
+ throw new RulesEvaluationException("Required context $name is missing for plugin "
+ . $plugin->getPluginId() . '.');
+ }
+ elseif (!$definition->isAllowedNull()) {
+ throw new RulesEvaluationException("The context for $name is NULL, but the context $name in "
. $plugin->getPluginId() . ' requires a value.');
}
+ }
+ }
+ }
+
+ /**
+ * Prepares plugin context based upon the set context configuration.
+ *
+ * The configuration is applied as far as possible without having execution
+ * time data. That means, the configured context values are set and context is
+ * refined while leveraging the definitions of selected data.
+ *
+ * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
+ * The plugin that is prepared.
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The metadata state, prepared for the current expression.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\ContextException
+ * Thrown if the plugin tries to access some not-defined context. As this is
+ * a developer error, this should not be caught.
+ *
+ * @see ::prepareContext()
+ */
+ protected function prepareContextWithMetadata(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
+ if (isset($this->configuration['context_values'])) {
+ foreach ($this->configuration['context_values'] as $name => $value) {
+ $plugin->setContextValue($name, $value);
+ }
+ }
- $context = $plugin->getContext($name);
- $new_context = Context::createFromContext($context, $this->configuration['context_values'][$name]);
- $plugin->setContext($name, $new_context);
+ if ($plugin instanceof ContextAwarePluginInterface) {
+ $selected_data = $this->getSelectedData($metadata_state);
+ // Getting context values may lead to undocumented exceptions if context
+ // is not set right now. So catch those exceptions.
+ // @todo: Remove ones https://www.drupal.org/node/2677162 got fixed.
+ try {
+ $plugin->refineContextDefinitions($selected_data);
}
- elseif ($definition->isRequired()) {
- throw new RulesEvaluationException("Required context $name is missing for plugin "
- . $plugin->getPluginId() . '.');
+ catch (ContextException $e) {
+ if (strpos($e->getMessage(), 'context is required') === FALSE) {
+ throw $e;
+ }
}
}
}
/**
- * Maps provided context values from the plugin to the Rules state.
+ * Gets definitions of all selected data at configuration time.
+ *
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The metadata state.
*
- * @param ContextProviderInterface $plugin
- * The plugin where the context values are extracted.
+ * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
+ * An array of data definitions for context that is mapped using a data
+ * selector, keyed by context name.
+ */
+ protected function getSelectedData(ExecutionMetadataStateInterface $metadata_state) {
+ $selected_data = [];
+ // Collected the definitions of selected data for refining context
+ // definitions.
+ if (isset($this->configuration['context_mapping'])) {
+ // If no state is available, we need to fetch at least the definitions of
+ // selected data for refining context.
+ foreach ($this->configuration['context_mapping'] as $name => $selector) {
+ try {
+ $selected_data[$name] = $this->getMappedDefinition($name, $metadata_state);
+ }
+ catch (RulesIntegrityException $e) {
+ // Ignore invalid data selectors here, such that context gets refined
+ // as far as possible still and can be respected by the UI when fixing
+ // broken selectors.
+ }
+ }
+ }
+ return $selected_data;
+ }
+
+ /**
+ * Gets the definition of the data that is mapped to the given context.
+ *
+ * @param string $context_name
+ * The name of the context.
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The metadata state containing metadata about available variables.
+ *
+ * @return \Drupal\Core\TypedData\DataDefinitionInterface|null
+ * A data definition if the property path could be applied, or NULL if the
+ * context is not mapped.
+ *
+ * @throws \Drupal\rules\Exception\RulesIntegrityException
+ * Thrown if the data selector that is configured for the context is
+ * invalid.
+ */
+ protected function getMappedDefinition($context_name, ExecutionMetadataStateInterface $metadata_state) {
+ if (isset($this->configuration['context_mapping'][$context_name])) {
+ return $metadata_state->fetchDefinitionByPropertyPath($this->configuration['context_mapping'][$context_name]);
+ }
+ }
+
+ /**
+ * Adds provided context values from the plugin to the execution state.
+ *
+ * @param CoreContextAwarePluginInterface $plugin
+ * The context aware plugin of which to add provided context.
* @param \Drupal\rules\Engine\ExecutionStateInterface $state
* The Rules state where the context variables are added.
*/
- protected function mapProvidedContext(ContextProviderInterface $plugin, ExecutionStateInterface $state) {
+ protected function addProvidedContext(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $state) {
+ // If the plugin does not support providing context, there is nothing to do.
+ if (!$plugin instanceof ContextProviderInterface) {
+ return;
+ }
$provides = $plugin->getProvidedContextDefinitions();
foreach ($provides as $name => $provided_definition) {
// Avoid name collisions in the rules state: provided variables can be
@@ -98,6 +227,61 @@ protected function mapProvidedContext(ContextProviderInterface $plugin, Executio
}
}
+ /**
+ * Adds the definitions of provided context to the execution metadata state.
+ *
+ * @param CoreContextAwarePluginInterface $plugin
+ * The context aware plugin of which to add provided context.
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The execution metadata state to add variables to.
+ */
+ protected function addProvidedContextDefinitions(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
+ // If the plugin does not support providing context, there is nothing to do.
+ if (!$plugin instanceof ContextProviderInterface) {
+ return;
+ }
+
+ foreach ($plugin->getProvidedContextDefinitions() as $name => $context_definition) {
+ if (isset($this->configuration['provides_mapping'][$name])) {
+ // Populate the state with the new variable that is provided by this
+ // plugin. That is necessary so that the integrity check in subsequent
+ // actions knows about the variable and does not throw violations.
+ $metadata_state->setDataDefinition(
+ $this->configuration['provides_mapping'][$name],
+ $context_definition->getDataDefinition()
+ );
+ }
+ else {
+ $metadata_state->setDataDefinition($name, $context_definition->getDataDefinition());
+ }
+ }
+ }
+
+ /**
+ * Asserts additional metadata.
+ *
+ * @param CoreContextAwarePluginInterface $plugin
+ * The context aware plugin.
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The execution metadata state.
+ */
+ protected function assertMetadata(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
+ // If the plugin does not implement the Rules-enhanced interface, skip this.
+ if (!$plugin instanceof ContextAwarePluginInterface) {
+ return;
+ }
+ $changed_definitions = $plugin->assertMetadata($this->getSelectedData($metadata_state));
+
+ // Reverse the mapping and apply the changes.
+ foreach ($changed_definitions as $context_name => $definition) {
+ $selector = $this->configuration['context_mapping'][$context_name];
+ // @todo: Deal with selectors matching not a context name.
+ if (strpos($selector, '.') === FALSE) {
+ $metadata_state->setDataDefinition($selector, $definition);
+ }
+ }
+ }
+
/**
* Process data context on the plugin, usually before it gets executed.
*
diff --git a/src/Context/ContextProviderTrait.php b/src/Context/ContextProviderTrait.php
index e0cd0847..7f4a6974 100644
--- a/src/Context/ContextProviderTrait.php
+++ b/src/Context/ContextProviderTrait.php
@@ -13,6 +13,9 @@
/**
* A trait implementing the ContextProviderInterface.
*
+ * This trait is intended for context aware plugins that want to provide
+ * context.
+ *
* The trait requires the plugin to use configuration as defined by the
* ContextConfig class.
*
diff --git a/src/Core/ConditionManager.php b/src/Core/ConditionManager.php
index 27b0e4e1..c3bbd9bf 100644
--- a/src/Core/ConditionManager.php
+++ b/src/Core/ConditionManager.php
@@ -16,6 +16,16 @@
*/
class ConditionManager extends CoreConditionManager {
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\rules\Core\RulesConditionInterface|\Drupal\Core\Condition\ConditionInterface
+ * A fully configured plugin instance.
+ */
+ public function createInstance($plugin_id, array $configuration = []) {
+ return parent::createInstance($plugin_id, $configuration);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Core/RulesActionBase.php b/src/Core/RulesActionBase.php
index 1559c58f..431a797b 100644
--- a/src/Core/RulesActionBase.php
+++ b/src/Core/RulesActionBase.php
@@ -7,6 +7,7 @@
namespace Drupal\rules\Core;
+use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\Core\Session\AccountInterface;
@@ -31,10 +32,35 @@ abstract class RulesActionBase extends ContextAwarePluginBase implements RulesAc
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function getContextValue($name) {
+ try {
+ return parent::getContextValue($name);
+ }
+ catch (ContextException $e) {
+ // Catch the undocumented exception thrown when no context value is set
+ // for a required context.
+ // @todo: Remove once https://www.drupal.org/node/2677162 is fixed.
+ if (strpos($e->getMessage(), 'context is required') === FALSE) {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function refineContextDefinitions(array $selected_data) {
// Do not refine anything by default.
}
+ /**
+ * {@inheritdoc}
+ */
+ public function assertMetadata(array $selected_data) {
+ // Nothing to assert by default.
+ return [];
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Core/RulesActionManagerInterface.php b/src/Core/RulesActionManagerInterface.php
index 36f4a304..cb3e9ecd 100644
--- a/src/Core/RulesActionManagerInterface.php
+++ b/src/Core/RulesActionManagerInterface.php
@@ -20,4 +20,12 @@
*/
interface RulesActionManagerInterface extends CategorizingPluginManagerInterface, ContextAwarePluginManagerInterface {
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\rules\Core\RulesActionInterface
+ * A fully configured plugin instance.
+ */
+ public function createInstance($plugin_id, array $configuration = []);
+
}
diff --git a/src/Core/RulesConditionBase.php b/src/Core/RulesConditionBase.php
index a7b5e3e8..9ac5d8ef 100644
--- a/src/Core/RulesConditionBase.php
+++ b/src/Core/RulesConditionBase.php
@@ -25,10 +25,35 @@ abstract class RulesConditionBase extends ConditionPluginBase implements RulesCo
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function refineContextDefinitions(array $selected_data) {
// Do not refine anything by default.
}
+ /**
+ * {@inheritdoc}
+ */
+ public function assertMetadata(array $selected_data) {
+ // Nothing to assert by default.
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContextValue($name) {
+ try {
+ return parent::getContextValue($name);
+ }
+ catch (ContextException $e) {
+ // Catch the undocumented exception thrown when no context value is set
+ // for a required context.
+ // @todo: Remove once https://www.drupal.org/node/2677162 is fixed.
+ if (strpos($e->getMessage(), 'context is required') === FALSE) {
+ throw $e;
+ }
+ }
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Core/RulesDefaultEventHandler.php b/src/Core/RulesDefaultEventHandler.php
index f2297bae..1af70b63 100644
--- a/src/Core/RulesDefaultEventHandler.php
+++ b/src/Core/RulesDefaultEventHandler.php
@@ -22,6 +22,7 @@ public function getContextDefinitions() {
$definition = $this->getPluginDefinition();
if ($this instanceof RulesConfigurableEventHandlerInterface) {
$this->refineContextDefinitions();
+ $definition = $this->getPluginDefinition();
}
return !empty($definition['context']) ? $definition['context'] : [];
}
diff --git a/src/Engine/ActionExpressionContainer.php b/src/Engine/ActionExpressionContainer.php
index c2677d90..fac597fe 100644
--- a/src/Engine/ActionExpressionContainer.php
+++ b/src/Engine/ActionExpressionContainer.php
@@ -10,12 +10,11 @@
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\rules\Context\ContextConfig;
use Drupal\rules\Exception\InvalidExpressionException;
-use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Container for actions.
*/
-abstract class ActionExpressionContainer extends ExpressionBase implements ActionExpressionContainerInterface, ContainerFactoryPluginInterface {
+abstract class ActionExpressionContainer extends ExpressionContainerBase implements ActionExpressionContainerInterface, ContainerFactoryPluginInterface {
/**
* List of actions that will be executed.
@@ -47,18 +46,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
}
}
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $configuration,
- $plugin_id,
- $plugin_definition,
- $container->get('plugin.manager.rules_expression')
- );
- }
-
/**
* {@inheritdoc}
*/
@@ -73,15 +60,6 @@ public function addExpressionObject(ExpressionInterface $expression) {
return $this;
}
- /**
- * {@inheritdoc}
- */
- public function addExpression($plugin_id, ContextConfig $config = NULL) {
- return $this->addExpressionObject(
- $this->expressionManager->createInstance($plugin_id, $config ? $config->toArray() : [])
- );
- }
-
/**
* {@inheritdoc}
*/
@@ -160,42 +138,4 @@ public function deleteExpression($uuid) {
return FALSE;
}
- /**
- * {@inheritdoc}
- */
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
- $violation_list = new IntegrityViolationList();
- foreach ($this->actions as $action) {
- $action_violations = $action->checkIntegrity($metadata_state);
- $violation_list->addAll($action_violations);
- }
- return $violation_list;
- }
-
- /**
- * {@inheritdoc}
- */
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
- if ($until) {
- if ($this->getUuid() === $until->getUuid()) {
- return TRUE;
- }
- foreach ($this->actions as $action) {
- if ($action->getUuid() === $until->getUuid()) {
- return TRUE;
- }
- $found = $action->prepareExecutionMetadataState($metadata_state, $until);
- if ($found) {
- return TRUE;
- }
- }
- return FALSE;
- }
-
- foreach ($this->actions as $action) {
- $action->prepareExecutionMetadataState($metadata_state);
- }
- return TRUE;
- }
-
}
diff --git a/src/Engine/ConditionExpressionContainer.php b/src/Engine/ConditionExpressionContainer.php
index 10452187..f23ac2da 100644
--- a/src/Engine/ConditionExpressionContainer.php
+++ b/src/Engine/ConditionExpressionContainer.php
@@ -10,17 +10,16 @@
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\rules\Context\ContextConfig;
use Drupal\rules\Exception\InvalidExpressionException;
-use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Container for conditions.
*/
-abstract class ConditionExpressionContainer extends ExpressionBase implements ConditionExpressionContainerInterface, ContainerFactoryPluginInterface {
+abstract class ConditionExpressionContainer extends ExpressionContainerBase implements ConditionExpressionContainerInterface, ContainerFactoryPluginInterface {
/**
* List of conditions that are evaluated.
*
- * @var \Drupal\rules\Core\RulesConditionInterface[]
+ * @var \Drupal\rules\Engine\ConditionExpressionInterface[]
*/
protected $conditions = [];
@@ -47,18 +46,6 @@ public function __construct(array $configuration, $plugin_id, array $plugin_defi
}
}
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $configuration,
- $plugin_id,
- $plugin_definition,
- $container->get('plugin.manager.rules_expression')
- );
- }
-
/**
* {@inheritdoc}
*/
@@ -73,15 +60,6 @@ public function addExpressionObject(ExpressionInterface $expression) {
return $this;
}
- /**
- * {@inheritdoc}
- */
- public function addExpression($plugin_id, ContextConfig $config = NULL) {
- return $this->addExpressionObject(
- $this->expressionManager->createInstance($plugin_id, $config ? $config->toArray() : [])
- );
- }
-
/**
* {@inheritdoc}
*/
@@ -188,42 +166,4 @@ public function deleteExpression($uuid) {
return FALSE;
}
- /**
- * {@inheritdoc}
- */
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
- $violation_list = new IntegrityViolationList();
- foreach ($this->conditions as $condition) {
- $condition_violations = $condition->checkIntegrity($metadata_state);
- $violation_list->addAll($condition_violations);
- }
- return $violation_list;
- }
-
- /**
- * {@inheritdoc}
- */
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
- if ($until) {
- if ($this->getUuid() === $until->getUuid()) {
- return TRUE;
- }
- foreach ($this->conditions as $condition) {
- if ($condition->getUuid() === $until->getUuid()) {
- return TRUE;
- }
- $found = $condition->prepareExecutionMetadataState($metadata_state, $until);
- if ($found) {
- return TRUE;
- }
- }
- return FALSE;
- }
-
- foreach ($this->conditions as $condition) {
- $condition->prepareExecutionMetadataState($metadata_state);
- }
- return TRUE;
- }
-
}
diff --git a/src/Engine/ConditionExpressionInterface.php b/src/Engine/ConditionExpressionInterface.php
index b10705bc..9f058e5b 100644
--- a/src/Engine/ConditionExpressionInterface.php
+++ b/src/Engine/ConditionExpressionInterface.php
@@ -12,6 +12,17 @@
*/
interface ConditionExpressionInterface extends ExpressionInterface {
+
+ /**
+ * Negates the result after evaluating this condition.
+ *
+ * @param bool $negate
+ * TRUE to indicate that the condition should be negated, FALSE otherwise.
+ *
+ * @return $this
+ */
+ public function negate($negate = TRUE);
+
/**
* Determines whether condition result will be negated.
*
diff --git a/src/Engine/ExecutionMetadataStateInterface.php b/src/Engine/ExecutionMetadataStateInterface.php
index 4215393a..fdf637f3 100644
--- a/src/Engine/ExecutionMetadataStateInterface.php
+++ b/src/Engine/ExecutionMetadataStateInterface.php
@@ -79,12 +79,13 @@ public function removeDataDefinition($name);
* @param string $property_path
* The property path, example: "node:title:value".
* @param string $langcode
- * The langauge code.
+ * The language code.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface
* A data definition if the property path could be applied.
*
* @throws \Drupal\rules\Exception\RulesIntegrityException
+ * Thrown if the property path is invalid.
*/
public function fetchDefinitionByPropertyPath($property_path, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED);
diff --git a/src/Engine/ExecutionState.php b/src/Engine/ExecutionState.php
index 18eba781..21d8d972 100644
--- a/src/Engine/ExecutionState.php
+++ b/src/Engine/ExecutionState.php
@@ -178,6 +178,13 @@ public function saveChangesLater($selector) {
return $this;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getAutoSaveSelectors() {
+ return array_keys($this->saveLater);
+ }
+
/**
* {@inheritdoc}
*/
@@ -185,14 +192,10 @@ public function autoSave() {
// Make changes permanent.
foreach ($this->saveLater as $selector => $flag) {
$typed_data = $this->fetchDataByPropertyPath($selector);
- // The returned data can be NULL, only save it if we actually have
- // something here.
- if ($typed_data) {
- // Things that can be saved must have a save() method, right?
- // Saving is always done at the root of the typed data tree, for example
- // on the entity level.
- $typed_data->getRoot()->getValue()->save();
- }
+ // Things that can be saved must have a save() method, right?
+ // Saving is always done at the root of the typed data tree, for example
+ // on the entity level.
+ $typed_data->getRoot()->getValue()->save();
}
return $this;
}
diff --git a/src/Engine/ExecutionStateInterface.php b/src/Engine/ExecutionStateInterface.php
index c75833fc..65464fb4 100644
--- a/src/Engine/ExecutionStateInterface.php
+++ b/src/Engine/ExecutionStateInterface.php
@@ -121,6 +121,15 @@ public function fetchDataByPropertyPath($property_path, $langcode = NULL);
*/
public function saveChangesLater($selector);
+ /**
+ * Returns the list of variables that should be auto-saved after execution.
+ *
+ * @return string[]
+ * The list of data selectors that specify the target object to be saved.
+ * Example: node.uid.entity.
+ */
+ public function getAutoSaveSelectors();
+
/**
* Saves all variables that have been marked for auto saving.
*
diff --git a/src/Engine/ExpressionContainerBase.php b/src/Engine/ExpressionContainerBase.php
new file mode 100644
index 00000000..f652e2b3
--- /dev/null
+++ b/src/Engine/ExpressionContainerBase.php
@@ -0,0 +1,110 @@
+get('plugin.manager.rules_expression')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addExpression($plugin_id, ContextConfig $config = NULL) {
+ return $this->addExpressionObject(
+ $this->expressionManager->createInstance($plugin_id, $config ? $config->toArray() : [])
+ );
+ }
+
+ /**
+ * Determines whether child-expressions are allowed to assert metadata.
+ *
+ * @return bool
+ * Whether child-expressions are allowed to assert metadata.
+ *
+ * @see \Drupal\rules\Engine\ExpressionInterface::prepareExecutionMetadataState()
+ */
+ abstract protected function allowsMetadataAssertions();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE) {
+ $violation_list = new IntegrityViolationList();
+ $this->prepareExecutionMetadataStateBeforeTraversal($metadata_state);
+ $apply_assertions = $apply_assertions && $this->allowsMetadataAssertions();
+ foreach ($this as $child_expression) {
+ $child_violations = $child_expression->checkIntegrity($metadata_state, $apply_assertions);
+ $violation_list->addAll($child_violations);
+ }
+ $this->prepareExecutionMetadataStateAfterTraversal($metadata_state);
+ return $violation_list;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL, $apply_assertions = TRUE) {
+ if ($until && $this->getUuid() === $until->getUuid()) {
+ return TRUE;
+ }
+ $this->prepareExecutionMetadataStateBeforeTraversal($metadata_state);
+ $apply_assertions = $apply_assertions && $this->allowsMetadataAssertions();
+ foreach ($this as $child_expression) {
+ $found = $child_expression->prepareExecutionMetadataState($metadata_state, $until, $apply_assertions);
+ // If the expression was found, we need to stop.
+ if ($found) {
+ return TRUE;
+ }
+ }
+ $this->prepareExecutionMetadataStateAfterTraversal($metadata_state);
+ }
+
+ /**
+ * Prepares execution metadata state before traversing through children.
+ *
+ * @see ::prepareExecutionMetadataState()
+ * @see ::checkIntegrity()
+ */
+ protected function prepareExecutionMetadataStateBeforeTraversal(ExecutionMetadataStateInterface $metadata_state) {
+ // Any pre-traversal preparations need to be added here.
+ }
+
+ /**
+ * Prepares execution metadata state after traversing through children.
+ *
+ * @see ::prepareExecutionMetadataState()
+ * @see ::checkIntegrity()
+ */
+ protected function prepareExecutionMetadataStateAfterTraversal(ExecutionMetadataStateInterface $metadata_state) {
+ // Any post-traversal preparations need to be added here.
+ }
+
+}
diff --git a/src/Engine/ExpressionInterface.php b/src/Engine/ExpressionInterface.php
index a910972d..7c2be02b 100644
--- a/src/Engine/ExpressionInterface.php
+++ b/src/Engine/ExpressionInterface.php
@@ -68,20 +68,6 @@ public function setRoot(ExpressionInterface $root);
*/
public function getLabel();
- /**
- * Verifies that this expression is configured correctly.
- *
- * Example: all variable names used in the expression are available.
- *
- * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
- * The configuration state used to hold available data definitions of
- * variables.
- *
- * @return \Drupal\rules\Engine\IntegrityViolationList
- * A list object containing \Drupal\rules\Engine\IntegrityViolation objects.
- */
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state);
-
/**
* Returns the UUID of this expression if it is nested in another expression.
*
@@ -99,29 +85,67 @@ public function getUuid();
public function setUuid($uuid);
/**
- * Prepares the execution metadata state by adding variables to it.
+ * Verifies that this expression is configured correctly.
+ *
+ * Example: All configured data selectors must be valid.
+ *
+ * Note that for checking integrity the execution metadata state must be
+ * passed prepared as achieved by ::prepareExecutionMetadataState() and the
+ * expression must apply all metadata state preparations during its integrity
+ * check as it does in ::prepareExecutionMetadataState().
+ * This allows for efficient integrity checks of expression trees; e.g. see
+ * \Drupal\rules\Engine\ActionExpressionContainer::checkIntegrity().
+ *
+ * @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
+ * The execution metadata state, prepared until right before this
+ * expression.
+ * @param bool $apply_assertions
+ * (optional) Whether to apply metadata assertions while preparing the
+ * execution metadata state. Defaults to TRUE.
+ *
+ * @return \Drupal\rules\Engine\IntegrityViolationList
+ * A list object containing \Drupal\rules\Engine\IntegrityViolation objects.
+ *
+ * @see ::prepareExecutionMetadataState()
+ */
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE);
+
+ /**
+ * Prepares the execution metadata state by adding metadata to it.
*
* If this expression contains other expressions then the metadata state is
* set up recursively. If a $until expression is specified then the setup will
- * stop right before that expression. This is useful for inspecting the state
- * at a certain point in the expression tree, for example to do autocompletion
- * of available variables in the state.
- *
- * The difference to fully preparing the state is that not all variables are
- * available in the middle of the expression tree. Preparing with
+ * stop right before that expression to calculate the state at this execution
+ * point.
+ * This is useful for inspecting the state at a certain point in the
+ * expression tree as needed during configuration, for example to do
+ * autocompletion of available variables in the state.
+ *
+ * The difference to fully preparing the state is that not necessarily all
+ * variables are available in the middle of the expression tree, as for
+ * example variables being added later are not added yet. Preparing with
* $until = NULL reflects the execution metadata state at the end of the
- * expression.
+ * expression execution.
*
* @param \Drupal\rules\Engine\ExecutionMetadataStateInterface $metadata_state
- * The execution metadata state to populate variables in.
+ * The execution metadata state, prepared until right before this
+ * expression.
* @param \Drupal\rules\Engine\ExpressionInterface $until
- * (optional) A nested expression if this expression is a container.
- * Preparation of the sate will happen right before that expression.
- *
- * @return bool
- * TRUE if $until is NULL or the nested expression was found in the tree,
- * FALSE otherwise.
+ * (optional) The expression at which metadata preparation should be
+ * stopped. The preparation of the state will be stopped right before that
+ * expression.
+ * @param bool $apply_assertions
+ * (optional) Whether to apply metadata assertions while preparing the
+ * execution metadata state. Defaults to TRUE. Metadata assertions should
+ * be only applied if the expression's execution is required for sub-sequent
+ * expressions being executed. For example, if a condition is optional as
+ * it is part of a logical OR expression, its assertions may not be applied.
+ * Defaults to TRUE.
+ *
+ * @return true|null
+ * True if the metadata has been prepared and the $until expression was
+ * found in the tree. Null otherwise.
*/
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL);
+ public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL, $apply_assertions = TRUE);
}
diff --git a/src/Engine/IntegrityViolationList.php b/src/Engine/IntegrityViolationList.php
index d48ab532..c62a40d2 100644
--- a/src/Engine/IntegrityViolationList.php
+++ b/src/Engine/IntegrityViolationList.php
@@ -30,6 +30,35 @@ public function addAll(IntegrityViolationList $other_list) {
}
}
+ /**
+ * Returns the violation at a given offset.
+ *
+ * @param int $offset
+ * The offset of the violation.
+ *
+ * @return \Drupal\rules\Engine\IntegrityViolationInterface
+ * The violation.
+ *
+ * @throws \OutOfBoundsException
+ * Thrown if the offset does not exist.
+ */
+ public function get($offset) {
+ return $this->offsetGet($offset);
+ }
+
+ /**
+ * Returns whether the given offset exists.
+ *
+ * @param int $offset
+ * The violation offset.
+ *
+ * @return bool
+ * Whether the offset exists.
+ */
+ public function has($offset) {
+ return $this->offsetExists($offset);
+ }
+
/**
* Creates a new violation with the message and adds it to this list.
*
diff --git a/src/Engine/RulesComponent.php b/src/Engine/RulesComponent.php
index 1e500c50..27049e64 100644
--- a/src/Engine/RulesComponent.php
+++ b/src/Engine/RulesComponent.php
@@ -69,7 +69,7 @@ public static function create(ExpressionInterface $expression) {
public static function createFromConfiguration(array $configuration) {
$configuration += [
'context_definitions' => [],
- 'provided_context' => [],
+ 'provided_context_definitions' => [],
];
// @todo: Can we improve this use dependency injection somehow?
$expression_manager = \Drupal::service('plugin.manager.rules_expression');
@@ -78,7 +78,7 @@ public static function createFromConfiguration(array $configuration) {
foreach ($configuration['context_definitions'] as $name => $definition) {
$component->addContextDefinition($name, ContextDefinition::createFromArray($definition));
}
- foreach ($configuration['provided_context'] as $name) {
+ foreach ($configuration['provided_context_definitions'] as $name => $definition) {
$component->provideContext($name);
}
return $component;
@@ -122,7 +122,7 @@ public function getConfiguration() {
'context_definitions' => array_map(function (ContextDefinitionInterface $definition) {
return $definition->toArray();
}, $this->contextDefinitions),
- 'provided_context' => $this->providedContext,
+ 'provided_context_definitions' => $this->providedContext,
];
}
diff --git a/src/Entity/ReactionRuleConfig.php b/src/Entity/ReactionRuleConfig.php
index 8620d9ec..9efed6cf 100644
--- a/src/Entity/ReactionRuleConfig.php
+++ b/src/Entity/ReactionRuleConfig.php
@@ -37,7 +37,7 @@
* config_export = {
* "id",
* "label",
- * "event",
+ * "events",
* "module",
* "description",
* "tag",
@@ -116,11 +116,16 @@ class ReactionRuleConfig extends ConfigEntityBase implements RulesUiComponentPro
protected $module = 'rules';
/**
- * The event name this reaction rule is reacting on.
+ * The events this reaction rule is reacting on.
*
- * @var string
+ * Events array. The array is numerically indexed and contains arrays with the
+ * following structure:
+ * - event_name: String with the event machine name.
+ * - configuration: An array containing the event configuration.
+ *
+ * @var array
*/
- protected $event;
+ protected $events = [];
/**
* Sets a Rules expression instance for this Reaction rule.
@@ -159,7 +164,7 @@ public function getExpression() {
*/
public function getComponent() {
$component = RulesComponent::create($this->getExpression());
- $component->addContextDefinitionsForEvents([$this->getEvent()]);
+ $component->addContextDefinitionsForEvents($this->getEventBaseNames());
return $component;
}
@@ -222,10 +227,55 @@ public function getTag() {
}
/**
- * Returns the event on which this rule will trigger.
+ * Gets configuration of all events the rule is reacting on.
+ *
+ * @return array
+ * The events array. The array is numerically indexed and contains arrays
+ * with the following structure:
+ * - event_name: String with the event machine name.
+ * - configuration: An array containing the event configuration.
+ */
+ public function getEvents() {
+ return $this->events;
+ }
+
+ /**
+ * Gets fully qualified names of all events the rule is reacting on.
+ *
+ * @return string[]
+ * The array of fully qualified event names of the rule.
+ */
+ public function getEventNames() {
+ $names = [];
+ foreach ($this->events as $event) {
+ $names[] = $event['event_name'];
+ }
+ return $names;
+ }
+
+ /**
+ * Gets the base names of all events the rule is reacting on.
+ *
+ * For a configured event name like {EVENT_NAME}--{SUFFIX}, the base event
+ * name {EVENT_NAME} is returned.
+ *
+ * @return string[]
+ * The array of base event names of the rule.
+ *
+ * @see \Drupal\rules\Core\RulesConfigurableEventHandlerInterface::getEventNameSuffix()
*/
- public function getEvent() {
- return $this->event;
+ public function getEventBaseNames() {
+ $names = [];
+ foreach ($this->events as $event) {
+ $event_name = $event['event_name'];
+ if (strpos($event_name, '--') !== FALSE) {
+ // Cut off any suffix from a configured event name.
+ $parts = explode('--', $event_name, 2);
+ $event_name = $parts[0];
+ }
+ $names[] = $event_name;
+ }
+ return $names;
}
/**
diff --git a/src/Entity/ReactionRuleStorage.php b/src/Entity/ReactionRuleStorage.php
index e03cab6d..50baa766 100644
--- a/src/Entity/ReactionRuleStorage.php
+++ b/src/Entity/ReactionRuleStorage.php
@@ -95,10 +95,10 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
protected function getRegisteredEvents() {
$events = [];
foreach ($this->loadMultiple() as $rules_config) {
- $event = $rules_config->getEvent();
- $event = $this->eventManager->getEventBaseName($event);
- if ($event && !isset($events[$event])) {
- $events[$event] = $event;
+ foreach ($rules_config->getEventBaseNames() as $event_name) {
+ if (!isset($events[$event_name])) {
+ $events[$event_name] = $event_name;
+ }
}
}
return $events;
@@ -119,10 +119,13 @@ public function save(EntityInterface $entity) {
// After the reaction rule is saved, we need to rebuild the container,
// otherwise the reaction rule will not fire. However, we can do an
- // optimization: if the event was already registered before, we do not have
- // to rebuild the container.
- if (empty($events_before[$entity->getEvent()])) {
- $this->drupalKernel->rebuildContainer();
+ // optimization: if every event was already registered before, we do not
+ // have to rebuild the container.
+ foreach ($entity->getEventBaseNames() as $event_name) {
+ if (empty($events_before[$event_name])) {
+ $this->drupalKernel->rebuildContainer();
+ break;
+ }
}
return $return;
diff --git a/src/Entity/RulesComponentConfig.php b/src/Entity/RulesComponentConfig.php
index 3c638cf5..e4962881 100644
--- a/src/Entity/RulesComponentConfig.php
+++ b/src/Entity/RulesComponentConfig.php
@@ -164,8 +164,10 @@ public function updateFromComponent(RulesComponent $component) {
*/
public function getContextDefinitions() {
$definitions = [];
- foreach ($this->component['context_definitions'] as $name => $definition) {
- $definitions[$name] = ContextDefinition::createFromArray($definition);
+ if (!empty($this->component['context_definitions'])) {
+ foreach ($this->component['context_definitions'] as $name => $definition) {
+ $definitions[$name] = ContextDefinition::createFromArray($definition);
+ }
}
return $definitions;
}
@@ -187,25 +189,34 @@ public function setContextDefinitions($definitions) {
}
/**
- * Returns the names of context that is provided back to the caller.
+ * Gets the definitions of the provided context.
*
- * @return string[]
- * The names of the context that is provided back.
+ * @return \Drupal\rules\Context\ContextDefinitionInterface[]
+ * The array of context definition, keyed by context name.
*/
- public function getProvidedContext() {
- return $this->component['provided_context'];
+ public function getProvidedContextDefinitions() {
+ $definitions = [];
+ if (!empty($this->component['provided_context_definitions'])) {
+ foreach ($this->component['provided_context_definitions'] as $name => $definition) {
+ $definitions[$name] = ContextDefinition::createFromArray($definition);
+ }
+ }
+ return $definitions;
}
/**
- * Sets the names of the context that is provided back to the caller.
+ * Sets the definitions of the provided context.
*
- * @param string[] $names
- * The names of the context that is provided back.
+ * @param \Drupal\rules\Context\ContextDefinitionInterface[] $definitions
+ * The array of context definitions, keyed by context name.
*
* @return $this
*/
- public function setProvidedContext($names) {
- $this->component['provided_context'] = $names;
+ public function setProvidedContextDefinitions($definitions) {
+ $this->component['provided_context_definitions'] = [];
+ foreach ($definitions as $name => $definition) {
+ $this->component['provided_context_definitions'][$name] = $definition->toArray();
+ }
return $this;
}
diff --git a/src/EventHandler/ConfigurableEventHandlerEntityBundle.php b/src/EventHandler/ConfigurableEventHandlerEntityBundle.php
index 86baa041..58362796 100644
--- a/src/EventHandler/ConfigurableEventHandlerEntityBundle.php
+++ b/src/EventHandler/ConfigurableEventHandlerEntityBundle.php
@@ -15,6 +15,42 @@
*/
class ConfigurableEventHandlerEntityBundle extends ConfigurableEventHandlerBase {
+ /**
+ * The bundles information for the entity.
+ *
+ * @var array
+ */
+ protected $bundlesInfo;
+
+ /**
+ * The entity info plugin definition.
+ *
+ * @var mixed
+ */
+ protected $entityInfo;
+
+ /**
+ * The entity type.
+ *
+ * @var string
+ */
+ protected $entityTypeId;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->entityTypeId = $plugin_definition['entity_type_id'];
+ // @todo: This needs to use dependency injection.
+ $this->entityInfo = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId);
+ // @tdo: use EntityTypeBundleInfo service.
+ $this->bundlesInfo = \Drupal::entityManager()->getBundleInfo($this->entityTypeId);
+ if (!$this->bundlesInfo) {
+ throw new \InvalidArgumentException('Unsupported event name passed.');
+ }
+ }
+
/**
* {@inheritdoc}
*/
@@ -30,42 +66,64 @@ public static function determineQualifiedEvents(Event $event, $event_name, array
* {@inheritdoc}
*/
public function summary() {
- // Nothing to do by default.
+ $bundle = $this->configuration['bundle'];
+ $bundle_label = isset($this->bundlesInfo[$bundle]['label']) ? $this->bundlesInfo[$bundle]['label'] : $bundle;
+ $suffix = isset($bundle) ? ' ' . t('of @bundle-key %name', array('@bundle-key' => $this->entityInfo->getBundleLabel(), '%name' => $bundle_label)) : '';
+ return $this->pluginDefinition['label']->render() . $suffix;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
- // Nothing to do by default.
+ $form['bundle'] = array(
+ '#type' => 'select',
+ '#title' => t('Restrict by @bundle', array('@bundle' => $this->entityInfo->getBundleLabel())),
+ '#description' => t('If you need to filter for multiple values, either add multiple events or use the "Entity is of bundle" condition instead.'),
+ '#default_value' => $this->configuration['bundle'],
+ '#empty_value' => '',
+ );
+ foreach ($this->bundlesInfo as $name => $bundle_info) {
+ $form['bundle']['#options'][$name] = $bundle_info['label'];
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractConfigurationFormValues(array &$form, FormStateInterface $form_state) {
+ $this->configuration['bundle'] = $form_state->getValue('bundle');
}
/**
* {@inheritdoc}
*/
public function validate() {
- // Nothing to check by default.
+ // Nothing to validate.
}
/**
* {@inheritdoc}
*/
public function getEventNameSuffix() {
- // Nothing to do by default.
+ return isset($this->configuration['bundle']) ? $this->configuration['bundle'] : FALSE;
}
/**
* {@inheritdoc}
*/
public function refineContextDefinitions() {
- // Nothing to refine by default.
+ if ($bundle = $this->getEventNameSuffix()) {
+ $this->pluginDefinition['context']['entity']->setBundles([$bundle]);
+ }
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
- // Nothing to calculate by default.
+ // @todo: Implement.
}
}
diff --git a/src/EventSubscriber/GenericEventSubscriber.php b/src/EventSubscriber/GenericEventSubscriber.php
index ce3edd81..4fa13f64 100644
--- a/src/EventSubscriber/GenericEventSubscriber.php
+++ b/src/EventSubscriber/GenericEventSubscriber.php
@@ -125,7 +125,7 @@ public function onRulesEvent(Event $event, $event_name) {
// another rule.
foreach ($triggered_events as $triggered_event) {
// @todo Only load active reaction rules here.
- $configs = $storage->loadByProperties(['event' => $triggered_event]);
+ $configs = $storage->loadByProperties(['events.*.event_name' => $triggered_event]);
// Loop over all rules and execute them.
foreach ($configs as $config) {
diff --git a/src/Form/ReactionRuleAddForm.php b/src/Form/ReactionRuleAddForm.php
index 2fb521bd..7b1869e7 100644
--- a/src/Form/ReactionRuleAddForm.php
+++ b/src/Form/ReactionRuleAddForm.php
@@ -8,6 +8,7 @@
namespace Drupal\rules\Form;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\rules\Core\RulesConfigurableEventHandlerInterface;
use Drupal\rules\Core\RulesEventManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -65,17 +66,43 @@ public function form(array $form, FormStateInterface $form_state) {
}
}
- $form['event'] = [
+ $form['events']['#tree'] = TRUE;
+ $form['events'][]['event_name'] = [
'#type' => 'select',
'#title' => $this->t('React on event'),
'#options' => $options,
'#required' => TRUE,
+ '#ajax' => $this->getDefaultAjax(),
'#description' => $this->t('Whenever the event occurs, rule evaluation is triggered.'),
+ '#executes_submit_callback' => array('::submitForm'),
];
+ $form['event_configuration'] = array();
+ if ($values = $form_state->getValue('events')) {
+ $event_name = $values[0]['event_name'];
+ if ($handler = $this->getEventHandler($event_name)) {
+ $form['event_configuration'] = $handler->buildConfigurationForm(array(), $form_state);
+ }
+ }
+
return $form;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function buildEntity(array $form, FormStateInterface $form_state) {
+ $entity = parent::buildEntity($form, $form_state);
+ foreach ($entity->getEventBaseNames() as $event_name) {
+ if ($handler = $this->getEventHandler($event_name)) {
+ $handler->extractConfigurationFormValues($form['event_configuration'], $form_state);
+ $entity->set('configuration', $handler->getConfiguration());
+ $entity->set('events', [['event_name' => $event_name . '--' . $handler->getConfiguration()['bundle']]]);
+ }
+ }
+ return $entity;
+ }
+
/**
* {@inheritdoc}
*/
@@ -86,4 +113,26 @@ public function save(array $form, FormStateInterface $form_state) {
$form_state->setRedirect('entity.rules_reaction_rule.edit_form', ['rules_reaction_rule' => $this->entity->id()]);
}
+ /**
+ * Gets event handler class.
+ *
+ * Currently event handler is available only when the event is configurable.
+ *
+ * @param $event_name
+ * The event base name.
+ * @param array $configuration
+ * The event configuration.
+ *
+ * @return \Drupal\rules\Core\RulesConfigurableEventHandlerInterface|null
+ * The event handler, null if there is no proper handler.
+ */
+ protected function getEventHandler($event_name, $configuration = []) {
+ $event_definition = $this->eventManager->getDefinition($event_name);
+ $handler_class = $event_definition['class'];
+ if (is_subclass_of($handler_class, RulesConfigurableEventHandlerInterface::class)) {
+ $handler = new $handler_class($configuration, $this->eventManager->getEventBaseName($event_name), $event_definition);
+ return $handler;
+ }
+ }
+
}
diff --git a/src/Form/ReactionRuleEditForm.php b/src/Form/ReactionRuleEditForm.php
index 1a876c44..4cf4b8c7 100644
--- a/src/Form/ReactionRuleEditForm.php
+++ b/src/Form/ReactionRuleEditForm.php
@@ -71,12 +71,18 @@ protected function prepareEntity() {
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
- $event_name = $this->entity->getEvent();
- $event_definition = $this->eventManager->getDefinition($event_name);
- $form['event']['#markup'] = $this->t('Event: @label (@name)', [
- '@label' => $event_definition['label'],
- '@name' => $event_name,
- ]);
+ foreach ($this->entity->getEventNames() as $key => $event_name) {
+ $event_base_name = $this->eventManager->getEventBaseName($event_name);
+ $event_definition = $this->eventManager->getDefinition($event_base_name);
+ $form['events'][$key] = [
+ '#type' => 'item',
+ '#title' => $this->t('Events:'),
+ '#markup' => $this->t('@label (@name)', [
+ '@label' => $event_definition['label'],
+ '@name' => $event_name,
+ ]),
+ ];
+ }
$form = $this->rulesUiHandler->getForm()->buildForm($form, $form_state);
return parent::form($form, $form_state);
}
diff --git a/src/Form/RulesComponentFormBase.php b/src/Form/RulesComponentFormBase.php
index ccb8c6dd..a29c7b90 100644
--- a/src/Form/RulesComponentFormBase.php
+++ b/src/Form/RulesComponentFormBase.php
@@ -19,6 +19,10 @@ abstract class RulesComponentFormBase extends EntityForm {
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
+ // Specify the wrapper div used by #ajax.
+ $form['#prefix'] = '
';
+ $form['#suffix'] = '
';
+
$form['settings'] = [
'#type' => 'details',
'#title' => $this->t('Settings'),
@@ -78,4 +82,32 @@ public function exists($id) {
return (bool) $this->entityTypeManager->getStorage($type)->load($id);
}
+ /**
+ * Get default form #ajax properties.
+ *
+ * @param string $effect
+ * (optional) The jQuery effect to use when placing the new HTML (used with
+ * 'wrapper'). Valid options are 'none' (default), 'slide', or 'fade'.
+ *
+ * @return array
+ */
+ public function getDefaultAjax($effect = 'none') {
+ return array(
+ 'callback' => '::reloadForm',
+ 'wrapper' => 'rules-form-wrapper',
+ 'effect' => $effect,
+ 'speed' => 'fast',
+ );
+ }
+
+ /**
+ * Ajax callback to reload the form.
+ *
+ * @return array
+ * The reloaded form.
+ */
+ public function reloadForm(array $form, FormStateInterface $form_state) {
+ return $form;
+ }
+
}
diff --git a/src/Plugin/Condition/DataComparison.php b/src/Plugin/Condition/DataComparison.php
index 27f6cdda..77d0c7db 100644
--- a/src/Plugin/Condition/DataComparison.php
+++ b/src/Plugin/Condition/DataComparison.php
@@ -21,11 +21,10 @@
* label = @Translation("Data to compare"),
* description = @Translation("The data to be checked to be empty, specified by using a data selector, e.g. 'node:uid:entity:name:value'.")
* ),
- * "operator" = @ContextDefinition("string",
+ * "operation" = @ContextDefinition("string",
* label = @Translation("Operator"),
- * description = @Translation("The comparison operator."),
+ * description = @Translation("The comparison operation."),
* default_value = "==",
- * required = FALSE
* ),
* "value" = @ContextDefinition("any",
* label = @Translation("Data value"),
@@ -44,8 +43,8 @@ class DataComparison extends RulesConditionBase {
*
* @param mixed $data
* Supplied data to test.
- * @param string $operator
- * Data comparison operator. Typically one of:
+ * @param string $operation
+ * Data comparison operation. Typically one of:
* - "=="
* - "<"
* - ">"
@@ -57,9 +56,9 @@ class DataComparison extends RulesConditionBase {
* @return bool
* The evaluation of the condition.
*/
- protected function doEvaluate($data, $operator, $value) {
- $operator = $operator ? $operator : '==';
- switch ($operator) {
+ protected function doEvaluate($data, $operation, $value) {
+ $operation = $operation ? $operation : '==';
+ switch ($operation) {
case '<':
return $data < $value;
@@ -82,4 +81,16 @@ protected function doEvaluate($data, $operator, $value) {
}
}
+ /**
+ * {@inheritdoc}
+ */
+ public function refineContextDefinitions(array $selected_data) {
+ if (isset($selected_data['data'])) {
+ $this->pluginDefinition['context']['value']->setDataType($selected_data['data']->getDataType());
+ if ($this->getContextValue('operation') == 'IN') {
+ $this->pluginDefinition['context']['value']->setMultiple();
+ }
+ }
+ }
+
}
diff --git a/src/Plugin/Condition/EntityIsOfBundle.php b/src/Plugin/Condition/EntityIsOfBundle.php
index a8393e02..78de8c35 100644
--- a/src/Plugin/Condition/EntityIsOfBundle.php
+++ b/src/Plugin/Condition/EntityIsOfBundle.php
@@ -20,7 +20,8 @@
* context = {
* "entity" = @ContextDefinition("entity",
* label = @Translation("Entity"),
- * description = @Translation("Specifies the entity for which to evaluate the condition.")
+ * description = @Translation("Specifies the entity for which to evaluate the condition."),
+ * assignment_restriction = "selector",
* ),
* "type" = @ContextDefinition("string",
* label = @Translation("Type"),
@@ -59,4 +60,18 @@ protected function doEvaluate(EntityInterface $entity, $type, $bundle) {
return $entity_bundle == $bundle && $entity_type == $type;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function assertMetadata(array $selected_data) {
+ // Assert the checked bundle.
+ $changed_definitions = [];
+ if (isset($selected_data['entity']) && $bundle = $this->getContextValue('bundle')) {
+ $changed_definitions['entity'] = clone $selected_data['entity'];
+ $bundles = is_array($bundle) ? $bundle : [$bundle];
+ $changed_definitions['entity']->setBundles($bundles);
+ }
+ return $changed_definitions;
+ }
+
}
diff --git a/src/Plugin/RulesAction/EntityCreate.php b/src/Plugin/RulesAction/EntityCreate.php
index fa733b22..aafd465f 100644
--- a/src/Plugin/RulesAction/EntityCreate.php
+++ b/src/Plugin/RulesAction/EntityCreate.php
@@ -38,6 +38,13 @@ class EntityCreate extends RulesActionBase implements ContainerFactoryPluginInte
*/
protected $entityTypeId;
+ /**
+ * The entity bundle key used for the entity type.
+ *
+ * @var string
+ */
+ protected $bundleKey;
+
/**
* Constructs an EntityCreate object.
*
@@ -54,6 +61,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->storage = $storage;
$this->entityTypeId = $plugin_definition['entity_type_id'];
+ $this->bundleKey = $plugin_definition['bundle_key'];
}
/**
@@ -71,11 +79,11 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function refineContextDefinitions(array $selected_data) {
if ($type = $this->entityTypeId) {
$data_type = "entity:$type";
- if ($bundle = $this->getContextValue('bundle')) {
+ if ($this->bundleKey && $bundle = $this->getContextValue($this->bundleKey)) {
$data_type .= ":$bundle";
}
diff --git a/src/Plugin/RulesAction/EntityCreateDeriver.php b/src/Plugin/RulesAction/EntityCreateDeriver.php
index f40c1ab0..18f88425 100644
--- a/src/Plugin/RulesAction/EntityCreateDeriver.php
+++ b/src/Plugin/RulesAction/EntityCreateDeriver.php
@@ -9,11 +9,12 @@
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\ContentEntityTypeInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\rules\Context\ContextDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -83,15 +84,28 @@ public function getDerivativeDefinitions($base_plugin_definition) {
// other required base fields. This matches the storage create() behavior,
// where only the bundle requirement is enforced.
$bundle_key = $entity_type->getKey('bundle');
+ $this->derivatives[$entity_type_id]['bundle_key'] = $bundle_key;
+
$base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
foreach ($base_field_definitions as $field_name => $definition) {
if ($field_name != $bundle_key && !$definition->isRequired()) {
continue;
}
+ $item_definition = $definition->getItemDefinition();
+ $type_definition = $item_definition->getPropertyDefinition($item_definition->getMainPropertyName());
+
+ // If this is an entity reference then we expect the target type as
+ // context.
+ if ($type_definition instanceof DataReferenceDefinitionInterface) {
+ $type_definition->getTargetDefinition();
+ }
+ $type = $type_definition->getDataType();
+
$is_bundle = ($field_name == $bundle_key);
$multiple = ($definition->getCardinality() === 1) ? FALSE : TRUE;
- $context_definition = ContextDefinition::create($definition->getType())
+
+ $context_definition = ContextDefinition::create($type)
->setLabel($definition->getLabel())
->setRequired($is_bundle)
->setMultiple($multiple)
diff --git a/src/Plugin/RulesAction/EntityFetchByField.php b/src/Plugin/RulesAction/EntityFetchByField.php
index 281726fe..8392938d 100644
--- a/src/Plugin/RulesAction/EntityFetchByField.php
+++ b/src/Plugin/RulesAction/EntityFetchByField.php
@@ -90,7 +90,7 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function refineContextDefinitions(array $selected_data) {
if ($type = $this->getContextValue('type')) {
$this->pluginDefinition['provides']['entity_fetched']->setDataType("entity:$type");
}
diff --git a/src/Plugin/RulesAction/EntityFetchById.php b/src/Plugin/RulesAction/EntityFetchById.php
index 9815ad5a..61a23020 100644
--- a/src/Plugin/RulesAction/EntityFetchById.php
+++ b/src/Plugin/RulesAction/EntityFetchById.php
@@ -81,7 +81,7 @@ public static function create(ContainerInterface $container, array $configuratio
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function refineContextDefinitions(array $selected_data) {
if ($type = $this->getContextValue('type')) {
$this->pluginDefinition['provides']['entity_fetched']->setDataType("entity:$type");
}
diff --git a/src/Plugin/RulesAction/EntitySave.php b/src/Plugin/RulesAction/EntitySave.php
index e2743959..1209ad14 100644
--- a/src/Plugin/RulesAction/EntitySave.php
+++ b/src/Plugin/RulesAction/EntitySave.php
@@ -25,7 +25,7 @@
* "immediate" = @ContextDefinition("boolean",
* label = @Translation("Force saving immediately"),
* description = @Translation("Usually saving is postponed till the end of the evaluation, so that multiple saves can be fold into one. If this set, saving is forced to happen immediately."),
- * default_value = NULL,
+ * default_value = FALSE,
* required = FALSE
* )
* }
diff --git a/src/Plugin/RulesAction/RulesComponentAction.php b/src/Plugin/RulesAction/RulesComponentAction.php
new file mode 100644
index 00000000..491e688b
--- /dev/null
+++ b/src/Plugin/RulesAction/RulesComponentAction.php
@@ -0,0 +1,129 @@
+storage = $storage;
+ $this->componentId = $plugin_definition['component_id'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager')->getStorage('rules_component')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute() {
+ $rules_config = $this->storage->load($this->componentId);
+
+ // Setup an isolated execution state for this expression and pass on the
+ // necessary context.
+ $rules_component = $rules_config->getComponent();
+ foreach ($this->getContexts() as $context_name => $context) {
+ // Pass through the already existing typed data objects to avoid creating
+ // them from scratch.
+ $rules_component->getState()->setVariableData($context_name, $context->getContextData());
+ }
+
+ // We don't use RulesComponent::execute() here since we don't want to
+ // auto-save immediately.
+ $state = $rules_component->getState();
+ $expression = $rules_component->getExpression();
+ $expression->executeWithState($state);
+
+ // Postpone auto-saving to the parent expression triggering this action.
+ foreach ($state->getAutoSaveSelectors() as $selector) {
+ $parts = explode('.', $selector);
+ $context_name = reset($parts);
+ if (array_key_exists($context_name, $this->context)) {
+ $this->saveLater[] = $context_name;
+ }
+ else {
+ // Otherwise we need to save here since it will not happen in the parent
+ // execution.
+ $typed_data = $state->fetchDataByPropertyPath($selector);
+ // Things that can be saved must have a save() method, right?
+ // Saving is always done at the root of the typed data tree, for example
+ // on the entity level.
+ $typed_data->getRoot()->getValue()->save();
+ }
+ }
+
+ foreach ($this->getProvidedContextDefinitions() as $name => $definition) {
+ $this->setProvidedValue($name, $state->getVariable($name));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function autoSaveContext() {
+ return $this->saveLater;
+ }
+
+}
diff --git a/src/Plugin/RulesAction/RulesComponentActionDeriver.php b/src/Plugin/RulesAction/RulesComponentActionDeriver.php
new file mode 100644
index 00000000..85329f77
--- /dev/null
+++ b/src/Plugin/RulesAction/RulesComponentActionDeriver.php
@@ -0,0 +1,82 @@
+storage = $storage;
+ $this->expressionManager = $expression_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager')->getStorage('rules_component'),
+ $container->get('plugin.manager.rules_expression')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ $rules_components = $this->storage->loadMultiple();
+ foreach ($rules_components as $rules_component) {
+
+ $component_config = $rules_component->get('component');
+ $expression_definition = $this->expressionManager->getDefinition($component_config['expression']['id']);
+
+ $this->derivatives[$rules_component->id()] = [
+ 'label' => $this->t('@expression_type: @label', [
+ '@expression_type' => $expression_definition['label'],
+ '@label' => $rules_component->label(),
+ ]),
+ 'category' => $this->t('Components'),
+ 'component_id' => $rules_component->id(),
+ 'context' => $rules_component->getContextDefinitions(),
+ 'provides' => $rules_component->getProvidedContextDefinitions(),
+ ] + $base_plugin_definition;
+ }
+
+ return $this->derivatives;
+ }
+
+}
diff --git a/src/Plugin/RulesAction/VariableAdd.php b/src/Plugin/RulesAction/VariableAdd.php
index 3b5e8ff7..94d9be9c 100644
--- a/src/Plugin/RulesAction/VariableAdd.php
+++ b/src/Plugin/RulesAction/VariableAdd.php
@@ -49,8 +49,9 @@ protected function doExecute($type, $value) {
/**
* {@inheritdoc}
*/
- public function refineContextDefinitions() {
+ public function refineContextDefinitions(array $selected_data) {
if ($type = $this->getContextValue('type')) {
+ $this->pluginDefinition['context']['value']->setDataType($type);
$this->pluginDefinition['provides']['variable_added']->setDataType($type);
}
}
diff --git a/src/Plugin/RulesExpression/ActionSet.php b/src/Plugin/RulesExpression/ActionSet.php
index a8470d56..17fef135 100644
--- a/src/Plugin/RulesExpression/ActionSet.php
+++ b/src/Plugin/RulesExpression/ActionSet.php
@@ -21,6 +21,13 @@
*/
class ActionSet extends ActionExpressionContainer {
+ /**
+ * {@inheritdoc}
+ */
+ protected function allowsMetadataAssertions() {
+ return TRUE;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Plugin/RulesExpression/Rule.php b/src/Plugin/RulesExpression/Rule.php
index b5a5b913..10fadcbe 100644
--- a/src/Plugin/RulesExpression/Rule.php
+++ b/src/Plugin/RulesExpression/Rule.php
@@ -103,8 +103,7 @@ public function executeWithState(ExecutionStateInterface $state) {
* {@inheritdoc}
*/
public function addCondition($condition_id, ContextConfig $config = NULL) {
- $this->conditions->addCondition($condition_id, $config);
- return $this;
+ return $this->conditions->addCondition($condition_id, $config);
}
/**
@@ -126,8 +125,7 @@ public function setConditions(ConditionExpressionContainerInterface $conditions)
* {@inheritdoc}
*/
public function addAction($action_id, ContextConfig $config = NULL) {
- $this->actions->addAction($action_id, $config);
- return $this;
+ return $this->actions->addAction($action_id, $config);
}
/**
@@ -215,26 +213,24 @@ public function deleteExpression($uuid) {
/**
* {@inheritdoc}
*/
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
- $violation_list = $this->conditions->checkIntegrity($metadata_state);
- $violation_list->addAll($this->actions->checkIntegrity($metadata_state));
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE) {
+ $violation_list = $this->conditions->checkIntegrity($metadata_state, $apply_assertions);
+ $violation_list->addAll($this->actions->checkIntegrity($metadata_state, $apply_assertions));
return $violation_list;
}
/**
* {@inheritdoc}
*/
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
- if ($until) {
- $found = $this->conditions->prepareExecutionMetadataState($metadata_state, $until);
- if (!$found) {
- $found = $this->actions->prepareExecutionMetadataState($metadata_state, $until);
- }
- return $found;
+ public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL, $apply_assertions = TRUE) {
+ // @todo: If the rule is nested, we may not pass assertions to following
+ // expressions as we do not know whether the rule fires at all. Should we
+ // clone the metadata state to ensure modifications stay local?
+ $found = $this->conditions->prepareExecutionMetadataState($metadata_state, $until, $apply_assertions);
+ if ($found) {
+ return TRUE;
}
- $this->conditions->prepareExecutionMetadataState($metadata_state);
- $this->actions->prepareExecutionMetadataState($metadata_state);
- return TRUE;
+ return $this->actions->prepareExecutionMetadataState($metadata_state, $until, $apply_assertions);
}
/**
diff --git a/src/Plugin/RulesExpression/RulesAction.php b/src/Plugin/RulesExpression/RulesAction.php
index 8ea59fc1..563f179f 100644
--- a/src/Plugin/RulesExpression/RulesAction.php
+++ b/src/Plugin/RulesExpression/RulesAction.php
@@ -7,9 +7,7 @@
namespace Drupal\rules\Plugin\RulesExpression;
-use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\rules\Context\ContextHandlerTrait;
use Drupal\rules\Context\DataProcessorManager;
use Drupal\rules\Core\RulesActionManagerInterface;
use Drupal\rules\Engine\ActionExpressionInterface;
@@ -17,7 +15,7 @@
use Drupal\rules\Engine\ExecutionStateInterface;
use Drupal\rules\Engine\ExpressionBase;
use Drupal\rules\Engine\ExpressionInterface;
-use Drupal\rules\Engine\IntegrityCheckTrait;
+use Drupal\rules\Context\ContextHandlerIntegrityTrait;
use Drupal\rules\Engine\IntegrityViolationList;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -35,8 +33,7 @@
*/
class RulesAction extends ExpressionBase implements ContainerFactoryPluginInterface, ActionExpressionInterface {
- use ContextHandlerTrait;
- use IntegrityCheckTrait;
+ use ContextHandlerIntegrityTrait;
/**
* The action manager used to instantiate the action plugin.
@@ -97,16 +94,7 @@ public function setConfiguration(array $configuration) {
public function executeWithState(ExecutionStateInterface $state) {
$action = $this->actionManager->createInstance($this->configuration['action_id']);
- // We have to forward the context values from our configuration to the
- // action plugin.
- $this->mapContext($action, $state);
-
- $action->refineContextdefinitions();
-
- // Send the context value through configured data processor before executing
- // the action.
- $this->processData($action, $state);
-
+ $this->prepareContext($action, $state);
$action->execute();
$auto_saves = $action->autoSaveContext();
@@ -117,28 +105,7 @@ public function executeWithState(ExecutionStateInterface $state) {
// Now that the action has been executed it can provide additional
// context which we will have to pass back in the evaluation state.
- $this->mapProvidedContext($action, $state);
- }
-
- /**
- * {@inheritdoc}
- */
- public function getContextDefinitions() {
- // Pass up the context definitions from the action plugin.
- $definition = $this->actionManager->getDefinition($this->configuration['action_id']);
- return !empty($definition['context']) ? $definition['context'] : [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getContextDefinition($name) {
- // Pass up the context definitions from the action plugin.
- $definition = $this->actionManager->getDefinition($this->configuration['action_id']);
- if (empty($definition['context'][$name])) {
- throw new ContextException(sprintf("The %s context is not a valid context.", $name));
- }
- return $definition['context'][$name];
+ $this->addProvidedContext($action, $state);
}
/**
@@ -165,7 +132,7 @@ public function getFormHandler() {
/**
* {@inheritdoc}
*/
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE) {
$violation_list = new IntegrityViolationList();
if (empty($this->configuration['action_id'])) {
$violation_list->addViolationWithMessage($this->t('Action plugin ID is missing'), $this->getUuid());
@@ -180,19 +147,29 @@ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state)
$action = $this->actionManager->createInstance($this->configuration['action_id']);
- return $this->doCheckIntegrity($action, $metadata_state);
+ // Prepare and refine the context before checking integrity, such that any
+ // context definition changes are respected while checking.
+ $this->prepareContextWithMetadata($action, $metadata_state);
+ $result = $this->checkContextConfigIntegrity($action, $metadata_state);
+ $this->prepareExecutionMetadataState($metadata_state, NULL, $apply_assertions);
+ return $result;
}
/**
* {@inheritdoc}
*/
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
+ public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL, $apply_assertions = TRUE) {
+ if ($until && $this->getUuid() === $until->getUuid()) {
+ return TRUE;
+ }
$action = $this->actionManager->createInstance($this->configuration['action_id']);
- $this->addProvidedVariablesToExecutionMetadataState($action, $metadata_state);
- if ($until) {
- return FALSE;
+ // Make sure to refine context first, such that possibly refined definitions
+ // of provided context are respected.
+ $this->prepareContextWithMetadata($action, $metadata_state);
+ $this->addProvidedContextDefinitions($action, $metadata_state);
+ if ($apply_assertions) {
+ $this->assertMetadata($action, $metadata_state);
}
- return TRUE;
}
}
diff --git a/src/Plugin/RulesExpression/RulesAnd.php b/src/Plugin/RulesExpression/RulesAnd.php
index 06444105..82efd8f5 100644
--- a/src/Plugin/RulesExpression/RulesAnd.php
+++ b/src/Plugin/RulesExpression/RulesAnd.php
@@ -47,4 +47,13 @@ public function evaluate(ExecutionStateInterface $state) {
return !empty($this->conditions);
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function allowsMetadataAssertions() {
+ // If the AND is not negated, all child-expressions must be executed - thus
+ // assertions can be added it.
+ return !$this->isNegated();
+ }
+
}
diff --git a/src/Plugin/RulesExpression/RulesCondition.php b/src/Plugin/RulesExpression/RulesCondition.php
index c0046538..d0637ac7 100644
--- a/src/Plugin/RulesExpression/RulesCondition.php
+++ b/src/Plugin/RulesExpression/RulesCondition.php
@@ -7,16 +7,15 @@
namespace Drupal\rules\Plugin\RulesExpression;
-use Drupal\Core\Condition\ConditionManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\rules\Context\ContextHandlerTrait;
use Drupal\rules\Context\DataProcessorManager;
+use Drupal\rules\Core\ConditionManager;
use Drupal\rules\Engine\ConditionExpressionInterface;
use Drupal\rules\Engine\ExecutionMetadataStateInterface;
use Drupal\rules\Engine\ExecutionStateInterface;
use Drupal\rules\Engine\ExpressionBase;
use Drupal\rules\Engine\ExpressionInterface;
-use Drupal\rules\Engine\IntegrityCheckTrait;
+use Drupal\rules\Context\ContextHandlerIntegrityTrait;
use Drupal\rules\Engine\IntegrityViolationList;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -34,13 +33,12 @@
*/
class RulesCondition extends ExpressionBase implements ConditionExpressionInterface, ContainerFactoryPluginInterface {
- use ContextHandlerTrait;
- use IntegrityCheckTrait;
+ use ContextHandlerIntegrityTrait;
/**
* The condition manager used to instantiate the condition plugin.
*
- * @var \Drupal\Core\Condition\ConditionManager
+ * @var \Drupal\rules\Core\ConditionManager
*/
protected $conditionManager;
@@ -55,7 +53,7 @@ class RulesCondition extends ExpressionBase implements ConditionExpressionInterf
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
- * @param \Drupal\Core\Condition\ConditionManager $condition_manager
+ * @param \Drupal\rules\Core\ConditionManager $condition_manager
* The condition manager.
* @param \Drupal\rules\Context\DataProcessorManager $processor_manager
* The data processor plugin manager.
@@ -112,21 +110,12 @@ public function executeWithState(ExecutionStateInterface $state) {
'negate' => $this->configuration['negate'],
]);
- // We have to forward the context values from our configuration to the
- // condition plugin.
- $this->mapContext($condition, $state);
-
- $condition->refineContextdefinitions();
-
- // Send the context values through configured data processors before
- // evaluating the condition.
- $this->processData($condition, $state);
-
+ $this->prepareContext($condition, $state);
$result = $condition->evaluate();
// Now that the condition has been executed it can provide additional
// context which we will have to pass back in the evaluation state.
- $this->mapProvidedContext($condition, $state);
+ $this->addProvidedContext($condition, $state);
if ($this->isNegated()) {
$result = !$result;
@@ -138,22 +127,16 @@ public function executeWithState(ExecutionStateInterface $state) {
/**
* {@inheritdoc}
*/
- public function isNegated() {
- return !empty($this->configuration['negate']);
+ public function negate($negate = TRUE) {
+ $this->configuration['negate'] = $negate;
+ return $this;
}
/**
* {@inheritdoc}
*/
- public function getContextDefinitions() {
- if (!isset($this->contextDefinitions)) {
- // Pass up the context definitions from the condition plugin.
- // @todo do not always create plugin instances here, the instance should
- // be reused. Maybe that is what plugin bags are for?
- $condition = $this->conditionManager->createInstance($this->configuration['condition_id']);
- $this->contextDefinitions = $condition->getContextDefinitions();
- }
- return $this->contextDefinitions;
+ public function isNegated() {
+ return !empty($this->configuration['negate']);
}
/**
@@ -180,7 +163,7 @@ public function getFormHandler() {
/**
* {@inheritdoc}
*/
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE) {
$violation_list = new IntegrityViolationList();
if (empty($this->configuration['condition_id'])) {
$violation_list->addViolationWithMessage($this->t('Condition plugin ID is missing'), $this->getUuid());
@@ -196,20 +179,31 @@ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state)
$condition = $this->conditionManager->createInstance($this->configuration['condition_id'], [
'negate' => $this->configuration['negate'],
]);
-
- return $this->doCheckIntegrity($condition, $metadata_state);
+ // Prepare and refine the context before checking integrity, such that any
+ // context definition changes are respected while checking.
+ $this->prepareContextWithMetadata($condition, $metadata_state);
+ $result = $this->checkContextConfigIntegrity($condition, $metadata_state);
+ $this->prepareExecutionMetadataState($metadata_state, NULL, $apply_assertions);
+ return $result;
}
/**
* {@inheritdoc}
*/
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
- $condition = $this->actionManager->createInstance($this->configuration['condition_id']);
- $this->addProvidedVariablesToExecutionMetadataState($condition, $metadata_state);
- if ($until) {
- return FALSE;
+ public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL, $apply_assertions = TRUE) {
+ if ($until && $this->getUuid() === $until->getUuid()) {
+ return TRUE;
+ }
+ $condition = $this->conditionManager->createInstance($this->configuration['condition_id'], [
+ 'negate' => $this->configuration['negate'],
+ ]);
+ // Make sure to refine context first, such that possibly refined definitions
+ // of provided context are respected.
+ $this->prepareContextWithMetadata($condition, $metadata_state);
+ $this->addProvidedContextDefinitions($condition, $metadata_state);
+ if ($apply_assertions && !$this->isNegated()) {
+ $this->assertMetadata($condition, $metadata_state);
}
- return TRUE;
}
}
diff --git a/src/Plugin/RulesExpression/RulesLoop.php b/src/Plugin/RulesExpression/RulesLoop.php
index 6e1efe96..01b275d6 100644
--- a/src/Plugin/RulesExpression/RulesLoop.php
+++ b/src/Plugin/RulesExpression/RulesLoop.php
@@ -11,7 +11,6 @@
use Drupal\rules\Engine\ActionExpressionContainer;
use Drupal\rules\Engine\ExecutionMetadataStateInterface;
use Drupal\rules\Engine\ExecutionStateInterface;
-use Drupal\rules\Engine\ExpressionInterface;
use Drupal\rules\Engine\IntegrityViolationList;
use Drupal\rules\Exception\RulesIntegrityException;
@@ -25,14 +24,22 @@
*/
class RulesLoop extends ActionExpressionContainer {
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ // Default to 'list_item' as variable name for the list item.
+ 'list_item' => 'list_item',
+ ];
+ }
+
/**
* {@inheritdoc}
*/
public function executeWithState(ExecutionStateInterface $state) {
$list_data = $state->fetchDataByPropertyPath($this->configuration['list']);
- // Use a configured list item variable name, otherwise fall back to just
- // 'list_item' as variable name.
- $list_item_name = isset($this->configuration['list_item']) ? $this->configuration['list_item'] : 'list_item';
+ $list_item_name = $this->configuration['list_item'];
foreach ($list_data as $item) {
$state->setVariableData($list_item_name, $item);
@@ -48,7 +55,7 @@ public function executeWithState(ExecutionStateInterface $state) {
/**
* {@inheritdoc}
*/
- public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state) {
+ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state, $apply_assertions = TRUE) {
$violation_list = new IntegrityViolationList();
if (empty($this->configuration['list'])) {
@@ -75,63 +82,50 @@ public function checkIntegrity(ExecutionMetadataStateInterface $metadata_state)
return $violation_list;
}
- if ($list_definition instanceof ListDataDefinitionInterface) {
- $list_item_definition = $list_definition->getItemDefinition();
- $metadata_state->setDataDefinition($list_item_name, $list_item_definition);
-
- $violation_list = parent::checkIntegrity($metadata_state);
-
- // Remove the list item variable after the loop, it is out of scope now.
- $metadata_state->removeDataDefinition($list_item_name);
+ if (!$list_definition instanceof ListDataDefinitionInterface) {
+ $violation_list->addViolationWithMessage($this->t('The data type of list variable %list is not a list.', [
+ '%list' => $this->configuration['list'],
+ ]));
return $violation_list;
}
- $violation_list->addViolationWithMessage($this->t('The data type of list variable %list is not a list.', [
- '%list' => $this->configuration['list'],
- ]));
+ // So far all ok, so continue with checking integrity in contained actions.
+ // The parent implementation will take care of invoking pre/post traversal
+ // metadata state preparations.
+ $violation_list = parent::checkIntegrity($metadata_state, $apply_assertions);
return $violation_list;
}
/**
* {@inheritdoc}
*/
- public function prepareExecutionMetadataState(ExecutionMetadataStateInterface $metadata_state, ExpressionInterface $until = NULL) {
- if ($until && $this->getUuid() === $until->getUuid()) {
- return TRUE;
- }
+ protected function allowsMetadataAssertions() {
+ // As the list can be empty, we cannot ensure child expressions are
+ // executed at all - thus no assertions can be added.
+ return FALSE;
+ }
- $list_item_name = isset($this->configuration['list_item']) ? $this->configuration['list_item'] : 'list_item';
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareExecutionMetadataStateBeforeTraversal(ExecutionMetadataStateInterface $metadata_state) {
try {
$list_definition = $metadata_state->fetchDefinitionByPropertyPath($this->configuration['list']);
$list_item_definition = $list_definition->getItemDefinition();
- $metadata_state->setDataDefinition($list_item_name, $list_item_definition);
+ $metadata_state->setDataDefinition($this->configuration['list_item'], $list_item_definition);
}
catch (RulesIntegrityException $e) {
// Silently eat the exception: we just continue without adding the list
// item definition to the state.
}
+ }
- if ($until) {
- foreach ($this->actions as $action) {
- if ($action->getUuid() === $until->getUuid()) {
- return TRUE;
- }
- $found = $action->prepareExecutionMetadataState($metadata_state, $until);
- if ($found) {
- return TRUE;
- }
- }
- // Remove the list item variable after the loop, it is out of scope now.
- $metadata_state->removeDataDefinition($list_item_name);
- return FALSE;
- }
-
- foreach ($this->actions as $action) {
- $action->prepareExecutionMetadataState($metadata_state);
- }
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareExecutionMetadataStateAfterTraversal(ExecutionMetadataStateInterface $metadata_state) {
// Remove the list item variable after the loop, it is out of scope now.
- $metadata_state->removeDataDefinition($list_item_name);
- return TRUE;
+ $metadata_state->removeDataDefinition($this->configuration['list_item']);
}
}
diff --git a/src/Plugin/RulesExpression/RulesOr.php b/src/Plugin/RulesExpression/RulesOr.php
index bc740109..bacc639f 100644
--- a/src/Plugin/RulesExpression/RulesOr.php
+++ b/src/Plugin/RulesExpression/RulesOr.php
@@ -34,4 +34,13 @@ public function evaluate(ExecutionStateInterface $state) {
return empty($this->conditions);
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function allowsMetadataAssertions() {
+ // We cannot garantuee child expressions are executed, thus we cannot allow
+ // metadata assertions.
+ return FALSE;
+ }
+
}
diff --git a/tests/modules/rules_test_default_component/config/install/rules.component.rules_test_default_component.yml b/tests/modules/rules_test_default_component/config/install/rules.component.rules_test_default_component.yml
index e0c9ca2d..91bff11f 100644
--- a/tests/modules/rules_test_default_component/config/install/rules.component.rules_test_default_component.yml
+++ b/tests/modules/rules_test_default_component/config/install/rules.component.rules_test_default_component.yml
@@ -13,8 +13,11 @@ component:
type: 'entity:user'
label: User
description: 'The user whose mail address to print.'
- provided_context:
- - concatenated
+ provided_context_definitions:
+ concatenated:
+ type: 'string'
+ label: Concatenated text
+ description: 'The concatenated text.'
expression:
id: rules_rule
conditions:
diff --git a/tests/src/Integration/Action/EntityCreateTest.php b/tests/src/Integration/Action/EntityCreateTest.php
index cbb8a75f..41a435bf 100644
--- a/tests/src/Integration/Action/EntityCreateTest.php
+++ b/tests/src/Integration/Action/EntityCreateTest.php
@@ -9,8 +9,11 @@
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\rules\Context\ContextDefinition;
use Drupal\Tests\rules\Integration\RulesEntityIntegrationTestBase;
+use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\rules\Plugin\RulesAction\EntityCreate
@@ -43,23 +46,33 @@ public function setUp() {
$bundle_field_definition_optional = $this->prophesize(BaseFieldDefinition::class);
$bundle_field_definition_required = $this->prophesize(BaseFieldDefinition::class);
+ $property_definition = $this->prophesize(DataDefinitionInterface::class);
+ $property_definition->getDataType()->willReturn('string');
+
+ $item_definition = $this->prophesize(FieldItemDataDefinition::class);
+ $item_definition->getPropertyDefinition(Argument::any())
+ ->willReturn($property_definition->reveal());
+ $item_definition->getMainPropertyName()->willReturn('value');
+
// The next methods are mocked because EntityCreateDeriver executes them,
// and the mocked field definition is instantiated without the necessary
// information.
+ $bundle_field_definition->getItemDefinition()
+ ->willReturn($item_definition->reveal());
$bundle_field_definition->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
- $bundle_field_definition->getType()->willReturn('string')
- ->shouldBeCalledTimes(1);
+ $bundle_field_definition->getType()->willReturn('string');
$bundle_field_definition->getLabel()->willReturn('Bundle')
->shouldBeCalledTimes(1);
$bundle_field_definition->getDescription()
->willReturn('Bundle mock description')
->shouldBeCalledTimes(1);
+ $bundle_field_definition_required->getItemDefinition()
+ ->willReturn($item_definition->reveal());
$bundle_field_definition_required->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
- $bundle_field_definition_required->getType()->willReturn('string')
- ->shouldBeCalledTimes(1);
+ $bundle_field_definition_required->getType()->willReturn('string');
$bundle_field_definition_required->getLabel()->willReturn('Required field')
->shouldBeCalledTimes(1);
$bundle_field_definition_required->getDescription()
@@ -140,7 +153,7 @@ public function testRequiredContexts() {
*/
public function testRefiningContextDefinitions() {
$this->action->setContextValue('bundle', 'bundle_test');
- $this->action->refineContextDefinitions();
+ $this->action->refineContextDefinitions([]);
$this->assertEquals(
$this->action->getProvidedContextDefinition('entity')
->getDataType(), 'entity:test:bundle_test'
diff --git a/tests/src/Integration/Action/EntityFetchByFieldTest.php b/tests/src/Integration/Action/EntityFetchByFieldTest.php
index 4e0aa43f..e101b5ce 100644
--- a/tests/src/Integration/Action/EntityFetchByFieldTest.php
+++ b/tests/src/Integration/Action/EntityFetchByFieldTest.php
@@ -172,7 +172,7 @@ public function testActionExecutionProvidedContextEntityType() {
*/
public function testRefiningContextDefinitions() {
$this->action->setContextValue('type', 'entity_test');
- $this->action->refineContextDefinitions();
+ $this->action->refineContextDefinitions([]);
$this->assertEquals(
$this->action->getProvidedContextDefinition('entity_fetched')
->getDataType(), 'entity:entity_test'
diff --git a/tests/src/Integration/Action/EntityFetchByIdTest.php b/tests/src/Integration/Action/EntityFetchByIdTest.php
index bd040717..bf633e1a 100644
--- a/tests/src/Integration/Action/EntityFetchByIdTest.php
+++ b/tests/src/Integration/Action/EntityFetchByIdTest.php
@@ -74,7 +74,7 @@ public function testActionExecution() {
*/
public function testRefiningContextDefinitions() {
$this->action->setContextValue('type', 'entity_test');
- $this->action->refineContextDefinitions();
+ $this->action->refineContextDefinitions([]);
$this->assertEquals(
$this->action->getProvidedContextDefinition('entity_fetched')
->getDataType(), 'entity:entity_test'
diff --git a/tests/src/Integration/Action/RulesComponentActionTest.php b/tests/src/Integration/Action/RulesComponentActionTest.php
new file mode 100644
index 00000000..67a7f1c8
--- /dev/null
+++ b/tests/src/Integration/Action/RulesComponentActionTest.php
@@ -0,0 +1,198 @@
+rulesExpressionManager->createRule();
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($rule);
+
+ $this->prophesizeStorage([$rules_config]);
+
+ $definition = $this->actionManager->getDefinition('rules_component:test_rule');
+ $this->assertEquals('Components', $definition['category']);
+ $this->assertEquals('Rule: Test rule', (string) $definition['label']);
+ }
+
+ /**
+ * Tests that the execution of the action invokes the Rules component.
+ */
+ public function testExecute() {
+ // Set up a rules component that will just save an entity.
+ $nested_rule = $this->rulesExpressionManager->createRule();
+ $nested_rule->addAction('rules_entity_save', ContextConfig::create()
+ ->map('entity', 'entity')
+ );
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($nested_rule);
+ $rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity')]);
+
+ $this->prophesizeStorage([$rules_config]);
+
+ // Invoke the rules component in another rule.
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule->addAction('rules_component:test_rule', ContextConfig::create()
+ ->map('entity', 'entity')
+ );
+
+ // The call to save the entity means that the action was executed.
+ $entity = $this->prophesizeEntity(EntityInterface::class);
+ $entity->save()->shouldBeCalledTimes(1);
+
+ RulesComponent::create($rule)
+ ->addContextDefinition('entity', ContextDefinition::create('entity'))
+ ->setContextValue('entity', $entity->reveal())
+ ->execute();
+ }
+
+ /**
+ * Tests that context definitions are available on the derived action.
+ */
+ public function testContextDefinitions() {
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule
+ ->addAction('rules_entity_save', ContextConfig::create()
+ ->map('entity', 'entity')
+ )
+ ->addAction('rules_test_string', ContextConfig::create()
+ ->setValue('text', 'x')
+ );
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($rule);
+
+ $context_definitions = ['entity' => ContextDefinition::create('entity')];
+ $rules_config->setContextDefinitions($context_definitions);
+ $provided_definitions = ['concatenated' => ContextDefinition::create('string')];
+ $rules_config->setProvidedContextDefinitions($provided_definitions);
+
+ $this->prophesizeStorage([$rules_config]);
+
+ $definition = $this->actionManager->getDefinition('rules_component:test_rule');
+ $this->assertEquals($context_definitions, $definition['context']);
+ $this->assertEquals($provided_definitions, $definition['provides']);
+ }
+
+ /**
+ * Tests that a rules component in an action can also provide variables.
+ */
+ public function testExecutionProvidedVariables() {
+ // Create a rule that produces a provided string variable.
+ $nested_rule = $this->rulesExpressionManager->createRule();
+ $nested_rule->addAction('rules_test_string', ContextConfig::create()
+ ->setValue('text', 'x')
+ );
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($nested_rule);
+ $rules_config->setProvidedContextDefinitions(['concatenated' => ContextDefinition::create('string')]);
+
+ $this->prophesizeStorage([$rules_config]);
+
+ // Invoke the rules component in another rule.
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule->addAction('rules_component:test_rule');
+
+ $result = RulesComponent::create($rule)
+ ->provideContext('concatenated')
+ ->execute();
+
+ $this->assertEquals('xx', $result['concatenated']);
+ }
+
+ /**
+ * Tests that auto saving is only triggered once with nested components.
+ */
+ public function testAutosaveOnlyOnce() {
+ $entity = $this->prophesizeEntity(EntityInterface::class);
+
+ $nested_rule = $this->rulesExpressionManager->createRule();
+ $nested_rule->addAction('rules_entity_save', ContextConfig::create()
+ ->map('entity', 'entity')
+ );
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($nested_rule);
+ $rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity')]);
+
+ $this->prophesizeStorage([$rules_config]);
+
+ // Create a rule with a nested rule. Overall there are 2 actions to set the
+ // entity then.
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule->addAction('rules_component:test_rule', ContextConfig::create()
+ ->map('entity', 'entity')
+ );
+ $rule->addAction('rules_entity_save', ContextConfig::create()
+ ->map('entity', 'entity')
+ );
+
+ // Auto-saving should only be triggered once on the entity.
+ $entity->save()->shouldBeCalledTimes(1);
+
+ RulesComponent::create($rule)
+ ->addContextDefinition('entity', ContextDefinition::create('entity'))
+ ->setContextValue('entity', $entity->reveal())
+ ->execute();
+ }
+
+ /**
+ * Prepares a mocked entity storage that returns the provided Rules configs.
+ *
+ * @param RulesComponentConfig[] $rules_configs
+ * The Rules componentn config entities that should be returned.
+ */
+ protected function prophesizeStorage($rules_configs) {
+ $storage = $this->prophesize(ConfigEntityStorageInterface::class);
+ $keyed_configs = [];
+
+ foreach ($rules_configs as $rules_config) {
+ $keyed_configs[$rules_config->id()] = $rules_config;
+ $storage->load($rules_config->id())->willReturn($rules_config);
+ }
+
+ $storage->loadMultiple(NULL)->willReturn($keyed_configs);
+ $this->entityTypeManager->getStorage('rules_component')->willReturn($storage->reveal());
+ }
+
+}
diff --git a/tests/src/Integration/Action/VariableAddTest.php b/tests/src/Integration/Action/VariableAddTest.php
index f96c55ec..32c71825 100644
--- a/tests/src/Integration/Action/VariableAddTest.php
+++ b/tests/src/Integration/Action/VariableAddTest.php
@@ -34,10 +34,12 @@ public function testExecute() {
$action = $this->actionManager->createInstance('rules_variable_add');
$action->setContextValue('type', 'string');
$action->setContextValue('value', $variable);
+ $action->refineContextDefinitions([]);
$action->execute();
$result = $action->getProvidedContext('variable_added');
$this->assertEquals($variable, $result->getContextValue());
+ $this->assertEquals('string', $result->getContextDefinition()->getDataType());
}
}
diff --git a/tests/src/Integration/Condition/DataComparisonTest.php b/tests/src/Integration/Condition/DataComparisonTest.php
index 6310ba0d..faf1711f 100644
--- a/tests/src/Integration/Condition/DataComparisonTest.php
+++ b/tests/src/Integration/Condition/DataComparisonTest.php
@@ -7,6 +7,7 @@
namespace Drupal\Tests\rules\Integration\Condition;
+use Drupal\Core\TypedData\DataDefinition;
use Drupal\Tests\rules\Integration\RulesIntegrationTestBase;
/**
@@ -55,76 +56,76 @@ public function testConditionEvaluationOperatorEquals() {
// is '==', TRUE is returned.
$this->condition
->setContextValue('data', 'Llama')
- ->setContextValue('operator', '==')
+ ->setContextValue('operation', '==')
->setContextValue('value', 'Llama');
$this->assertTrue($this->condition->evaluate());
// Test that when the data string does not equal the value string and the
- // operator is '==', FALSE is returned.
+ // operation is '==', FALSE is returned.
$this->condition
->setContextValue('data', 'Kitten')
- ->setContextValue('operator', '==')
+ ->setContextValue('operation', '==')
->setContextValue('value', 'Llama');
$this->assertFalse($this->condition->evaluate());
- // Test that when both data and value are false booleans and the operator
+ // Test that when both data and value are false booleans and the operation
// is '==', TRUE is returned.
$this->condition
->setContextValue('data', FALSE)
- ->setContextValue('operator', '==')
+ ->setContextValue('operation', '==')
->setContextValue('value', FALSE);
$this->assertTrue($this->condition->evaluate());
// Test that when a boolean data does not equal a boolean value
- // and the operator is '==', FALSE is returned.
+ // and the operation is '==', FALSE is returned.
$this->condition
->setContextValue('data', TRUE)
- ->setContextValue('operator', '==')
+ ->setContextValue('operation', '==')
->setContextValue('value', FALSE);
$this->assertFalse($this->condition->evaluate());
}
/**
- * Tests evaluating the condition with the "contains" operator.
+ * Tests evaluating the condition with the "contains" operation.
*
* @covers ::evaluate
*/
public function testConditionEvaluationOperatorContains() {
// Test that when the data string contains the value string, and the
- // operator is 'CONTAINS', TRUE is returned.
+ // operation is 'CONTAINS', TRUE is returned.
$this->condition
->setContextValue('data', 'Big Llama')
- ->setContextValue('operator', 'contains')
+ ->setContextValue('operation', 'contains')
->setContextValue('value', 'Llama');
$this->assertTrue($this->condition->evaluate());
// Test that when the data string does not contain the value string, and
- // the operator is 'contains', TRUE is returned.
+ // the operation is 'contains', TRUE is returned.
$this->condition
->setContextValue('data', 'Big Kitten')
- ->setContextValue('operator', 'contains')
+ ->setContextValue('operation', 'contains')
->setContextValue('value', 'Big Kitten');
$this->assertTrue($this->condition->evaluate());
- // Test that when a data array contains the value string, and the operator
+ // Test that when a data array contains the value string, and the operation
// is 'CONTAINS', TRUE is returned.
$this->condition
->setContextValue('data', ['Llama', 'Kitten'])
- ->setContextValue('operator', 'contains')
+ ->setContextValue('operation', 'contains')
->setContextValue('value', 'Llama');
$this->assertTrue($this->condition->evaluate());
// Test that when a data array does not contain the value array, and the
- // operator is 'CONTAINS', TRUE is returned.
+ // operation is 'CONTAINS', TRUE is returned.
$this->condition
->setContextValue('data', ['Kitten'])
- ->setContextValue('operator', 'contains')
+ ->setContextValue('operation', 'contains')
->setContextValue('value', ['Llama']);
$this->assertFalse($this->condition->evaluate());
}
/**
- * Tests evaluating the condition with the "IN" operator.
+ * Tests evaluating the condition with the "IN" operation.
*
* @covers ::evaluate
*/
@@ -132,61 +133,61 @@ public function testConditionEvaluationOperatorIn() {
// Test that when the data string is 'IN' the value array, TRUE is returned.
$this->condition
->setContextValue('data', 'Llama')
- ->setContextValue('operator', 'IN')
+ ->setContextValue('operation', 'IN')
->setContextValue('value', ['Llama', 'Kitten']);
$this->assertTrue($this->condition->evaluate());
- // Test that when the data array is not in the value array, and the operator
- // is 'IN', FALSE is returned.
+ // Test that when the data array is not in the value array, and the
+ // operation is 'IN', FALSE is returned.
$this->condition
->setContextValue('data', ['Llama'])
- ->setContextValue('operator', 'IN')
+ ->setContextValue('operation', 'IN')
->setContextValue('value', ['Kitten']);
$this->assertFalse($this->condition->evaluate());
}
/**
- * Tests evaluating the condition with the "is less than" operator.
+ * Tests evaluating the condition with the "is less than" operation.
*
* @covers ::evaluate
*/
public function testConditionEvaluationOperatorLessThan() {
- // Test that when data is less than value and operator is '<',
+ // Test that when data is less than value and operation is '<',
// TRUE is returned.
$this->condition
->setContextValue('data', 1)
- ->setContextValue('operator', '<')
+ ->setContextValue('operation', '<')
->setContextValue('value', 2);
$this->assertTrue($this->condition->evaluate());
- // Test that when data is greater than value and operator is '<',
+ // Test that when data is greater than value and operation is '<',
// FALSE is returned.
$this->condition
->setContextValue('data', 2)
- ->setContextValue('operator', '<')
+ ->setContextValue('operation', '<')
->setContextValue('value', 1);
$this->assertFalse($this->condition->evaluate());
}
/**
- * Tests evaluating the condition with the "is greater than" operator.
+ * Tests evaluating the condition with the "is greater than" operation.
*
* @covers ::evaluate
*/
public function testConditionEvaluationOperatorGreaterThan() {
- // Test that when data is greater than value and operator is '>',
+ // Test that when data is greater than value and operation is '>',
// TRUE is returned.
$this->condition
->setContextValue('data', 2)
- ->setContextValue('operator', '>')
+ ->setContextValue('operation', '>')
->setContextValue('value', 1);
$this->assertTrue($this->condition->evaluate());
- // Test that when data is less than value and operator is '>',
+ // Test that when data is less than value and operation is '>',
// FALSE is returned.
$this->condition
->setContextValue('data', 1)
- ->setContextValue('operator', '>')
+ ->setContextValue('operation', '>')
->setContextValue('value', 2);
$this->assertFalse($this->condition->evaluate());
}
@@ -200,4 +201,23 @@ public function testSummary() {
$this->assertEquals('Data comparison', $this->condition->summary());
}
+ /**
+ * @covers ::refineContextDefinitions
+ */
+ public function testRefineContextDefinitions() {
+ // When a string is selected for comparison, the value must be string also.
+ $this->condition->refineContextDefinitions([
+ 'data' => DataDefinition::create('string'),
+ ]);
+ $this->assertEquals('string', $this->condition->getContextDefinition('value')->getDataType());
+
+ // IN operation requires a list of strings as value.
+ $this->condition->setContextValue('operation', 'IN');
+ $this->condition->refineContextDefinitions([
+ 'data' => DataDefinition::create('string'),
+ ]);
+ $this->assertEquals('string', $this->condition->getContextDefinition('value')->getDataType());
+ $this->assertTrue($this->condition->getContextDefinition('value')->isMultiple());
+ }
+
}
diff --git a/tests/src/Integration/Engine/IntegrityCheckTest.php b/tests/src/Integration/Engine/IntegrityCheckTest.php
index 4466834a..ebb731ae 100644
--- a/tests/src/Integration/Engine/IntegrityCheckTest.php
+++ b/tests/src/Integration/Engine/IntegrityCheckTest.php
@@ -166,7 +166,7 @@ public function testInvalidProvidedName() {
}
/**
- * Tests the input restrction on contexts.
+ * Tests the input restriction on contexts.
*/
public function testInputRestriction() {
$rule = $this->rulesExpressionManager->createRule();
@@ -235,7 +235,7 @@ public function testPrimitiveTypeViolation() {
->checkIntegrity();
$this->assertEquals(1, iterator_count($violation_list));
$this->assertEquals(
- 'Expected a primitive data type for context Text to compare but got a list data type instead.',
+ 'Expected a string data type for context Text to compare but got a list data type instead.',
(string) $violation_list[0]->getMessage()
);
$this->assertEquals($condition->getUuid(), $violation_list[0]->getUuid());
@@ -287,7 +287,7 @@ public function testComplexTypeViolation() {
->checkIntegrity();
$this->assertEquals(1, iterator_count($violation_list));
$this->assertEquals(
- 'Expected a complex data type for context Node but got a list data type instead.',
+ 'Expected a entity:node data type for context Node but got a list data type instead.',
(string) $violation_list[0]->getMessage()
);
$this->assertEquals($condition->getUuid(), $violation_list[0]->getUuid());
@@ -299,7 +299,7 @@ public function testComplexTypeViolation() {
public function testMissingRequiredContext() {
$rule = $this->rulesExpressionManager->createRule();
- // The condition is completely unconfigured, missing 2 required contexts.
+ // The condition is completely un-configured, missing 2 required contexts.
$condition = $this->rulesExpressionManager->createCondition('rules_node_is_of_type');
$rule->addExpressionObject($condition);
@@ -339,4 +339,87 @@ public function testNestedExpressionUuids() {
$this->assertEquals($action->getUuid(), $violation_list[0]->getUuid());
}
+ /**
+ * Tests using provided variables in sub-sequent actions passes checks.
+ */
+ public function testUsingProvidedVariables() {
+ $rule = $this->rulesExpressionManager->createRule();
+
+ $rule->addAction('rules_variable_add', ContextConfig::create()
+ ->setValue('type', 'any')
+ ->setValue('value', 'foo')
+ );
+ $rule->addAction('rules_variable_add', ContextConfig::create()
+ ->setValue('type', 'any')
+ ->map('value', 'variable_added')
+ );
+
+ $violation_list = RulesComponent::create($rule)
+ ->checkIntegrity();
+ $this->assertEquals(0, iterator_count($violation_list));
+ }
+
+ /**
+ * Tests that refined context is respected when checking context.
+ */
+ public function testRefinedContextViolation() {
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule->addAction('rules_variable_add', ContextConfig::create()
+ ->setValue('type', 'integer')
+ ->map('value', 'text')
+ );
+
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('text', ContextDefinition::create('string'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+ }
+
+ /**
+ * Tests context can be refined based upon mapped context.
+ */
+ public function testRefiningContextBasedonMappedContext() {
+ // DataComparision condition refines context based on selected data. Thus
+ // it for the test and ensure checking integrity passes when the comparison
+ // value is of a compatible type and fails else.
+ $rule = $this->rulesExpressionManager->createRule();
+ $rule->addCondition('rules_data_comparison', ContextConfig::create()
+ ->map('data', 'text')
+ ->map('value', 'text2')
+ );
+
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('text', ContextDefinition::create('string'))
+ ->addContextDefinition('text2', ContextDefinition::create('string'))
+ ->checkIntegrity();
+ $this->assertEquals(0, iterator_count($violation_list));
+
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('text', ContextDefinition::create('string'))
+ ->addContextDefinition('text2', ContextDefinition::create('integer'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+ }
+
+ /**
+ * Tests using provided variables with refined context.
+ */
+ public function testUsingRefinedProvidedVariables() {
+ $rule = $this->rulesExpressionManager->createRule();
+
+ $rule->addAction('rules_variable_add', ContextConfig::create()
+ ->setValue('type', 'string')
+ ->setValue('value', 'foo')
+ );
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'variable_added')
+ ->setValue('type', 'status')
+ );
+ // The message action requires a string, thus if the context is not refined
+ // it will end up as "any" and integrity check would fail.
+ $violation_list = RulesComponent::create($rule)
+ ->checkIntegrity();
+ $this->assertEquals(0, iterator_count($violation_list));
+ }
+
}
diff --git a/tests/src/Integration/Engine/PrepareExecutionMetadataStateTest.php b/tests/src/Integration/Engine/PrepareExecutionMetadataStateTest.php
index a415c67d..de34d5bb 100644
--- a/tests/src/Integration/Engine/PrepareExecutionMetadataStateTest.php
+++ b/tests/src/Integration/Engine/PrepareExecutionMetadataStateTest.php
@@ -34,7 +34,7 @@ public function testAddingVariable() {
$state = ExecutionMetadataState::create();
$found = $rule->prepareExecutionMetadataState($state);
$this->assertTrue($state->hasDataDefinition('result'));
- $this->assertTrue($found);
+ $this->assertNull($found);
}
/**
@@ -113,7 +113,7 @@ public function testPrepareAfterLoop() {
$found = $rule->prepareExecutionMetadataState($state);
$this->assertFalse($state->hasDataDefinition('list_item'));
- $this->assertTrue($found);
+ $this->assertNull($found);
}
}
diff --git a/tests/src/Integration/RulesIntegrationTestBase.php b/tests/src/Integration/RulesIntegrationTestBase.php
index dccd21c7..b31b72ee 100644
--- a/tests/src/Integration/RulesIntegrationTestBase.php
+++ b/tests/src/Integration/RulesIntegrationTestBase.php
@@ -11,6 +11,7 @@
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
@@ -204,6 +205,11 @@ public function setUp() {
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityTypeManager->getDefinitions()->willReturn([]);
+ // Setup a rules_component storage mock which returns nothing by default.
+ $storage = $this->prophesize(ConfigEntityStorageInterface::class);
+ $storage->loadMultiple(NULL)->willReturn([]);
+ $this->entityTypeManager->getStorage('rules_component')->willReturn($storage->reveal());
+
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
$this->entityFieldManager->getBaseFieldDefinitions()->willReturn([]);
diff --git a/tests/src/Kernel/ConfigurableEventHandlerTest.php b/tests/src/Kernel/ConfigurableEventHandlerTest.php
index 28c2bfa3..57eb1534 100644
--- a/tests/src/Kernel/ConfigurableEventHandlerTest.php
+++ b/tests/src/Kernel/ConfigurableEventHandlerTest.php
@@ -93,9 +93,11 @@ public function testConfigurableEventHandler() {
);
$config_entity1 = $this->storage->create([
'id' => 'test_rule1',
- 'expression_id' => 'rules_rule',
- 'event' => 'rules_entity_presave:node--page',
- ])->setExpression($rule1);
+ ]);
+ $config_entity1->set('events', [
+ ['event_name' => 'rules_entity_presave:node--page'],
+ ]);
+ $config_entity1->set('expression', $rule1->getConfiguration());
$config_entity1->save();
// Create rule2 with the 'rules_entity_presave:node' event.
@@ -106,9 +108,11 @@ public function testConfigurableEventHandler() {
);
$config_entity2 = $this->storage->create([
'id' => 'test_rule2',
- 'expression_id' => 'rules_rule',
- 'event' => 'rules_entity_presave:node',
- ])->setExpression($rule2);
+ ]);
+ $config_entity2->set('events', [
+ ['event_name' => 'rules_entity_presave:node'],
+ ]);
+ $config_entity2->set('expression', $rule2->getConfiguration());
$config_entity2->save();
// The logger instance has changed, refresh it.
diff --git a/tests/src/Kernel/CoreIntegrationTest.php b/tests/src/Kernel/CoreIntegrationTest.php
index 64293ab3..bb9c3365 100644
--- a/tests/src/Kernel/CoreIntegrationTest.php
+++ b/tests/src/Kernel/CoreIntegrationTest.php
@@ -7,9 +7,11 @@
namespace Drupal\Tests\rules\Kernel;
+use Drupal\node\Entity\Node;
use Drupal\rules\Context\ContextConfig;
use Drupal\rules\Context\ContextDefinition;
use Drupal\rules\Engine\RulesComponent;
+use Drupal\rules\Entity\RulesComponentConfig;
use Drupal\user\Entity\User;
/**
@@ -244,6 +246,45 @@ public function testDataSetEntities() {
$this->assertNotNull($node->id(), 'Node ID is set, which means that the node has been auto-saved.');
}
+ /**
+ * Tests that auto saving in a component executed as action works.
+ */
+ public function testComponentActionAutoSave() {
+ $entity_type_manager = $this->container->get('entity_type.manager');
+ $entity_type_manager->getStorage('node_type')
+ ->create(['type' => 'page'])
+ ->save();
+
+ $nested_rule = $this->expressionManager->createRule();
+ // Create a node entity with the action.
+ $nested_rule->addAction('rules_entity_create:node', ContextConfig::create()
+ ->setValue('type', 'page')
+ );
+ // Set the title of the new node so that it is marked for auto-saving.
+ $nested_rule->addAction('rules_data_set', ContextConfig::create()
+ ->map('data', 'entity.title')
+ ->setValue('value', 'new title')
+ );
+
+ $rules_config = new RulesComponentConfig([
+ 'id' => 'test_rule',
+ 'label' => 'Test rule',
+ ], 'rules_component');
+ $rules_config->setExpression($nested_rule);
+ $rules_config->save();
+
+ // Invoke the rules component in another rule.
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('rules_component:test_rule');
+
+ RulesComponent::create($rule)->execute();
+
+ $nodes = Node::loadMultiple();
+ $node = reset($nodes);
+ $this->assertEquals('new title', $node->getTitle());
+ $this->assertNotNull($node->id(), 'Node ID is set, which means that the node has been auto-saved.');
+ }
+
/**
* Tests using global context.
*/
diff --git a/tests/src/Kernel/Engine/MetadataAssertionTest.php b/tests/src/Kernel/Engine/MetadataAssertionTest.php
new file mode 100644
index 00000000..6cea3a66
--- /dev/null
+++ b/tests/src/Kernel/Engine/MetadataAssertionTest.php
@@ -0,0 +1,171 @@
+installSchema('system', ['sequences']);
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('node');
+ $this->installConfig(['field']);
+
+ $entity_type_manager = $this->container->get('entity_type.manager');
+ $entity_type_manager->getStorage('node_type')
+ ->create(['type' => 'page'])
+ ->save();
+
+ FieldStorageConfig::create([
+ 'field_name' => 'field_text',
+ 'type' => 'string',
+ 'entity_type' => 'node',
+ 'cardinality' => 1,
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field_text',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ ])->save();
+ }
+
+ /**
+ * Tests asserting metadata using the EntityIfOfBundle condition.
+ */
+ public function testAssertingEntityBundle() {
+ // When trying to use the field_text field without knowledge of the bundle,
+ // the field is not available.
+ $rule = $this->expressionManager->createRule();
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'node.field_text.value')
+ ->setValue('type', 'status')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+ $this->assertEquals(
+ 'Data selector %selector for context %context_name is invalid. @message',
+ $violation_list->get(0)->getMessage()->getUntranslatedString()
+ );
+
+ // Now add the EntityIsOfBundle condition and try again.
+ $rule->addCondition('rules_entity_is_of_bundle', ContextConfig::create()
+ ->map('entity', 'node')
+ ->setValue('type', 'node')
+ ->setValue('bundle', 'page')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(0, iterator_count($violation_list));
+ }
+
+ /**
+ * Tests asserted metadata is handled correctly in OR and AND containers.
+ */
+ public function testAssertingWithLogicalOperations() {
+ // Add an nested AND and make sure it keeps working.
+ $rule = $this->expressionManager->createRule();
+ $and = $this->expressionManager->createAnd();
+ $and->addCondition('rules_entity_is_of_bundle', ContextConfig::create()
+ ->map('entity', 'node')
+ ->setValue('type', 'node')
+ ->setValue('bundle', 'page')
+ );
+ $rule->addExpressionObject($and);
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'node.field_text.value')
+ ->setValue('type', 'status')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(0, iterator_count($violation_list));
+
+ // Add an nested OR and make sure it is ignored.
+ $rule = $this->expressionManager->createRule();
+ $or = $this->expressionManager->createOr();
+ $or->addCondition('rules_entity_is_of_bundle', ContextConfig::create()
+ ->map('entity', 'node')
+ ->setValue('type', 'node')
+ ->setValue('bundle', 'page')
+ );
+ $rule->addExpressionObject($or);
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'node.field_text.value')
+ ->setValue('type', 'status')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+ }
+
+ /**
+ * Tests asserted metadata of negated conditions is ignored.
+ */
+ public function testAssertingOfNegatedConditions() {
+ // Negate the condition only and make sure it is ignored.
+ $rule = $this->expressionManager->createRule();
+ $rule->addCondition('rules_entity_is_of_bundle', ContextConfig::create()
+ ->map('entity', 'node')
+ ->setValue('type', 'node')
+ ->setValue('bundle', 'page')
+ )->negate(TRUE);
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'node.field_text.value')
+ ->setValue('type', 'status')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+
+ // Add an negated AND and make sure it is ignored.
+ $rule = $this->expressionManager->createRule();
+ $and = $this->expressionManager->createAnd();
+ $and->addCondition('rules_entity_is_of_bundle', ContextConfig::create()
+ ->map('entity', 'node')
+ ->setValue('type', 'node')
+ ->setValue('bundle', 'page')
+ );
+ $and->negate(TRUE);
+ $rule->addExpressionObject($and);
+ $rule->addAction('rules_system_message', ContextConfig::create()
+ ->map('message', 'node.field_text.value')
+ ->setValue('type', 'status')
+ );
+ $violation_list = RulesComponent::create($rule)
+ ->addContextDefinition('node', ContextDefinition::create('entity:node'))
+ ->checkIntegrity();
+ $this->assertEquals(1, iterator_count($violation_list));
+ }
+
+}
diff --git a/tests/src/Kernel/EventIntegrationTest.php b/tests/src/Kernel/EventIntegrationTest.php
index d1dff4f4..7bc7e5d3 100644
--- a/tests/src/Kernel/EventIntegrationTest.php
+++ b/tests/src/Kernel/EventIntegrationTest.php
@@ -53,7 +53,7 @@ public function testUserLoginEvent() {
$config_entity = $this->storage->create([
'id' => 'test_rule',
- 'event' => 'rules_user_login',
+ 'events' => [['event_name' => 'rules_user_login']],
'expression' => $rule->getConfiguration(),
]);
$config_entity->save();
@@ -79,7 +79,7 @@ public function testUserLogoutEvent() {
$config_entity = $this->storage->create([
'id' => 'test_rule',
- 'event' => 'rules_user_logout',
+ 'events' => [['event_name' => 'rules_user_logout']],
'expression' => $rule->getConfiguration(),
]);
$config_entity->save();
@@ -105,7 +105,7 @@ public function testCronEvent() {
$config_entity = $this->storage->create([
'id' => 'test_rule',
- 'event' => 'rules_system_cron',
+ 'events' => [['event_name' => 'rules_system_cron']],
'expression' => $rule->getConfiguration(),
]);
$config_entity->save();
@@ -130,7 +130,7 @@ public function testSystemLoggerEvent() {
$config_entity = $this->storage->create([
'id' => 'test_rule',
- 'event' => 'rules_system_logger_event',
+ 'events' => [['event_name' => 'rules_system_logger_event']],
'expression' => $rule->getConfiguration(),
]);
$config_entity->save();
@@ -156,7 +156,7 @@ public function testInitEvent() {
$config_entity = $this->storage->create([
'id' => 'test_rule',
- 'event' => KernelEvents::REQUEST,
+ 'events' => [['event_name' => KernelEvents::REQUEST]],
'expression' => $rule->getConfiguration(),
]);
$config_entity->save();
@@ -180,4 +180,37 @@ public function testInitEvent() {
$this->assertRulesLogEntryExists('action called');
}
+ /**
+ * Test that rules config supports multiple events.
+ */
+ public function testMultipleEvents() {
+ $rule = $this->expressionManager->createRule();
+ $rule->addCondition('rules_test_true');
+ $rule->addAction('rules_test_log');
+
+ $config_entity = $this->storage->create([
+ 'id' => 'test_rule',
+ ]);
+ $config_entity->set('events', [
+ ['event_name' => 'rules_user_login'],
+ ['event_name' => 'rules_user_logout'],
+ ]);
+ $config_entity->set('expression', $rule->getConfiguration());
+ $config_entity->save();
+
+ // The logger instance has changed, refresh it.
+ $this->logger = $this->container->get('logger.channel.rules');
+
+ $account = User::create(['name' => 'test_user']);
+ // Invoke the hook manually which should trigger the rules_user_login event.
+ rules_user_login($account);
+ // Invoke the hook manually which should trigger the rules_user_logout
+ // event.
+ rules_user_logout($account);
+
+ // Test that the action in the rule logged something.
+ $this->assertRulesLogEntryExists('action called');
+ $this->assertRulesLogEntryExists('action called', 1);
+ }
+
}
diff --git a/tests/src/Unit/ContextHandlerTraitTest.php b/tests/src/Unit/ContextHandlerTraitTest.php
index 442c6c36..f45f50d4 100644
--- a/tests/src/Unit/ContextHandlerTraitTest.php
+++ b/tests/src/Unit/ContextHandlerTraitTest.php
@@ -22,7 +22,7 @@ class ContextHandlerTraitTest extends RulesUnitTestBase {
/**
* Tests that a missing required context triggers an exception.
*
- * @covers ::mapContext
+ * @covers ::prepareContext
*
* @expectedException \Drupal\rules\Exception\RulesEvaluationException
*
@@ -43,13 +43,16 @@ public function testMissingContext() {
$plugin->getContextDefinitions()
->willReturn(['test' => $context_definition->reveal()])
->shouldBeCalled(1);
+ $plugin->getContextValue('test')
+ ->willReturn(NULL)
+ ->shouldBeCalled(1);
$plugin->getPluginId()->willReturn('testplugin')->shouldBeCalledTimes(1);
$state = $this->prophesize(ExecutionStateInterface::class);
// Make the 'mapContext' method visible.
$reflection = new \ReflectionClass($trait);
- $method = $reflection->getMethod('mapContext');
+ $method = $reflection->getMethod('prepareContext');
$method->setAccessible(TRUE);
$method->invokeArgs($trait, [$plugin->reveal(), $state->reveal()]);
}
diff --git a/tests/src/Unit/RulesConditionTest.php b/tests/src/Unit/RulesConditionTest.php
index 262b8fb6..ec1d1533 100644
--- a/tests/src/Unit/RulesConditionTest.php
+++ b/tests/src/Unit/RulesConditionTest.php
@@ -7,7 +7,7 @@
namespace Drupal\Tests\rules\Unit;
-use Drupal\Core\Condition\ConditionManager;
+use Drupal\rules\Core\ConditionManager;
use Drupal\Core\Plugin\Context\ContextDefinitionInterface;
use Drupal\rules\Context\DataProcessorInterface;
use Drupal\rules\Context\ContextConfig;
@@ -28,7 +28,7 @@ class RulesConditionTest extends UnitTestCase {
/**
* The mocked condition manager.
*
- * @var \Drupal\Core\Condition\ConditionManager|\Prophecy\Prophecy\ProphecyInterface
+ * @var \Drupal\rules\Core\ConditionManager|\Prophecy\Prophecy\ProphecyInterface
*/
protected $conditionManager;
@@ -73,22 +73,6 @@ public function setUp() {
$this->conditionManager->reveal(), $this->processorManager->reveal());
}
- /**
- * Tests that context definitions are retrieved form the plugin.
- */
- public function testContextDefinitions() {
- $context_definition = $this->prophesize(ContextDefinitionInterface::class);
- $this->trueCondition->getContextDefinitions()
- ->willReturn(['test' => $context_definition->reveal()])
- ->shouldBeCalledTimes(1);
-
- $this->conditionManager->createInstance('test_condition')
- ->willReturn($this->trueCondition->reveal())
- ->shouldBeCalledTimes(1);
-
- $this->assertSame($this->conditionExpression->getContextDefinitions(), ['test' => $context_definition->reveal()]);
- }
-
/**
* Tests that context values get data processed with processor mappings.
*/
@@ -117,11 +101,11 @@ public function testDataProcessor() {
// Mock some original old value that will be replaced by the data processor.
$this->trueCondition->getContextValue('test')
->willReturn('old_value')
- ->shouldBeCalledTimes(1);
+ ->shouldBeCalled();
// The outcome of the data processor needs to get set on the condition.
$this->trueCondition->setContextValue('test', 'new_value')->shouldBeCalledTimes(1);
- $this->trueCondition->refineContextDefinitions()->shouldBeCalledTimes(1);
+ $this->trueCondition->refineContextDefinitions([])->shouldBeCalledTimes(1);
$data_processor = $this->prophesize(DataProcessorInterface::class);
$data_processor->process('old_value', Argument::any())
@@ -146,7 +130,7 @@ public function testDataProcessor() {
*/
public function testNegation() {
$this->trueCondition->getContextDefinitions()->willReturn([]);
- $this->trueCondition->refineContextDefinitions()->shouldBeCalledTimes(1);
+ $this->trueCondition->refineContextDefinitions([])->shouldBeCalledTimes(1);
$this->trueCondition->getProvidedContextDefinitions()
->willReturn([])
->shouldBeCalledTimes(1);