diff --git a/src/Engine/RulesComponent.php b/src/Engine/RulesComponent.php index e7c74180..cdd0345a 100644 --- a/src/Engine/RulesComponent.php +++ b/src/Engine/RulesComponent.php @@ -29,9 +29,9 @@ class RulesComponent { protected $contextDefinitions = []; /** - * List of context names that is provided back to the caller. + * List of context definitions (or NULLs for BC) keyed by name. * - * @var string[] + * @var \Drupal\rules\Context\ContextDefinitionInterface|NULL[] */ protected $providedContext = []; @@ -75,7 +75,7 @@ class RulesComponent { $component->addContextDefinition($name, ContextDefinition::createFromArray($definition)); } foreach ($configuration['provided_context_definitions'] as $name => $definition) { - $component->provideContext($name); + $component->provideContext($name, ContextDefinition::createFromArray($definition)); } return $component; } @@ -110,15 +110,21 @@ class RulesComponent { * nested 'id' key. * - context_definitions: Array of context definition arrays, keyed by * context name. - * - provided_context: The names of the context that is provided back. + * - provided_context_definitions: Array of provided context definitions, + * keyed by context name. */ public function getConfiguration() { + $provided_context_definitions = $this->providedContext; + array_walk($provided_context_definitions, function (ContextDefinitionInterface &$definition, $name, $context_definitions) { + $definition = $definition ? $definition->toArray() : ($context_definitions[$name] ? $context_definitions[$name]->toArray() : []); + }, $this->contextDefinitions); + return [ 'expression' => $this->expression->getConfiguration(), 'context_definitions' => array_map(function (ContextDefinitionInterface $definition) { return $definition->toArray(); }, $this->contextDefinitions), - 'provided_context_definitions' => $this->providedContext, + 'provided_context_definitions' => $provided_context_definitions, ]; } @@ -183,11 +189,13 @@ class RulesComponent { * * @param string $name * The name of the context to provide. + * @param \Drupal\rules\Context\ContextDefinitionInterface $definition + * The definition of the context provided. * * @return $this */ - public function provideContext($name) { - $this->providedContext[] = $name; + public function provideContext($name, ContextDefinitionInterface $definition = NULL) { + $this->providedContext[$name] = $definition; return $this; } @@ -198,7 +206,7 @@ class RulesComponent { * The names of the context that is provided back. */ public function getProvidedContext() { - return $this->providedContext; + return array_keys($this->providedContext); } /** @@ -232,10 +240,17 @@ class RulesComponent { * Thrown if the Rules expression triggers errors during execution. */ public function execute() { + // Add provided context that are to context to the execution state. + foreach ($this->providedContext as $name => $definition) { + if (!isset($this->contextDefinitions[$name]) && $definition) { + $this->state->setVariable($name, $definition, NULL); + } + } + $this->expression->executeWithState($this->state); $this->state->autoSave(); $result = []; - foreach ($this->providedContext as $name) { + foreach ($this->providedContext as $name => $definition) { $result[$name] = $this->state->getVariableValue($name); } return $result; @@ -289,6 +304,11 @@ class RulesComponent { foreach ($this->contextDefinitions as $name => $context_definition) { $data_definitions[$name] = $context_definition->getDataDefinition(); } + foreach ($this->providedContext as $name => $context_definition) { + if (empty($data_definitions[$name]) && $context_definition) { + $data_definitions[$name] = $context_definition->getDataDefinition(); + } + } return ExecutionMetadataState::create($data_definitions); } diff --git a/src/Form/ReactionRuleAddForm.php b/src/Form/ReactionRuleAddForm.php index d5730bd4..140b8b1c 100644 --- a/src/Form/ReactionRuleAddForm.php +++ b/src/Form/ReactionRuleAddForm.php @@ -3,6 +3,7 @@ namespace Drupal\rules\Form; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\rules\Core\RulesEventManager; use Drupal\rules\Engine\ExpressionManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -24,11 +25,13 @@ class ReactionRuleAddForm extends RulesComponentFormBase { * * @param \Drupal\rules\Engine\ExpressionManagerInterface $expression_manager * The expression manager. + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. * @param \Drupal\rules\Core\RulesEventManager $event_manager * The Rules event manager. */ - public function __construct(ExpressionManagerInterface $expression_manager, RulesEventManager $event_manager) { - parent::__construct($expression_manager); + public function __construct(ExpressionManagerInterface $expression_manager, TypedDataManagerInterface $typed_data_manager, RulesEventManager $event_manager) { + parent::__construct($expression_manager, $typed_data_manager); $this->eventManager = $event_manager; } @@ -36,7 +39,7 @@ class ReactionRuleAddForm extends RulesComponentFormBase { * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('plugin.manager.rules_expression'), $container->get('plugin.manager.rules_event')); + return new static($container->get('plugin.manager.rules_expression'), $container->get('typed_data_manager'), $container->get('plugin.manager.rules_event')); } /** diff --git a/src/Form/ReactionRuleEditForm.php b/src/Form/ReactionRuleEditForm.php index 2afaffcf..66e3436e 100644 --- a/src/Form/ReactionRuleEditForm.php +++ b/src/Form/ReactionRuleEditForm.php @@ -3,6 +3,7 @@ namespace Drupal\rules\Form; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; use Drupal\rules\Core\RulesEventManager; use Drupal\rules\Engine\ExpressionManagerInterface; use Drupal\rules\Ui\RulesUiConfigHandler; @@ -28,15 +29,17 @@ class ReactionRuleEditForm extends RulesComponentFormBase { protected $rulesUiHandler; /** - * Constructs a new object of this class. + * Constructs a new reaction rule form. * * @param \Drupal\rules\Engine\ExpressionManagerInterface $expression_manager * The expression manager. + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. * @param \Drupal\rules\Core\RulesEventManager $event_manager - * The event plugin manager. + * The Rules event manager. */ - public function __construct(ExpressionManagerInterface $expression_manager, RulesEventManager $event_manager) { - parent::__construct($expression_manager); + public function __construct(ExpressionManagerInterface $expression_manager, TypedDataManagerInterface $typed_data_manager, RulesEventManager $event_manager) { + parent::__construct($expression_manager, $typed_data_manager); $this->eventManager = $event_manager; } @@ -44,7 +47,7 @@ class ReactionRuleEditForm extends RulesComponentFormBase { * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('plugin.manager.rules_expression'), $container->get('plugin.manager.rules_event')); + return new static($container->get('plugin.manager.rules_expression'), $container->get('typed_data_manager'), $container->get('plugin.manager.rules_event')); } /** diff --git a/src/Form/RulesComponentFormBase.php b/src/Form/RulesComponentFormBase.php index b58e035d..8d211ef4 100644 --- a/src/Form/RulesComponentFormBase.php +++ b/src/Form/RulesComponentFormBase.php @@ -4,7 +4,10 @@ namespace Drupal\rules\Form; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\rules\Entity\RulesComponentConfig; use Drupal\rules\Engine\ExpressionManagerInterface; +use Drupal\rules\Context\ContextDefinition; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,11 +22,21 @@ abstract class RulesComponentFormBase extends EntityForm { */ protected $expressionManager; + /** + * The typed data manager. + * + * @var \Drupal\Core\TypedData\TypedDataManagerInterface + */ + protected $typedDataManager; + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('plugin.manager.rules_expression')); + return new static( + $container->get('plugin.manager.rules_expression'), + $container->get('typed_data_manager') + ); } /** @@ -31,9 +44,12 @@ abstract class RulesComponentFormBase extends EntityForm { * * @param \Drupal\rules\Engine\ExpressionManagerInterface $expression_manager * The expression manager. + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. */ - public function __construct(ExpressionManagerInterface $expression_manager) { + public function __construct(ExpressionManagerInterface $expression_manager, TypedDataManagerInterface $typed_data_manager) { $this->expressionManager = $expression_manager; + $this->typedDataManager = $typed_data_manager; } /** @@ -83,6 +99,112 @@ abstract class RulesComponentFormBase extends EntityForm { '#title' => $this->t('Description'), ]; + if (!$this->entity instanceof RulesComponentConfig) { + return parent::form($form, $form_state); + } + + $form['settings']['variables'] = [ + '#type' => 'table', + '#tree' => TRUE, + '#header' => [ + $this->t('Data Type'), + $this->t('Label'), + $this->t('Machine Name'), + $this->t('Usage'), + $this->t('Weight'), + ], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'settings-variables-order-weight', + ], + ], + ]; + + $context_definitions = $this->entity->getContextDefinitions(); + $provided_context_definitions = $this->entity->getProvidedContextDefinitions(); + + // Build one array of usages, this allows us to only loop and build the + // table once. + $def_usages = []; + foreach (array_keys($context_definitions) as $name) { + $def_usages[$name] = !empty($provided_context_definitions[$name]) ? 'both' : 'parameter'; + } + foreach (array_keys($provided_context_definitions) as $name) { + $def_usages[$name] = !empty($def_usages[$name]) ? $def_usages[$name] : 'provided'; + } + + // Always add 3 paramater type usages to the end. + $def_usages['new_1'] = 'parameter'; + $def_usages['new_2'] = 'parameter'; + $def_usages['new_3'] = 'parameter'; + + // Prepare some variables for the loop. + $weight = 0; + $usage_options = [ + 'parameter' => $this->t('Parameter'), + 'provided' => $this->t('Provided'), + 'both' => $this->t('Parameter & Provided'), + ]; + $data_types = $this->typedDataManager->getDefinitions(); + foreach ($data_types as $id => $data_definition) { + $category = !empty($data_definition['deriver']) ? $data_types[$data_definition['id']]['label'] : $this->t('Data'); + if (is_callable([$category, 'render'])) { + $category = $category->render(); + } + + $data_type_options[$category][$id] = $data_definition['label']; + } + + foreach ($def_usages as $name => $usage) { + $list_var = ($usage != 'provided') ? 'context_definitions' : 'provided_context_definitions'; + $context_definition = isset(${$list_var}[$name]) ? ${$list_var}[$name] : NULL; + + $row = []; + $row['#attributes']['class'][] = 'draggable'; + $row['#weight'] = $weight; + $row['type'] = [ + '#title' => $this->t('Data Type'), + '#title_display' => 'invisible', + '#type' => 'select', + '#options' => $data_type_options, + '#default_value' => $context_definition ? $context_definition->getDataType() : NULL, + ]; + $row['label'] = [ + '#title' => $this->t('Label'), + '#title_display' => 'invisible', + '#type' => 'textfield', + '#default_value' => $context_definition ? $context_definition->getLabel() : NULL, + ]; + $row['name'] = [ + '#title' => $this->t('Machine Name'), + '#title_display' => 'invisible', + '#type' => 'textfield', + '#default_value' => $context_definition ? $name : NULL, + ]; + $row['usage'] = [ + '#title' => $this->t('Usage'), + '#title_display' => 'invisible', + '#type' => 'select', + '#options' => $usage_options, + '#default_value' => $usage, + ]; + $row['weight'] = [ + '#title' => $this->t('Weight'), + '#title_display' => 'invisible', + '#type' => 'weight', + '#default_value' => $weight, + '#attributes' => ['class' => ['settings-variables-order-weight']], + ]; + + // Add the row to the table and include it in included defs. + $form['settings']['variables'][$name] = $row; + + // Increment the weight. + $weight++; + } + return parent::form($form, $form_state); } @@ -96,6 +218,36 @@ abstract class RulesComponentFormBase extends EntityForm { $tags = array_map('trim', explode(',', $entity->get('tags'))); } $entity->set('tags', $tags); + if ($entity instanceof RulesComponentConfig) { + // Set the context definitions. + $variables = $form_state->getValue('variables'); + uasort($variables, ['\Drupal\Component\Utility\SortArray', 'sortByWeightElement']); + + // Create the relevant context definitions. + $context_definitions = $provided_context_definitions = []; + foreach ($variables as $variable) { + // Skip any rows where the context name has not been set. + if (empty($variable['name'])) { + continue; + } + + // Build a context definition. + $context_definition = ContextDefinition::create($variable['type']); + $context_definition->setLabel($variable['label']); + + // Add the cotnext definition to the correct array(s). + if (in_array($variable['usage'], ['parameter', 'both'])) { + $context_definitions[$variable['name']] = $context_definition; + } + if (in_array($variable['usage'], ['provided', 'both'])) { + $provided_context_definitions[$variable['name']] = $context_definition; + } + } + + // Set the context definitions. + $entity->setContextDefinitions($context_definitions); + $entity->setProvidedContextDefinitions($provided_context_definitions); + } return $entity; } diff --git a/tests/src/Functional/UiPageTest.php b/tests/src/Functional/UiPageTest.php index 6c808bee..8bf47060 100644 --- a/tests/src/Functional/UiPageTest.php +++ b/tests/src/Functional/UiPageTest.php @@ -23,6 +23,83 @@ class UiPageTest extends RulesBrowserTestBase { */ protected $profile = 'minimal'; + /** + * Tests that the rules component listing page works. + */ + public function testRulesComponentPage() { + $account = $this->drupalCreateUser(['administer rules']); + $this->drupalLogin($account); + + $this->drupalGet('admin/config/workflow/rules/components'); + $this->assertSession()->statusCodeEquals(200); + + // Test that there is an empty rules component listing. + $this->assertSession()->pageTextContains('There is no Rules component yet.'); + } + + /** + * Test create a rules component. + */ + public function testCreateRulesComponent() { + $account = $this->drupalCreateUser(['administer rules']); + $this->drupalLogin($account); + + $this->drupalGet('admin/config/workflow/rules/components'); + $this->clickLink('Add rule'); + + $this->fillField('Label', 'Test Component'); + $this->fillField('Machine-readable name', 'test_component'); + $this->fillField('variables[new_1][type]', 'string'); + $this->fillField('variables[new_1][label]', 'String'); + $this->fillField('variables[new_1][name]', 'string'); + $this->fillField('variables[new_1][usage]', 'parameter'); + $this->fillField('variables[new_2][type]', 'integer'); + $this->fillField('variables[new_2][label]', 'Integer'); + $this->fillField('variables[new_2][name]', 'integer'); + $this->fillField('variables[new_2][usage]', 'provided'); + $this->fillField('variables[new_3][type]', 'entity:user'); + $this->fillField('variables[new_3][label]', 'User'); + $this->fillField('variables[new_3][name]', 'user'); + $this->fillField('variables[new_3][usage]', 'both'); + + $this->pressButton('Save'); + + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('Component Test Component has been created.'); + + $this->assertSession()->fieldValueEquals('variables[string][type]', 'string'); + $this->assertSession()->fieldValueEquals('variables[string][label]', 'String'); + $this->assertSession()->fieldValueEquals('variables[string][name]', 'string'); + $this->assertSession()->fieldValueEquals('variables[string][usage]', 'parameter'); + $this->assertSession()->fieldValueEquals('variables[integer][usage]', 'provided'); + $this->assertSession()->fieldValueEquals('variables[user][usage]', 'both'); + + $this->clickLink('Add condition'); + $this->fillField('Condition', 'rules_user_is_blocked'); + $this->pressButton('Continue'); + + $this->pressButton('Switch to data selection'); + + $this->fillField('context[user][setting]', 'user'); + $this->pressButton('Save'); + $this->assertSession()->pageTextContains('You have unsaved changes.'); + + $this->clickLink('Add action'); + $this->fillField('Action', 'rules_data_set'); + $this->pressButton('Continue'); + + $this->pressButton('context_data'); + $this->fillField('context[data][setting]', 'integer'); + $this->fillField('context[value][setting]', '4'); + $this->pressButton('Save'); + + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('You have unsaved changes.'); + + $this->pressButton('Save'); + $this->assertSession()->pageTextContains('Rule component Test Component has been updated.'); + } + /** * Tests that the reaction rule listing page works. */