Transcript

Ruling Drupal 8 with

#d8rulesJosef Dabernig, Amazee Labs

@dasjo

• Facet API Pretty Paths

• Geocluster

• Drupal Centroamerica, Austria, Switzerland, ++

• Deputy Head Technology @ Amazee Labs

twitter.com/dasjo

• fago (Rules creator)• klausi (co-maintainer)• dasjo (communication)• fubhy (developer)• nico grienauer (design)• steve purkiss

(developer)

#d8rules team

Rules

Rules

Rules

Rules• Build flexible workflows

using events, condition & actions

• Send customized mails to notify your users

• Create custom redirections, system messages, breadcrumbs

Rules• Build flexible workflows

using events, condition & actions

• Send customized mails to notify your users

• Create custom redirections, system messages, breadcrumbs

• 300.000+ reported installs(30% of all Drupal 7 sites)

• Hundreds of integration modules

• Entity API, Fields, Views, Webform, Context, Features, Search API, Tokens, Paths, Menus, Queue, Field formatter conditions, ...

Drupal 8 wins

Drupal 8 wins

• OOP, Dependency Injection

• APIs• PHPUnit• Symfony2

• Removed legacy modules

• Web services built in

• Front-end, responsive, ...

Drupal 8 for Rules

• Plug-in API• Entity & Typed Data API• Conditions API• (Actions API -> forked)• Context API shared w/ Core, Page Manager• Configuration Management (CMI, YAML)

Reusable components (“rtools”)

• Tokens (automatic, based on typed data)• Typed data widgets & formatters• Extended Context API• Embeddable Rules UI components

• Actions & Conditions• Rules data selector for tokens, contexts

Reusable components (“rtools”)

Site building

• Admin UI usability improvements• (Simple Views Bulk operations in core)• “Inline Rules” instead of Rule sets / Rules

conditional• Deployable configuration

Campaign

#d8rules goals• Accelerate Drupal 8 uptake by ensuring that

Rules as a key contributed module is ready, early enough.

• Enable flexible workflows in Drupal 8 sites that are easily configurable & reusable.

• Make Drupal contributions sustainable by funding contributed code used on hundreds of thousands of sites.

Sponsors

Funding state

Rules 8.x development status

Rules 8.x M1Rules core API fundamentals

✓ Rules core engine, plugin types✓ Align Rules condition & action APIs with core✓ Parameter configuration & Context mapping

Rules 8.x M2Rules core completion with basic UI

✓ Completed Rules engine features✓ Completed Rules plugins (Events, Loops, API)✓ Configuration entity, CMI and integrity checks✓ Basic UI and autocomplete functionality✓ API for embedding the Rules UI✓ Twig-style token replacements

Rules 8.x M3Rules release

❏ Stable Rules 8.x-3.0 Release❏ Rules Engine follow-ups❏ Complete Rules UI❏ Rules scheduler port❏ Port existing integrations (almost done!)

Rules 8.xRoadmap

https://www.drupal.org/node/2245015

Sprints, trainings & sessionsSzeged, Portoroz, Austin, Drupalaton, Amsterdam, Zurich, Bogotá, Vienna, Milano, Montpellier, Bratislava, Sunderland, Barcelona, Mumbai, Heidelberg, Granada, ..

http://d8rules.org/events

Contribution

• 114 forks, 40 contributors• paranoik, stevepurkiss, scuts, jibran, chindris,

omissis, ndewhurst, jzavrl, MegaChriz, bbujisic, dawehner, torenware, bartfeenstra, M1r1k, rinasek, joashuataylor, lokapujya, icanblink, bojanz, a.milkovsky, mariancalinro, pjezek,czigor, mikl, nlisgo, nielsdefeyter, ...

• 427 closed pull requests, Thank you all!

Getting started

• Setup Drupal 8• Fork Rules 8.x on github:

• https://github.com/fago/rules

Diving into Rules 8.x

Writing Conditions, Actions & Events

Plug-ins

• OOP• Annotations• Auto-loading• Discovery• Derivatives

Provide conditions

• hook_rules_condition_info()-> ?

Provide conditions

• hook_rules_condition_info-> Implement a Condition plug-in

/** * Provides a 'Node is sticky' condition. * * @Condition( * id = "rules_node_is_sticky", * label = @Translation("Node is sticky"), * category = @Translation("Node"), * context = {…} * ) */class NodeIsSticky extends RulesConditionBase {

/** * {@inheritdoc} */ public function evaluate() { $node = $this->getContextValue('node'); return $node->isSticky(); }

}

