Commit 9029304a authored by mxh's avatar mxh Committed by Jürgen Haas
Browse files

Issue #3292951 by mxh: Improve handling on level of typed data in DTO and Configuration

parent d0400c00
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ class EcaExecutionConfigSubscriber extends EcaBase {
    $event = $before_event->getEvent();
    if ($event instanceof ConfigCrudEvent) {
      $config = $event->getConfig();
      $this->tokenService->addTokenData('config', $config->getRawData());
      $this->tokenService->addTokenData('config', $config->get());
      $this->tokenService->addTokenData('config_name', $config->getName());
    }
  }
+1 −1
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ abstract class ConfigActionBase extends ConfigurableActionBase {
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface|null
   */
  protected ?ConfigFactoryInterface $configFactory;
  protected ?ConfigFactoryInterface $configFactory = NULL;

  /**
   * {@inheritdoc}
+51 −1
Original line number Diff line number Diff line
@@ -2,7 +2,10 @@

namespace Drupal\eca_config\Plugin\Action;

use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\eca\Plugin\Action\ActionBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Action to read configuration.
@@ -15,6 +18,23 @@ use Drupal\Core\Form\FormStateInterface;
 */
class ConfigRead extends ConfigActionBase {

  /**
   * The typed config manager.
   *
   * @var \Drupal\Core\Config\TypedConfigManagerInterface|null
   */
  protected ?TypedConfigManagerInterface $typedConfigManager = NULL;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): ActionBase {
    /** @var \Drupal\eca_config\Plugin\Action\ConfigRead $instance */
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->setTypedConfigManager($container->get('config.typed'));
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
@@ -27,7 +47,27 @@ class ConfigRead extends ConfigActionBase {
    $config_factory = $this->getConfigFactory();

    $config = $include_overridden ? $config_factory->get($config_name) : $config_factory->getEditable($config_name);
    if ($include_overridden) {
      // No usage of typed config when overridden values shall be included.
      // This prevents the DTO from accidentally saving overriden values.
      $value = $config->get($config_key);
    }
    else {
      $value = $this->typedConfigManager->createFromNameAndData($config->getName(), $config->get());
      if ($config_key !== '') {
        $key_parts = explode('.', $config_key);
        while (($key = array_shift($key_parts)) !== NULL) {
          foreach ($value as $k => $element) {
            if ($k === $key) {
              $value = $element;
              break;
            }
          }
          $value = NULL;
          break;
        }
      }
    }

    $token->addTokenData($token_name, $value);
  }
@@ -73,4 +113,14 @@ class ConfigRead extends ConfigActionBase {
    parent::submitConfigurationForm($form, $form_state);
  }

  /**
   * Set the typed config manager.
   *
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $manager
   *   The manager.
   */
  public function setTypedConfigManager(TypedConfigManagerInterface $manager) {
    $this->typedConfigManager = $manager;
  }

}
+87 −15
Original line number Diff line number Diff line
@@ -5,14 +5,14 @@ namespace Drupal\eca\Plugin\DataType;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\Plugin\DataType\Map;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\eca\TypedData\DataTransferObjectDefinition;

@@ -75,18 +75,13 @@ class DataTransferObject extends Map {
    }
    /** @var \Drupal\eca\Plugin\DataType\DataTransferObject $dto */
    if (isset($value)) {
      if ($value instanceof FieldableEntityInterface) {
        $dto->setStringRepresentation($value->id());
        $dto->setValue($value->getFields());
      }
      elseif ($value instanceof ConfigEntityInterface) {
      if ($value instanceof EntityInterface) {
        $dto->setStringRepresentation($value->id());
        $dto->setValue($value->toArray());
      }
      elseif ($value instanceof Config) {
        $dto->setValue($value->getRawData());
        $dto->setStringRepresentation($value->getName());
      }
      elseif (is_scalar($value)) {
      if (is_scalar($value)) {
        $dto->setStringRepresentation($value);
      }
      else {
@@ -244,8 +239,21 @@ class DataTransferObject extends Map {
   */
  public function setValue($values, $notify = TRUE) {
    if ($values instanceof TypedDataInterface) {
      if (($values instanceof TraversableTypedDataInterface) && ($elements = static::traverseElements($values))) {
        $values = $elements;
      }
      else {
        $values = $values->getValue();
      }
    }
    if ($values instanceof EntityInterface) {
      $values = $values->getTypedData()->getProperties();
    }
    elseif ($values instanceof Config) {
      /** @var \Drupal\Core\TypedData\TraversableTypedDataInterface $typed_config */
      $typed_config = \Drupal::service('config.typed')->createFromNameAndData($values->getName(), $values->getRawData());
      $values = static::traverseElements($typed_config);
    }
    if (is_null($values)) {
      // Shortcut to make this DTO empty.
      $this->stringRepresentation = NULL;
@@ -439,6 +447,9 @@ class DataTransferObject extends Map {
    elseif ($value instanceof EntityInterface) {
      $this->writePropertyValue($property_name, $this->wrapEntityValue($property_name, $value));
    }
    elseif ($value instanceof Config) {
      $this->writePropertyValue($property_name, $this->wrapConfigValue($property_name, $value));
    }
    elseif (is_scalar($value)) {
      $this->writePropertyValue($property_name, $this->wrapScalarValue($property_name, $value));
    }
@@ -518,6 +529,31 @@ class DataTransferObject extends Map {
    }
  }

  /**
   * Saves contained data that belongs to a saveable resource.
   */
  public function saveData(): void {
    $saveables = [];
    foreach ($this->properties as $property) {
      $value = $property->getValue();
      if ((($value instanceof EntityInterface) || ($value instanceof Config) && !($value instanceof ImmutableConfig)) && !in_array($value, $saveables, TRUE)) {
        $saveables[] = $value;
        continue;
      }
      $parent = NULL;
      while (($property->getParent() !== $parent) && ($parent = $property->getParent())) {
        $parent_value = $parent->getValue();
        if ((($parent_value instanceof EntityInterface) || ($parent_value instanceof Config) && !($parent_value instanceof ImmutableConfig)) && !in_array($parent_value, $saveables, TRUE)) {
          $saveables[] = $parent_value;
          break;
        }
      }
    }
    foreach ($saveables as $saveable) {
      $saveable->save();
    }
  }

  /**
   * Wraps the scalar value by a Typed Data object.
   *
@@ -529,7 +565,7 @@ class DataTransferObject extends Map {
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The Typed Data object.
   */
  protected function wrapScalarValue($name, $value) {
  protected function wrapScalarValue($name, $value): TypedDataInterface {
    $manager = $this->getTypedDataManager();
    $scalar_type = 'string';
    if (is_numeric($value)) {
@@ -558,7 +594,7 @@ class DataTransferObject extends Map {
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The Typed Data object.
   */
  protected function wrapEntityValue($name, EntityInterface $value) {
  protected function wrapEntityValue($name, EntityInterface $value): TypedDataInterface {
    $manager = $this->getTypedDataManager();
    $instance = $manager->createInstance('entity', [
      'data_definition' => EntityDataDefinition::create($value->getEntityTypeId(), $value->bundle()),
@@ -569,6 +605,25 @@ class DataTransferObject extends Map {
    return $instance;
  }

  /**
   * Wraps the config by a Typed Data object.
   *
   * @param string $name
   *   The property name.
   * @param \Drupal\Core\Config\Config $value
   *   The config.
   *
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The Typed Data object.
   */
  protected function wrapConfigValue($name, Config $value) : TypedDataInterface {
    /** @var \Drupal\Core\config\TypedConfigManager $manager */
    $manager = \Drupal::service('config.typed');
    /** @var \Drupal\Core\TypedData\TraversableTypedDataInterface $typed_config */
    $typed_config = $manager->createFromNameAndData($value->getName(), $value->getRawData());
    return $manager->create($typed_config->getDataDefinition(), $value->getRawData(), $name, $this);
  }

  /**
   * Wraps an iterable value by a Typed Data object.
   *
@@ -580,7 +635,7 @@ class DataTransferObject extends Map {
   * @return \Drupal\Core\TypedData\TypedDataInterface
   *   The Typed Data object.
   */
  protected function wrapIterableValue($name, $value) {
  protected function wrapIterableValue($name, $value): TypedDataInterface {
    $instance = static::create(NULL, $this, $name, FALSE);
    foreach ($value as $k => $v) {
      $instance->set($k, $v, FALSE);
@@ -596,7 +651,7 @@ class DataTransferObject extends Map {
   *   that items before that can safely be skipped (for example, when removing
   *   an item at a given index).
   */
  protected function rekey(int $from_index = 0) {
  protected function rekey(int $from_index = 0): void {
    $assoc = [];
    $sequence = [];
    foreach ($this->properties as $p_name => $p_val) {
@@ -615,4 +670,21 @@ class DataTransferObject extends Map {
    }
  }

  /**
   * Helper method to traverse and collect the traversed elements.
   *
   * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $traversable
   *   The traversable object.
   *
   * @return \Drupal\Core\TypedData\TypedDataInterface[]
   *   The traversed elements.
   */
  protected static function traverseElements(TraversableTypedDataInterface $traversable): array {
    $elements = [];
    foreach ($traversable as $key => $element) {
      $elements[$key] = $element;
    }
    return $elements;
  }

}
+90 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\eca\Kernel;

use Drupal\eca\Plugin\DataType\DataTransferObject;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\user\Entity\User;

/**
 * Kernel tests for data transfer objects.
 *
 * @group eca
 * @group eca_core
 */
class DataTransferObjectTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'filter',
    'text',
    'node',
    'eca',
    'eca_array',
  ];

  /**
   * {@inheritdoc}
   */
  public function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installEntitySchema('node');
    $this->installSchema('user', ['users_data']);
    $this->installSchema('node', ['node_access']);
    $this->installConfig(static::$modules);
    User::create(['uid' => 0, 'name' => 'guest'])->save();
    User::create(['uid' => 1, 'name' => 'admin'])->save();
    // Create an Article content type.
    $node_type = NodeType::create(['type' => 'article', 'name' => 'Article']);
    $node_type->save();
  }

  /**
   * Tests collecting data from multiple sources and saving them at once.
   */
  public function testDtoSave(): void {
    $node = Node::create([
      'type' => 'article',
      'title' => $this->randomMachineName(),
      'status' => TRUE,
      'uid' => 0,
    ]);
    $dto = DataTransferObject::create();
    $dto->set('title', $node->get('title'));
    $user = User::load(1);
    $dto->set('username', $user->get('name'));
    $node_type = NodeType::load('article');
    $dto->set('node_type', $node_type);

    $new_title = $this->randomMachineName();
    $dto->get('title')->setValue($new_title);
    $this->assertEquals($new_title, $node->title->value);

    $new_username = $this->randomMachineName();
    $dto->get('username')->setValue($new_username);
    $this->assertEquals($new_username, $user->name->value);

    $dto->get('node_type')->getValue()->set('name', 'ECA Article');
    $this->assertEquals('ECA Article', $node_type->get('name'));

    $this->assertTrue($node->isNew());
    $dto->saveData();
    $this->assertFalse($node->isNew());
    $node = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($node->id());
    $this->assertEquals($new_title, $node->title->value);
    $user = \Drupal::entityTypeManager()->getStorage('user')->loadUnchanged(1);
    $this->assertEquals($new_username, $user->name->value);
    /** @var \Drupal\node\Entity\NodeType $node_type */
    $node_type = \Drupal::entityTypeManager()->getStorage('node_type')->loadUnchanged('article');
    $this->assertEquals('ECA Article', $node_type->get('name'));
  }

}