Commit 4db47231 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3302666 by alexpott, bircher: Add the ability to call the same method...

Issue #3302666 by alexpott, bircher: Add the ability to call the same method multiple times via config actions
parent 90bc662c
Loading
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\Core\Config\Action\Attribute;

// cspell:ignore inflector
use Drupal\Core\Config\Action\Exists;
use Drupal\Core\StringTranslation\TranslatableMarkup;

@@ -17,8 +18,22 @@ final class ActionMethod {
   *   Determines behavior of action depending on entity existence.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|string $adminLabel
   *   The admin label for the user interface.
   * @param bool|string $pluralize
   *   Determines whether to create a pluralized version of the method to enable
   *   the action to be called multiple times before saving the entity. The
   *   default behaviour is to create an action with a plural form as determined
   *   by \Symfony\Component\String\Inflector\EnglishInflector::pluralize().
   *   For example, 'grantPermission' has a pluralized version of
   *   'grantPermissions'. If a string is provided this will be the full action
   *   ID. For example, if the method is called 'addArray' this can be set to
   *   'addMultipleArrays'. Set to FALSE if a pluralized version does not make
   *   logical sense.
   */
  public function __construct(public readonly Exists $exists = Exists::ERROR_IF_NOT_EXISTS, public readonly TranslatableMarkup|string $adminLabel = '') {
  public function __construct(
    public readonly Exists $exists = Exists::ERROR_IF_NOT_EXISTS,
    public readonly TranslatableMarkup|string $adminLabel = '',
    public readonly bool|string $pluralize = TRUE
  ) {
  }

}
+5 −0
Original line number Diff line number Diff line
@@ -7,6 +7,11 @@ interface ConfigActionPluginInterface {
  /**
   * Applies the config action.
   *
   * @param string $configName
   *   The name of the config to apply the action to.
   * @param mixed $value
   *   The value for the action to use.
   *
   * @throws ConfigActionException
   */
  public function apply(string $configName, mixed $value): void;
+45 −3
Original line number Diff line number Diff line
@@ -2,14 +2,18 @@

namespace Drupal\Core\Config\Action\Plugin\ConfigAction\Deriver;

// cspell:ignore inflector
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\Action\EntityMethodException;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\String\Inflector\EnglishInflector;
use Symfony\Component\String\Inflector\InflectorInterface;

/**
 * Derives config action methods from attributed config entity methods.
@@ -21,6 +25,11 @@ final class EntityMethodDeriver extends DeriverBase implements ContainerDeriverI

  use StringTranslationTrait;

  /**
   * Inflector to pluralize words.
   */
  protected readonly InflectorInterface $inflector;

  /**
   * Constructs new EntityMethodDeriver.
   *
@@ -28,6 +37,7 @@ final class EntityMethodDeriver extends DeriverBase implements ContainerDeriverI
   *   The entity type manager.
   */
  public function __construct(protected readonly EntityTypeManagerInterface $entityTypeManager) {
    $this->inflector = new EnglishInflector();
  }

  /**
@@ -53,19 +63,31 @@ final class EntityMethodDeriver extends DeriverBase implements ContainerDeriverI
            /** @var \Drupal\Core\Config\Action\Attribute\ActionMethod  $action_attribute */
            $action_attribute = $attribute->newInstance();

            $derivative['admin_label'] = $action_attribute->adminLabel ?: $this->t('@entity_type @method', [$entity_type->getLabel(), $method->name]);
            $derivative['admin_label'] = $action_attribute->adminLabel ?: $this->t('@entity_type @method', ['@entity_type' => $entity_type->getLabel(), '@method' => $method->name]);
            $derivative['constructor_args'] = [
              'method' => $method->name,
              'exists' => $action_attribute->exists,
              'numberOfParams' => $method->getNumberOfParameters(),
              'numberOfRequiredParams' => $method->getNumberOfRequiredParameters(),
              'pluralized' => FALSE,
            ];
            $derivative['entity_types'] = [$entity_type->id()];
            // Build a config action identifier from the entity type's config
            // prefix  and the method name. For example, the Role entity adds a
            // 'user.role:grantPermission' action.
            $derivative_id = $entity_type->getConfigPrefix() . PluginBase::DERIVATIVE_SEPARATOR . $method->name;
            $this->derivatives[$derivative_id] = $derivative;
            $this->addDerivative($method->name, $entity_type, $derivative, $method->name);

            $pluralized_name = match(TRUE) {
              is_string($action_attribute->pluralize) => $action_attribute->pluralize,
              $action_attribute->pluralize === FALSE => '',
              default => $this->inflector->pluralize($method->name)[0]
            };
            // Add a pluralized version of the plugin.
            if (strlen($pluralized_name) > 0) {
              $derivative['constructor_args']['pluralized'] = TRUE;
              $derivative['admin_label'] = $this->t('@admin_label (multiple calls)', ['@admin_label' => $derivative['admin_label']]);
              $this->addDerivative($pluralized_name, $entity_type, $derivative, $method->name);
            }
          }
        }
      }