Provide actions

• hook_rules_action_info-> Implement an Action plug-in

/** * Provides a 'Delete entity' action. * * @Action( * id = "rules_entity_delete", * label = @Translation("Delete entity"), * category = @Translation("Entity"), * context = {…} * ) */class EntityDelete extends RulesActionBase {

/** * {@inheritdoc} */ public function execute() { $entity = $this->getContextValue('entity'); $entity->delete(); }

}

Provide events

• hook_rules_event_info-> Specify Symfony event metadata in *.rules.events.yml

rules_user_login:

label: 'User has logged in'

category: 'User'

context:

account:

type: 'entity:user'

label: 'Logged in user'

Event example from rules.rules.events.yml

Event invocation

/** * Implements hook_user_login(). */function rules_user_login($account) { // Set the account twice on the event: as the main subject but also in the // list of arguments. $event = new UserLoginEvent($account, ['account' => $account]); $event_dispatcher = Drupal::service('event_dispatcher'); $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);}

Event invocation

/** * Implements hook_user_login(). */function rules_user_login($account) { // Set the account twice on the event: as the main subject but also in the // list of arguments. $event = new UserLoginEvent($account, ['account' => $account]); $event_dispatcher = Drupal::service('event_dispatcher'); $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);}

Context

Context

● Defining context for a plug-in

/** * Provides a 'Delete entity' action. * * @Action( * id = "rules_entity_delete", * label = @Translation("Delete entity"), * category = @Translation("Entity"), * context = {…} * ) */class EntityDelete extends RulesActionBase {

/** * {@inheritdoc} */ public function execute() { $entity = $this->getContextValue('entity'); $entity->delete(); }

}

/** * Provides a 'Delete entity' action. * * @Action( * id = "rules_entity_delete", * label = @Translation("Delete entity"), * category = @Translation("Entity"), * context = { * "entity" = @ContextDefinition("entity", * label = @Translation("Entity"), * description = @Translation("Specifies the entity, which should be deleted permanently.") * ) * } * ) */class EntityDelete extends RulesActionBase {...}

Context

● Defining context for a plug-in● Using context within a plug-in

class EntityDelete extends RulesActionBase {

/** * Executes the action with the given context. */ public function doExecute(EntityInterface $entity) { $entity->delete(); }

}

class EntityDelete extends RulesActionBase {

/** * Executes the action with the given context. */ public function doExecute(EntityInterface $entity) { $entity->delete(); }

}

class FetchEntityById extends RulesActionBase implements ContainerFactoryPluginInterface {

/** * Executes the action with the given context. */ public function doExecute($entity_type, $entity_id) { $storage = $this->entityManager->getStorage($entity_type); $entity = $storage->load($entity_id); $this->setProvidedValue('entity', $entity); }}

class FetchEntityById extends RulesActionBase implements ContainerFactoryPluginInterface {

/** * Executes the action with the given context. */ public function doExecute($entity_type, $entity_id) { $storage = $this->entityManager->getStorage($entity_type); $entity = $storage->load($entity_id); $this->setProvidedValue('entity', $entity); }}

Context

● Defining context for a plug-in● Using context within a plug-in● Passing context to the plug-in

class ListCountIsTest extends RulesIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { // Test that the list count is greater than 2. $this->condition ->setContextValue('list', [1, 2, 3, 4]) ->setContextValue('operator', '>') ->setContextValue('value', '2'); $this->assertTrue($condition->evaluate()); }

}

class ListCountIsTest extends RulesIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { // Test that the list count is greater than 2. $this->condition ->setContextValue('list', [1, 2, 3, 4]) ->setContextValue('operator', '>') ->setContextValue('value', '2'); $this->assertTrue($condition->evaluate()); }

}

Storing configuration

Storing configuration

• D7: Entity exportables

• Rules:• RulesPlugin->export()

-> ?• RulesEntityController->import()

-> ?

Storing configuration

• D7: Entity exportables

• Rules:• RulesPlugin->export()

-> RulesExpression::getConfiguration()• RulesEntityController->import()

-> RulesExpression::setConfiguration()

Drupal 8: CMI● Rules leverages the Config System● Two types of config entities:

○ Reaction Rules (WIP)○ Components

● Inherit features from CMI○ Config deployment / import&export / sync○ Config Translation (instead of Entity i18n)○ Default config

Provide default rule configs

• hook_default_rules_configuration()-> ?

Provide default rule configs

• hook_default_rules_configuration()-> rules.reaction.*.yml-> rules.component.*.yml

langcode: enstatus: truedependencies: {}id: rules_test_default_componentlabel: Rules test default componentmodule: rulesdescription: 'Tests adding Rules component by default.'tag: 'test'core: 8.xexpression_id: rules_ruleconfiguration:

...

configuration: id: rules_rule context: user: type: 'entity:user' label: User conditions: id: rules_and conditions: {} actions: id: rules_action_set actions: - id: rules_action action_id: rules_system_message context_mapping: message: 'user:mail:value'

Executing the component$config_entity = RulesComponent::load('rules_test_default_component');

$expression = $config_entity->getExpression();

$expression ->setContextValue('user', \Drupal::currentUser()) ->execute();

Describe data to Rules

Drupal 7• hook_rules_data_info()

-> ?• hook_entity_property_info_alter

-> ?

Typed Data API

• Consistent way of interacting with any data based on metadata

• Part of Drupal 8 & Entity Fields• Defines a type system for PHP:

○ Primitive types (integer, float, string, dates, ..)○ Complex types○ Lists (with items of a specified type)

Data types● any● string, integer,

uri, float, ...○ email○ timestamp,

datetime_iso8601○ timespan,

duration_iso8601

● entity○ entity:node○ entity:comment

● field_item○ field_item:string○ field_item:text○ field_item:image

/** * The float data type. * * The plain value of a float is a regular PHP float. For setting the value * any PHP variable that casts to a float may be passed. * * @DataType( * id = "float", * label = @Translation("Float") * ) */class FloatData extends PrimitiveBase implements FloatInterface {

/** * {@inheritdoc} */ public function getCastedValue() { return (float) $this->value; }}

/** * The float data type. * * The plain value of a float is a regular PHP float. For setting the value * any PHP variable that casts to a float may be passed. * * @DataType( * id = "float", * label = @Translation("Float") * ) */class FloatData extends PrimitiveBase implements FloatInterface {

/** * {@inheritdoc} */ public function getCastedValue() { return (float) $this->value; }}

/** * Plugin implementation of the 'link' field type. * * @FieldType( * id = "link", * label = @Translation("Link"), * description = @Translation(“..."), * default_widget = "link_default", * default_formatter = "link", * constraints = {"LinkType" = {}} * ) */class LinkItem extends FieldItemBase implements LinkItemInterface {

public static function propertyDefinitions( FieldStorageDefinitionInterface $field_definition) {

$properties['url'] = DataDefinition::create('string') ->setLabel(t('URL'));… return $properties; }

/** * Plugin implementation of the 'link' field type. * * @FieldType( * id = "link", * label = @Translation("Link"), * description = @Translation(“..."), * default_widget = "link_default", * default_formatter = "link", * constraints = {"LinkType" = {}} * ) */class LinkItem extends FieldItemBase implements LinkItemInterface {

public static function propertyDefinitions( FieldStorageDefinitionInterface $field_definition) {

$properties['url'] = DataDefinition::create('string') ->setLabel(t('URL'));… return $properties; }

Describe data to rules

• hook_rules_data_info()-> ?

• hook_entity_property_info_alter-> ?

Describe data to rules

• hook_rules_data_info()-> Data type plugins, Typed Data

• hook_entity_property_info_alter-> hook_data_type_info_alter()-> hook_entity_base_field_info/alter()-> hook_entity_bundle_field_info/alter()-> FieldItem::propertyDefintions()

Every content entity & field type in Drupal 8 is supported by Rules out-of-the box!

[META] Rules 8.x UI

Lists & Multiple values● list<foo> notation is gone● “list” data type & per data type class● Separate data definitions for lists vs. list

items● Mark context as “multiple”

Creating a Rule with context

$rule = $this->expressionManager->createRule([ 'context_definitions' => [ 'test' => [ 'type' => 'string', 'label' => 'Test string', ], ], ]);

$rule = $this->expressionManager->createRule([ 'context_definitions' => [ 'test' => [ 'type' => 'string', 'label' => 'Test string', ], ], ]);

$rule->addCondition('rules_test_string_condition', ContextConfig::create()->map('text', 'test')

);

$rule = $this->expressionManager->createRule([ 'context_definitions' => [ 'test' => [ 'type' => 'string', 'label' => 'Test string', ], ], ]);

$rule->addCondition('rules_test_string_condition', ContextConfig::create()->map('text', 'test')

);

$rule->addAction('rules_test_log');

$rule = $this->expressionManager->createRule([ 'context_definitions' => [ 'test' => [ 'type' => 'string', 'label' => 'Test string', ], ], ]);

$rule->addCondition('rules_test_string_condition', ContextConfig::create()->map('text', 'test')

);

$rule->addAction('rules_test_log'); $rule->setContextValue('test', 'test value');

$rule = $this->expressionManager->createRule([ 'context_definitions' => [ 'test' => [ 'type' => 'string', 'label' => 'Test string', ], ], ]);

$rule->addCondition('rules_test_string_condition', ContextConfig::create()->map('text', 'test')

);

$rule->addAction('rules_test_log'); $rule->setContextValue('test', 'test value'); $rule->execute();

Mapping required and provided context

$rule = $this->expressionManager->createRule();

$rule = $this->expressionManager->createRule(); // Condition provides a "provided_text" variable. $rule->addCondition('rules_test_provider');

$rule = $this->expressionManager->createRule(); // Condition provides a "provided_text" variable. $rule->addCondition('rules_test_provider'); // Action provides a "concatenated" variable. $rule->addAction('rules_test_string',

ContextConfig::create()->provideAs('text', 'provided_text')

);

$rule = $this->expressionManager->createRule(); // Condition provides a "provided_text" variable. $rule->addCondition('rules_test_provider'); // Action provides a "concatenated" variable. $rule->addAction('rules_test_string',

ContextConfig::create()->provideAs('text', 'provided_text')

); // Same action again now provides "concatenated2" $rule->addAction('rules_test_string',

ContextConfig::create()->map(text, 'concatenated') ->provideAs('concatenated', 'concatenated2')

);

$rule = $this->expressionManager->createRule(); // Condition provides a "provided_text" variable. $rule->addCondition('rules_test_provider'); // Action provides a "concatenated" variable. $rule->addAction('rules_test_string',

ContextConfig::create()->provideAs('text', 'provided_text')

); // Same action again now provides "concatenated2" $rule->addAction('rules_test_string',

