Skip to content
64 changes: 49 additions & 15 deletions src/Plugin/RulesAction/EntitySave.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,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 = NULL,
* 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 @@ -42,6 +46,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
111 changes: 111 additions & 0 deletions src/Plugin/RulesAction/EntitySaveDeriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

/**
* @file
* Contains \Drupal\rules\Plugin\RulesAction\EntitySaveDeriver.
*/

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' => [],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

context should also contain entity. did not figure out how to do it

Copy link
Owner

Choose a reason for hiding this comment

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

add an entity key as below?

'provides' => [
'entity' => ContextDefinition::create("entity:$entity_type_id")
->setLabel($entity_type->getLabel())
->setRequired(TRUE),
],
] + $base_plugin_definition;
// Add a required context for the bundle key, and optional contexts for
// other required base fields. This matches the storage create() behavior,
// where only the bundle requirement is enforced.
$bundle_key = $entity_type->getKey('bundle');
$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;
}

$is_bundle = ($field_name == $bundle_key);
$multiple = ($definition->getCardinality() === 1) ? FALSE : TRUE;
$context_definition = ContextDefinition::create($definition->getType())
->setLabel($definition->getLabel())
->setRequired($is_bundle)
->setMultiple($multiple)
->setDescription($definition->getDescription());

if ($is_bundle) {
$context_definition->setAssignmentRestriction(ContextDefinition::ASSIGNMENT_RESTRICTION_INPUT);
}

$this->derivatives[$entity_type_id]['context'][$field_name] = $context_definition;
}
}

return $this->derivatives;
}

}
20 changes: 10 additions & 10 deletions tests/src/Integration/Action/EntityCreateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,31 @@ public function setUp() {
// and the mocked field definition is instantiated without the necessary
// information.
$bundle_field_definition->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition->getType()->willReturn('string')
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition->getLabel()->willReturn('Bundle')
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition->getDescription()
->willReturn('Bundle mock description')
->shouldBeCalledTimes(1);
->shouldBeCalled();

$bundle_field_definition_required->getCardinality()->willReturn(1)
->shouldBeCalledTimes(1);
->shouldBeCalled();
$bundle_field_definition_required->getType()->willReturn('string')
->shouldBeCalledTimes(1);
->shouldBeCalled();
$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
83 changes: 82 additions & 1 deletion tests/src/Integration/Action/EntitySaveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
namespace Drupal\Tests\rules\Integration\Action;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Tests\rules\Integration\RulesEntityIntegrationTestBase;

/**
Expand All @@ -30,6 +33,11 @@ class EntitySaveTest extends RulesEntityIntegrationTestBase {
*/
protected $entity;

/**
* A constant that will be used instead of an entity.
*/
const ENTITY_REPLACEMENT = 'This is a fake entity';
Copy link
Owner

Choose a reason for hiding this comment

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

what? if something, that it needs to return a mock


/**
* {@inheritdoc}
*/
Expand All @@ -38,7 +46,63 @@ public function setUp() {

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

$this->action = $this->actionManager->createInstance('rules_entity_save');
// Prepare some mocked bundle field definitions. This is needed because
// EntityCreateDeriver adds required contexts for required fields, and
// assumes that the bundle field is required.
$entity_definition = $this->prophesize(EntityDataDefinition::class);
$bundle_field_definition = $this->prophesize(BaseFieldDefinition::class);
$bundle_field_definition_optional = $this->prophesize(BaseFieldDefinition::class);
$bundle_field_definition_required = $this->prophesize(BaseFieldDefinition::class);

// The next methods are mocked because EntitySaveDeriver executes them,
// and the mocked field definition is instantiated without the necessary
// information.
$bundle_field_definition->getCardinality()->willReturn(1)
->shouldBeCalled();
$bundle_field_definition->getType()->willReturn('string')
->shouldBeCalled();
$bundle_field_definition->getLabel()->willReturn('Bundle')
->shouldBeCalled();
$bundle_field_definition->getDescription()
->willReturn('Bundle mock description')
->shouldBeCalled();

$bundle_field_definition_required->getCardinality()->willReturn(1)
->shouldBeCalled();
$bundle_field_definition_required->getType()->willReturn('string')
->shouldBeCalled();
$bundle_field_definition_required->getLabel()->willReturn('Required field')
->shouldBeCalled();
$bundle_field_definition_required->getDescription()
->willReturn('Required field mock description')
->shouldBeCalled();
$bundle_field_definition_required->isRequired()
->willReturn(TRUE)
->shouldBeCalled();

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

// Prepare mocked entity storage.
$entity_type_storage = $this->prophesize(EntityStorageBase::class);
$entity_type_storage->save(['entity' => $this->entity, 'bundle' => 'test', 'field_required' => NULL])
->willReturn(self::ENTITY_REPLACEMENT);

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

// Return a mocked list of base fields definitions.
$this->entityFieldManager->getBaseFieldDefinitions('test')
->willReturn([
'bundle' => $bundle_field_definition->reveal(),
'field_required' => $bundle_field_definition_required->reveal(),
'field_optional' => $bundle_field_definition_optional->reveal(),
]);

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

/**
Expand All @@ -50,6 +114,23 @@ public function testSummary() {
$this->assertEquals('Save entity', $this->action->summary());
}

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

// @todo Exception: The entity context is not a valid context.
$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();
$entity = $this->action->getProvidedContext('entity')->getContextValue();
Copy link
Owner

Choose a reason for hiding this comment

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

why would entity save provide an entity? it doesn't afaik

$this->assertEquals(self::ENTITY_REPLACEMENT, $entity);
Copy link
Owner

Choose a reason for hiding this comment

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

not necessary, line 123 contains the needed test coverage

}

/**
* Tests the action execution when saving immediately.
*
Expand Down