@@ -73,4 +95,24 @@ final class EntityMethodDeriver extends DeriverBase implements ContainerDeriverI
    return $this->derivatives;
  }

  /**
   * Adds a derivative.
   *
   * @param string $action_id
   *   The action ID.
   * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type
   *   The entity type.
   * @param array $derivative
   *   The derivative definition.
   * @param string $methodName
   *   The method name.
   */
  private function addDerivative(string $action_id, ConfigEntityTypeInterface $entity_type, array $derivative, string $methodName): void {
    $id = $entity_type->getConfigPrefix() . PluginBase::DERIVATIVE_SEPARATOR . $action_id;
    if (isset($this->derivatives[$id])) {
      throw new EntityMethodException(sprintf('Duplicate action can not be created for ID \'%s\' for %s::%s(). The existing action is for the ::%s() method', $id, $entity_type->getClass(), $methodName, $this->derivatives[$id]['constructor_args']['method']));
    }
    $this->derivatives[$id] = $derivative;
  }

}
+43 −2
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ use Drupal\Core\Config\Action\ConfigActionPluginInterface;
use Drupal\Core\Config\Action\EntityMethodException;
use Drupal\Core\Config\Action\Exists;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

@@ -53,6 +54,8 @@ final class EntityMethod implements ConfigActionPluginInterface, ContainerFactor
   *   The number of parameters the method has.
   * @param int $numberOfRequiredParams
   *   The number of required parameters the method has.
   * @param bool $pluralized
   *   Determines whether an array maps to multiple calls.
   */
  public function __construct(
    protected readonly string $pluginId,
@@ -60,7 +63,8 @@ final class EntityMethod implements ConfigActionPluginInterface, ContainerFactor
    protected readonly string $method,
    protected readonly Exists $exists,
    protected readonly int $numberOfParams,
    protected readonly int $numberOfRequiredParams
    protected readonly int $numberOfRequiredParams,
    protected readonly bool $pluralized
  ) {
  }

@@ -86,6 +90,43 @@ final class EntityMethod implements ConfigActionPluginInterface, ContainerFactor
      return;
    }

    $entity = $this->pluralized ? $this->applyPluralized($entity, $value) : $this->applySingle($entity, $value);
    $entity->save();
  }

  /**
   * Apply the action to entity treating the $values array as multiple calls.
   *
   * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
   *   The entity to apply the action to.
   * @param mixed $values
   *   The values for the action to use.
   *
   * @return \Drupal\Core\Config\Entity\ConfigEntityInterface
   *   The unsaved entity with the action applied.
   */
  private function applyPluralized(ConfigEntityInterface $entity, mixed $values): ConfigEntityInterface {
    if (!is_array($values)) {
      throw new EntityMethodException(sprintf('The pluralized entity method config action \'%s\' requires an array value in order to call %s::%s() multiple times', $this->pluginId, $entity->getEntityType()->getClass(), $this->method));
    }
    foreach ($values as $value) {
      $entity = $this->applySingle($entity, $value);
    }
    return $entity;
  }

  /**
   * Apply the action to entity treating the $values array a single call.
   *
   * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
   *   The entity to apply the action to.
   * @param mixed $value
   *   The value for the action to use.
   *
   * @return \Drupal\Core\Config\Entity\ConfigEntityInterface
   *   The unsaved entity with the action applied.
   */
  private function applySingle(ConfigEntityInterface $entity, mixed $value): ConfigEntityInterface {
    // If $value is not an array then we only support calling the method if the
    // number of parameters or required parameters is 1. If there is only 1
    // parameter and $value is an array then assume that the parameter expects
@@ -99,7 +140,7 @@ final class EntityMethod implements ConfigActionPluginInterface, ContainerFactor
    else {
      $entity->{$this->method}(...$value);
    }
    $entity->save();
    return $entity;
  }

}
+4 −0
Original line number Diff line number Diff line
@@ -40,4 +40,8 @@ function config_test_entity_type_alter(array &$entity_types) {
  if (\Drupal::service('state')->get('config_test.lookup_keys', FALSE)) {
    $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']);
  }

  if (\Drupal::service('state')->get('config_test.class_override', FALSE)) {
    $entity_types['config_test']->setClass(\Drupal::service('state')->get('config_test.class_override'));
  }
}
Loading