ContextConfig::create()->map(text, 'concatenated') ->provideAs('concatenated', 'concatenated2')

); $rule->execute();

Advanced / under the hood

Provide other Rules plugins

• hook_rules_plugin_info-> Implement a RulesExpression plugin

Provide input evaluators• hook_rules_evaluator_info

-> Implement a RulesDataProcessor plugin

/** * A data processor for applying numerical offsets. * * The plugin configuration must contain the following entry: * - offset: the value that should be added. * * @RulesDataProcessor( * id = "rules_numeric_offset", * label = @Translation("Apply numeric offset") * ) */class NumericOffset extends PluginBase implements RulesDataProcessorInterface {

/** * {@inheritdoc} */ public function process($value) { return $value + $this->configuration['offset']; }

}

Automated testing

Automated testing

• RulesUnitTestBase extends UnitTestCase• Internal unit tests such as RuleTest, RulesAndTest,

RulesContextTraitTest, …

Automated testing

• RulesUnitTestBase extends UnitTestCase• Internal unit tests such as RuleTest, RulesAndTest,

RulesContextTraitTest, …• RulesIntegrationTestBase

• use ActionManager, ConditionManager, TypedDataManager, …

class DataListCountIs extends RulesConditionBase {

/** * {@inheritdoc} */ public function evaluate() { $list = $this->getContextValue('list'); $operator = $this->getContextValue('operator'); $value = $this->getContextValue('value');

switch ($operator) { case '==': return count($list) == $value;

case '<'; return count($list) < $value;

case '>'; return count($list) > $value;

} }

}

