Verified Commit 20a2005e authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3422821 by phenaproxima, alexpott: Add a config action to add entity...

Issue #3422821 by phenaproxima, alexpott: Add a config action to add entity types and bundles to a Content Moderation workflow

(cherry picked from commit b4fc09885e286797b37da9026f3521ddd65daa54)
parent d5049df6
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@ variables:
  MYSQL_DATABASE: mysql
  MYSQL_USER: drupaltestbot
  MYSQL_PASSWORD: drupaltestbotpw
  TEST_DIRECTORIES: "core/tests/Drupal/Tests/Core/Recipe core/tests/Drupal/KernelTests/Core/Recipe core/tests/Drupal/FunctionalTests/Core/Recipe core/tests/Drupal/KernelTests/Core/Config/Action core/tests/Drupal/KernelTests/Core/Config/Storage/Checkpoint core/tests/Drupal/Tests/Core/Config/Checkpoint core/tests/Drupal/Tests/Core/Config/Action"
  CODE_DIRECTORIES: "core/lib/Drupal/Core/Recipe core/lib/Drupal/Core/Config/Action core/modules/config/tests/config_action_duplicate_test core/tests/fixtures/recipes core/lib/Drupal/Core/Config/Checkpoint"
  TEST_DIRECTORIES: "core/tests/Drupal/Tests/Core/Recipe core/tests/Drupal/KernelTests/Core/Recipe core/tests/Drupal/FunctionalTests/Core/Recipe core/tests/Drupal/KernelTests/Core/Config/Action core/tests/Drupal/KernelTests/Core/Config/Storage/Checkpoint core/tests/Drupal/Tests/Core/Config/Checkpoint core/tests/Drupal/Tests/Core/Config/Action core/modules/content_moderation/tests/src/Kernel/ConfigAction"
  CODE_DIRECTORIES: "core/lib/Drupal/Core/Recipe core/lib/Drupal/Core/Config/Action core/modules/config/tests/config_action_duplicate_test core/tests/fixtures/recipes core/lib/Drupal/Core/Config/Checkpoint core/modules/content_moderation/src/Plugin/ConfigAction"
  ALL_DIRECTORIES: "${CODE_DIRECTORIES} ${TEST_DIRECTORIES}"

default:
+74 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\content_moderation\Plugin\ConfigAction;

use Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface;
use Drupal\Core\Config\Action\Attribute\ConfigAction;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Config\Action\ConfigActionPluginInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\workflows\WorkflowInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

#[ConfigAction(
  id: 'add_moderation',
  entity_types: ['workflow'],
  deriver: AddModerationDeriver::class,
)]
final class AddModeration implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {

  public function __construct(
    private readonly ConfigManagerInterface $configManager,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly string $pluginId,
    private readonly string $targetEntityType,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    assert(is_array($plugin_definition));
    $target_entity_type = $plugin_definition['target_entity_type'];

    return new static(
      $container->get(ConfigManagerInterface::class),
      $container->get(EntityTypeManagerInterface::class),
      $plugin_id,
      $target_entity_type,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function apply(string $configName, mixed $value): void {
    $workflow = $this->configManager->loadConfigEntityByName($configName);
    assert($workflow instanceof WorkflowInterface);

    $plugin = $workflow->getTypePlugin();
    if (!$plugin instanceof ContentModerationInterface) {
      throw new ConfigActionException("The $this->pluginId config action only works with Content Moderation workflows.");
    }

    assert($value === '*' || is_array($value));
    if ($value === '*') {
      /** @var \Drupal\Core\Entity\EntityTypeInterface $definition */
      $definition = $this->entityTypeManager->getDefinition($this->targetEntityType);
      /** @var string $bundle_entity_type */
      $bundle_entity_type = $definition->getBundleEntityType();

      $value = $this->entityTypeManager->getStorage($bundle_entity_type)
        ->getQuery()
        ->accessCheck(FALSE)
        ->execute();
    }
    foreach ($value as $bundle) {
      $plugin->addEntityTypeAndBundle($this->targetEntityType, $bundle);
    }
    $workflow->save();
  }

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

declare(strict_types=1);

namespace Drupal\content_moderation\Plugin\ConfigAction;

// cspell:ignore inflector

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\String\Inflector\EnglishInflector;

final class AddModerationDeriver extends DeriverBase implements ContainerDeriverInterface {

  use StringTranslationTrait;

  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id): static {
    return new static(
      $container->get(EntityTypeManagerInterface::class),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $inflector = new EnglishInflector();

    foreach ($this->entityTypeManager->getDefinitions() as $id => $entity_type) {
      if ($bundle_entity_type = $entity_type->getBundleEntityType()) {
        /** @var \Drupal\Core\Entity\EntityTypeInterface $bundle_entity_type */
        $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type);
        // Convert unique plugin IDs, like `taxonomy_vocabulary`, into strings
        // like `TaxonomyVocabulary`.
        $suffix = Container::camelize($bundle_entity_type->id());
        [$suffix] = $inflector->pluralize($suffix);
        $this->derivatives["add{$suffix}"] = [
          'target_entity_type' => $id,
          'admin_label' => $this->t('Add moderation to all @bundles', [
            '@bundles' => $bundle_entity_type->getPluralLabel() ?: $bundle_entity_type->id(),
          ]),
        ] + $base_plugin_definition;
      }
    }
    return parent::getDerivativeDefinitions($base_plugin_definition);
  }

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

namespace Drupal\Tests\content_moderation\Kernel\ConfigAction;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Recipe\Recipe;
use Drupal\Core\Recipe\RecipeRunner;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
use Drupal\workflows\Entity\Workflow;

/**
 * @covers \Drupal\content_moderation\Plugin\ConfigAction\AddModeration
 * @covers \Drupal\content_moderation\Plugin\ConfigAction\AddModerationDeriver
 * @group content_moderation
 * @group Recipe
 */
class AddModerationConfigActionTest extends KernelTestBase {

  use ContentTypeCreationTrait;
  use TaxonomyTestTrait;

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

  public function testAddEntityTypeAndBundle(): void {
    $this->installConfig('node');

    $this->createContentType(['type' => 'a']);
    $this->createContentType(['type' => 'b']);
    $this->createContentType(['type' => 'c']);
    $this->createVocabulary(['vid' => 'tags']);

    $recipe = $this->createRecipe('workflows.workflow.editorial');
    RecipeRunner::processRecipe($recipe);

    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface $plugin */
    $plugin = Workflow::load('editorial')?->getTypePlugin();
    $this->assertSame(['a', 'b'], $plugin->getBundlesForEntityType('node'));
    $this->assertSame(['tags'], $plugin->getBundlesForEntityType('taxonomy_term'));
  }

  public function testWorkflowMustBeContentModeration(): void {
    $this->enableModules(['workflows', 'workflow_type_test']);

    $workflow = Workflow::create([
      'id' => 'test',
      'label' => 'Test workflow',
      'type' => 'workflow_type_test',
    ]);
    $workflow->save();

    $recipe = $this->createRecipe($workflow->getConfigDependencyName());
    $this->expectException(ConfigActionException::class);
    $this->expectExceptionMessage("The add_moderation:addNodeTypes config action only works with Content Moderation workflows.");
    RecipeRunner::processRecipe($recipe);
  }

  public function testActionOnlyTargetsWorkflows(): void {
    $recipe = $this->createRecipe('user.role.anonymous');
    $this->expectException(PluginNotFoundException::class);
    $this->expectExceptionMessage('The "addNodeTypes" plugin does not exist.');
    RecipeRunner::processRecipe($recipe);
  }

  public function testDeriverAdminLabel(): void {
    $this->enableModules(['workflows', 'content_moderation']);

    /** @var array<string, array{admin_label: \Stringable}> $definitions */
    $definitions = $this->container->get('plugin.manager.config_action')
      ->getDefinitions();

    $this->assertSame('Add moderation to all content types', (string) $definitions['add_moderation:addNodeTypes']['admin_label']);
    $this->assertSame('Add moderation to all vocabularies', (string) $definitions['add_moderation:addTaxonomyVocabularies']['admin_label']);
  }

  private function createRecipe(string $config_name): Recipe {
    $dir = uniqid('public://');
    mkdir($dir);

    $recipe = <<<YAML
name: 'Add entity types and bundles to workflow'
recipes:
  - editorial_workflow
config:
  actions:
    $config_name:
      addNodeTypes:
        - a
        - b
      addTaxonomyVocabularies: '*'
YAML;
    file_put_contents($dir . '/recipe.yml', $recipe);
    return Recipe::createFromDirectory($dir);
  }

}
+15 −0
Original line number Diff line number Diff line
@@ -365,6 +365,21 @@ parameters:
			count: 1
			path: core/lib/Drupal/Core/Recipe/UnknownRecipeException.php

		-
			message: "#^Method Drupal\\\\content_moderation\\\\Plugin\\\\ConfigAction\\\\AddModeration\\:\\:create\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#"
			count: 1
			path: core/modules/content_moderation/src/Plugin/ConfigAction/AddModeration.php

		-
			message: "#^Method Drupal\\\\content_moderation\\\\Plugin\\\\ConfigAction\\\\AddModerationDeriver\\:\\:getDerivativeDefinitions\\(\\) has parameter \\$base_plugin_definition with no value type specified in iterable type array\\.$#"
			count: 1
			path: core/modules/content_moderation/src/Plugin/ConfigAction/AddModerationDeriver.php

		-
			message: "#^Method Drupal\\\\content_moderation\\\\Plugin\\\\ConfigAction\\\\AddModerationDeriver\\:\\:getDerivativeDefinitions\\(\\) return type has no value type specified in iterable type array\\.$#"
			count: 1
			path: core/modules/content_moderation/src/Plugin/ConfigAction/AddModerationDeriver.php

		-
			message: "#^PHPDoc tag @var for variable \\$sync_data has no value type specified in iterable type array\\.$#"
			count: 1