Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Save entity" derivative #419

Open
wants to merge 13 commits into
base: 8.x-3.x
Choose a base branch
from
64 changes: 49 additions & 15 deletions src/Plugin/RulesAction/EntitySave.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,36 @@
namespace Drupal\rules\Plugin\RulesAction;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\rules\Core\RulesActionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityStorageInterface;

/**
* Provides a 'Save entity' action.
*
* @RulesAction(
* id = "rules_entity_save",
* label = @Translation("Save entity"),
* category = @Translation("Entity"),
* context = {
* "entity" = @ContextDefinition("entity",
* label = @Translation("Entity"),
* description = @Translation("Specifies the entity, which should be saved permanently.")
* ),
* "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 = FALSE,
* required = FALSE
* )
* }
* deriver = "Drupal\rules\Plugin\RulesAction\EntitySaveDeriver",
* )
*
* @todo: Add access callback information from Drupal 7.
*/
class EntitySave extends RulesActionBase {
class EntitySave extends RulesActionBase implements ContainerFactoryPluginInterface {

/**
* The entity storage service.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;

/**
* The entity type id.
*
* @var string
*/
protected $entityTypeId;

/**
* Flag that indicates if the entity should be auto-saved later.
Expand All @@ -37,6 +41,36 @@ class EntitySave extends RulesActionBase {
*/
protected $saveLater = TRUE;

/**
* Constructs an EntitySave object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->storage = $storage;
$this->entityTypeId = $plugin_definition['entity_type_id'];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about this __construct()

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems ok?

}

/**
* {@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($plugin_definition['entity_type_id'])
);
}

/**
* Saves the Entity.
*
Expand Down
95 changes: 95 additions & 0 deletions src/Plugin/RulesAction/EntitySaveDeriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Drupal\rules\Plugin\RulesAction;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\rules\Context\ContextDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Derives entity save plugin definitions based on content entity types.
*
* @see \Drupal\rules\Plugin\RulesAction\EntitySave
*/
class EntitySaveDeriver extends DeriverBase implements ContainerDeriverInterface {

use StringTranslationTrait;

/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/

protected $entityTypeManager;

/**
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing NL before this line.

* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface;
*/
protected $entityFieldManager;

/**
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing NL before this line.

* Saves a new EntitySaveDeriver object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->stringTranslation = $string_translation;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('string_translation')
);
}

/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Only allow content entities and ignore configuration entities.
if (!$entity_type instanceof ContentEntityTypeInterface) {
continue;
}

$this->derivatives[$entity_type_id] = [
'label' => $this->t('Save @entity_type', ['@entity_type' => $entity_type->getLowercaseLabel()]),
'category' => $entity_type->getLabel(),
'entity_type_id' => $entity_type_id,
'context' => [
'entity' => ContextDefinition::create("entity:$entity_type_id")
->setLabel($entity_type->getLabel())
->setDescription($this->t('Specifies the @entity_type_label that should be saved permanently.', ['@entity_type_label' => $entity_type->getLabel()]))
->setRequired(TRUE),
'immediate' => ContextDefinition::create('boolean')
->setLabel($this->t('Force saving immediately'))
->setDescription($this->t('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.'))
->setDefaultValue(FALSE)
->setRequired(FALSE),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but this should be actually required. NULL is invalid, only TRUE or FALSE is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I change it to TRUE?

It was "required = FALSE" check the line 29 above in EntitySave.php.

],
] + $base_plugin_definition;
}

return $this->derivatives;
}

}
16 changes: 8 additions & 8 deletions tests/src/Integration/Action/EntityCreateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,31 +55,31 @@ public function setUp() {
$bundle_field_definition->getItemDefinition()
->willReturn($item_definition->reveal());
$bundle_field_definition->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition->getType()->willReturn('string');
$bundle_field_definition->getLabel()->willReturn('Bundle')
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition->getDescription()
->willReturn('Bundle mock description')
->shouldBeCalledTimes(1);
->shouldBeCalled();

$bundle_field_definition_required->getItemDefinition()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all these mocks really needed? Looks like left-overs from EntityCreateTest for me?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the EntityCreateTest file.

the $bundle_field_definition_required and $bundle_field_definition_optional are used in willReturn()

->willReturn($item_definition->reveal());
$bundle_field_definition_required->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition_required->getType()->willReturn('string');
$bundle_field_definition_required->getLabel()->willReturn('Required field')
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition_required->getDescription()
->willReturn('Required field mock description')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The test does not work without these lines. All the comments in EntityCreateTest.php are unrelated. I only changed shouldBeCalledTimes(1) to shouldBeCalled() as you suggested during discussion.

->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition_required->isRequired()
->willReturn(TRUE)
->shouldBeCalledTimes(1);
->shouldBeCalled();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if those are called 0 times, remove them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those are called 1 time


$bundle_field_definition_optional->isRequired()
->willReturn(FALSE)
->shouldBeCalledTimes(1);
->shouldBeCalled();

// Prepare mocked entity storage.
$entity_type_storage = $this->prophesize(EntityStorageBase::class);
Expand Down
27 changes: 25 additions & 2 deletions tests/src/Integration/Action/EntitySaveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\Tests\rules\Integration\Action;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Tests\rules\Integration\RulesEntityIntegrationTestBase;

/**
Expand Down Expand Up @@ -33,7 +34,15 @@ public function setUp() {

$this->entity = $this->prophesizeEntity(EntityInterface::class);

$this->action = $this->actionManager->createInstance('rules_entity_save');
// Prepare mocked entity storage.
$entity_type_storage = $this->prophesize(EntityStorageBase::class);

// Return the mocked storage controller.
$this->entityTypeManager->getStorage('test')
->willReturn($entity_type_storage->reveal());

// Instantiate the action we are testing.
$this->action = $this->actionManager->createInstance('rules_entity_save:test');
}

/**
Expand All @@ -42,7 +51,21 @@ public function setUp() {
* @covers ::summary
*/
public function testSummary() {
$this->assertEquals('Save entity', $this->action->summary());
$this->assertEquals('Save test', $this->action->summary());
}

/**
* Tests the action execution.
*
* @covers ::execute
*/
public function testActionExecution() {
$this->entity->save()->shouldBeCalledTimes(1);

$this->action->setContextValue('entity', $this->entity->reveal())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gives exception " The entity context is not a valid context."

->setContextValue('immediate', TRUE);

$this->action->execute();
}

/**
Expand Down
28 changes: 21 additions & 7 deletions tests/src/Integration/Action/RulesComponentActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\rules\Context\ContextConfig;
use Drupal\rules\Context\ContextDefinition;
use Drupal\rules\Engine\RulesComponent;
Expand All @@ -17,6 +18,19 @@
*/
class RulesComponentActionTest extends RulesEntityIntegrationTestBase {

/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Prepare mocked entity storage.
$entity_type_storage = $this->prophesize(EntityStorageBase::class);

// Return the mocked storage controller.
$this->entityTypeManager->getStorage('test')
->willReturn($entity_type_storage->reveal());
}

/**
* Tests that a rule can be used as action.
*/
Expand All @@ -42,7 +56,7 @@ public function testActionAvailable() {
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()
$nested_rule->addAction('rules_entity_save:test', ContextConfig::create()
->map('entity', 'entity')
);

Expand All @@ -51,7 +65,7 @@ public function testExecute() {
'label' => 'Test rule',
], 'rules_component');
$rules_config->setExpression($nested_rule);
$rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity')]);
$rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity:test')]);

$this->prophesizeStorage([$rules_config]);

Expand All @@ -66,7 +80,7 @@ public function testExecute() {
$entity->save()->shouldBeCalledTimes(1);

RulesComponent::create($rule)
->addContextDefinition('entity', ContextDefinition::create('entity'))
->addContextDefinition('entity', ContextDefinition::create('entity:test'))
->setContextValue('entity', $entity->reveal())
->execute();
}
Expand Down Expand Up @@ -139,7 +153,7 @@ public function testAutosaveOnlyOnce() {
$entity = $this->prophesizeEntity(EntityInterface::class);

$nested_rule = $this->rulesExpressionManager->createRule();
$nested_rule->addAction('rules_entity_save', ContextConfig::create()
$nested_rule->addAction('rules_entity_save:test', ContextConfig::create()
->map('entity', 'entity')
);

Expand All @@ -148,7 +162,7 @@ public function testAutosaveOnlyOnce() {
'label' => 'Test rule',
], 'rules_component');
$rules_config->setExpression($nested_rule);
$rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity')]);
$rules_config->setContextDefinitions(['entity' => ContextDefinition::create('entity:test')]);

$this->prophesizeStorage([$rules_config]);

Expand All @@ -158,15 +172,15 @@ public function testAutosaveOnlyOnce() {
$rule->addAction('rules_component:test_rule', ContextConfig::create()
->map('entity', 'entity')
);
$rule->addAction('rules_entity_save', ContextConfig::create()
$rule->addAction('rules_entity_save:test', 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'))
->addContextDefinition('entity', ContextDefinition::create('entity:test'))
->setContextValue('entity', $entity->reveal())
->execute();
}
Expand Down
Loading