class ListCountIsTest extends RulesIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { // Test that the list count is greater than 2. $condition = $this->condition ->setContextValue('list', [1, 2, 3, 4]) ->setContextValue('operator', '>') ->setContextValue('value', '2'); $this->assertTrue($condition->evaluate());

// Test that the list count is not equal to 0. $condition = $this->condition ->setContextValue('list', [1, 2, 3]) ->setContextValue('operator', '==') ->setContextValue('value', '0'); $this->assertFalse($condition->evaluate()); }

}

Automated testing

• RulesUnitTestBase extends UnitTestCase• Internal unit tests such as RuleTest, RulesAndTest,

RulesContextTraitTest, …• RulesIntegrationTestBase

• use ActionManager, ConditionManager, TypedDataManager, …

Automated testing

• RulesUnitTestBase extends UnitTestCase• Internal unit tests such as RuleTest, RulesAndTest,

RulesContextTraitTest, …• RulesIntegrationTestBase

• use ActionManager, ConditionManager, TypedDataManager, …

• RulesEntityIntegrationTestBase

/** * @Condition( * id = "rules_entity_is_of_type", * label = @Translation("Entity is of type"), * category = @Translation("Entity"), * context = { * "entity" = @ContextDefinition("entity", * label = @Translation("Entity"), * ), * "type" = @ContextDefinition("string", * label = @Translation("Type"), * ) * } * ) */class EntityIsOfType extends RulesConditionBase {

public function evaluate() { $provided_entity = $this->getContextValue('entity'); $specified_type = $this->getContextValue('type'); $entity_type = $provided_entity->getEntityTypeId();

// Check whether the entity type matches. return $entity_type == $specified_type; }}

class EntityIsOfTypeTest extends RulesEntityIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); $entity->expects($this->exactly(2)) ->method('getEntityTypeId') ->will($this->returnValue('node'));

// Add the test node to our context as the evaluated entity. // First, test with a value that should evaluate TRUE. $this->condition->setContextValue('entity', $entity) ->setContextValue('type', 'node'); $this->assertTrue($this->condition->evaluate());

// Then test with values that should evaluate FALSE. $this->condition->setContextValue('type', 'taxonomy_term'); $this->assertFalse($this->condition->evaluate()); }}

class EntityIsOfTypeTest extends RulesEntityIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); $entity->expects($this->exactly(2)) ->method('getEntityTypeId') ->will($this->returnValue('node'));

// Add the test node to our context as the evaluated entity. // First, test with a value that should evaluate TRUE. $this->condition->setContextValue('entity', $entity) ->setContextValue('type', 'node'); $this->assertTrue($this->condition->evaluate());

// Then test with values that should evaluate FALSE. $this->condition->setContextValue('type', 'taxonomy_term'); $this->assertFalse($this->condition->evaluate()); }}

class EntityIsOfTypeTest extends RulesEntityIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); $entity->expects($this->exactly(2)) ->method('getEntityTypeId') ->will($this->returnValue('node'));

// Add the test node to our context as the evaluated entity. // First, test with a value that should evaluate TRUE. $this->condition->setContextValue('entity', $entity) ->setContextValue('type', 'node'); $this->assertTrue($this->condition->evaluate());

// Then test with values that should evaluate FALSE. $this->condition->setContextValue('type', 'taxonomy_term'); $this->assertFalse($this->condition->evaluate()); }}

class EntityIsOfTypeTest extends RulesEntityIntegrationTestBase {

/** * Tests evaluating the condition. * * @covers ::evaluate() */ public function testConditionEvaluation() { $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); $entity->expects($this->exactly(2)) ->method('getEntityTypeId') ->will($this->returnValue('node'));

// Add the test node to our context as the evaluated entity. // First, test with a value that should evaluate TRUE. $this->condition->setContextValue('entity', $entity) ->setContextValue('type', 'node'); $this->assertTrue($this->condition->evaluate());

// Then test with values that should evaluate FALSE. $this->condition->setContextValue('type', 'taxonomy_term'); $this->assertFalse($this->condition->evaluate()); }}

RulesState

RulesState

• variables• applyDataSelector• autoSave

Traits

trait StringTranslationTrait {

/** * The string translation service. * * @var \Drupal\Core\StringTranslation\TranslationInterface */ protected $stringTranslation;

/** * Translates a string to the current language or to a given language. * * See the t() documentation for details. */ protected function t($string, …) { return $this->getStringTranslation()->translate($string, …); }

}

class MyClass {

use StringTranslationTrait;

public function __construct( TranslationInterface $string_translation) { $this->stringTranslation = $string_translation; }

/** * Does something. */ public function doSth() { // ... $string = $this->t('Something'); // ... }

}

Traits

• RulesContextTrait• in addition to ContextAwarePluginBase• used in RulesActionBase and RulesConditionBase

Outro

Sprint with us!

• Port actions & conditions• https://www.drupal.org/node/2245015

Sprint with us!

• Port actions & conditions• https://www.drupal.org/node/2245015

Fluxkraft UI proposals

Rules Transformers

• https://www.drupal.org/node/2251267

NoFlo