workflow-8.x-1.x-dev/src/Entity/WorkflowState.php

src/Entity/WorkflowState.php
<?php

namespace Drupal\workflow\Entity;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Serialization\Attribute\JsonSchema;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workflow\WorkflowTypeAttributeTrait;
use Drupal\workflow\WorkflowURLRouteParametersTrait;

/**
 * Workflow configuration entity to persistently store configuration.
 *
 * @ConfigEntityType(
 *   id = "workflow_state",
 *   label = @Translation("Workflow state"),
 *   label_singular = @Translation("Workflow state"),
 *   label_plural = @Translation("Workflow states"),
 *   label_count = @PluralTranslation(
 *     singular = "@count Workflow state",
 *     plural = "@count Workflow states",
 *   ),
 *   module = "workflow",
 *   static_cache = TRUE,
 *   translatable = FALSE,
 *   handlers = {
 *     "access" = "Drupal\workflow\WorkflowAccessControlHandler",
 *     "list_builder" = "Drupal\workflow\WorkflowStateListBuilder",
 *     "form" = {
 *        "delete" = "Drupal\Core\Entity\EntityDeleteForm",
 *      }
 *   },
 *   config_prefix = "state",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight",
 *   },
 *   config_export = {
 *     "id",
 *     "label",
 *     "weight",
 *     "module",
 *     "wid",
 *     "sysid",
 *     "status",
 *     "single_state_widget",
 *   },
 *   links = {
 *     "canonical" = "/admin/config/workflow/workflow/{workflow_type}",
 *     "collection" = "/admin/config/workflow/workflow/{workflow_type}/states",
 *   },
 * )
 */
class WorkflowState extends ConfigEntityBase implements WorkflowStateInterface, MarkupInterface {

  /*
   * Add variables and get/set methods for Workflow property.
   */
  use WorkflowTypeAttributeTrait;

  /*
   * Add translation trait.
   */
  use StringTranslationTrait;

  /*
   * Provide URL route parameters for entity links.
   */
  use WorkflowURLRouteParametersTrait;

  /*
   * The default weight for creation state, to have it on top of state list.
   */
  private const CREATION_DEFAULT_WEIGHT = -50;

  /*
   * The internal value to determine the creation state.
   */
  private const CREATION_STATE = 1;

  /*
   * A value to initially create 1 new creation state, once per Workflow type, without encoded workflow_type.
   */
  public const CREATION_STATE_NAME = 'creation';
  /*
   * The fixed to-be-translated label of the creation state.
   */
  private const CREATION_STATE_LABEL = 'Creation';

  /**
   * The machine name.
   *
   * @var string
   */
  public $id;

  /**
   * The human readable name.
   *
   * @var string
   */
  public $label;

  /**
   * The weight of this Workflow state.
   *
   * @var int
   */
  public $weight;

  /**
   * The module implementing this object, for config_export.
   *
   * @var string
   */

  protected $module = 'workflow';

  /**
   * The fixed System ID.
   *
   * @var int
   */
  public $sysid = 0;

  /**
   * Indicator if the State can be used or not.
   *
   * @var int
   */
  public $status = 1;

  /**
   * Widget behaviour when user has only 1 transition option.
   *
   * @var string
   */
  protected $single_state_widget = '';

  /**
   * CRUD functions.
   */

  /**
   * Constructs the object.
   *
   * @param array $values
   *   The list of values.
   * @param string $entity_type_id
   *   The name of the new State. If '(creation)', a CreationState is generated.
   */
  public function __construct(array $values = [], $entity_type_id = 'workflow_state') {
    $sid = $values['id'] ?? NULL;
    $values['label'] ??= $sid ?? '';

    // Set default values for '(creation)' state.
    // This only happens when a creation state is explicitly created.
    if ($sid == self::CREATION_STATE_NAME) {
      $values['sysid'] = self::CREATION_STATE;
      $values['weight'] = self::CREATION_DEFAULT_WEIGHT;
      // Do not translate the machine_name.
      $values['label'] = $this->t(self::CREATION_STATE_LABEL);
    }
    parent::__construct($values, $entity_type_id);
  }

  /**
   * {@inheritdoc}
   *
   * Calls static::label() and is used in workflow_allowed_state_names().
   */
  #[JsonSchema(['type' => 'string', 'description' => 'Workflow State name'])]
  public function __toString() {
    $label = $this->t('@label', ['@label' => $this->label()])->__toString();
    return $label;
  }

  /**
   * {@inheritdoc}
   *
   * Required for check on MarkupInterface in views\filter\WorkflowState.
   */
  public function jsonSerialize(): mixed {
    return $this->__toString();
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    parent::calculateDependencies();
    // We cannot use $this->getWorkflow()->getConfigDependencyName() because
    // calling $this->getWorkflow() here causes an infinite loop.
    /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $workflow_type */
    $workflow_type = \Drupal::entityTypeManager()->getDefinition('workflow_type');
    $this->addDependency('config', "{$workflow_type->getConfigPrefix()}.{$this->getWorkflowId()}");
    return $this;
  }

  /**
   * {@inheritdoc}
   *
   * Avoids error on WorkflowStateListBuilder:
   * "Cannot load the "workflow_state" entity with NULL ID."
   *
   * @return \Drupal\workflow\Entity\WorkflowState|null
   *   The state.
   */
  public static function load($id): ?WorkflowState {
    // Call loadMultiple, making use of module cache.
    $states = static::loadMultiple([$id]);
    return reset($states) ?: NULL;
  }

  /**
   * Get all states in the system, with options to filter, only where a workflow exists.
   *
   * {@inheritdoc}
   *
   * @param array $ids
   *   An array of State IDs, or NULL to load all states.
   * @param string $wid
   *   The requested Workflow ID.
   * @param bool $reset
   *   An option to refresh all caches.
   *
   * @return \Drupal\workflow\Entity\WorkflowState[]
   *   An array of cached states, keyed by state_id.
   *
   * @_deprecated WorkflowState::getStates() ==> WorkflowState::loadMultiple()
   */
  public static function loadMultiple(?array $ids = NULL, $wid = '', $reset = FALSE): array {
    /** @var \Drupal\workflow\Entity\WorkflowState[] $states */
    static $states = NULL;
    // Avoid PHP8.2 Error: Constant expression contains invalid operations.
    if (!$states && $states ??= parent::loadMultiple()) {
      // Sort the States on state weight.
      // @todo Sort States via 'orderby: weight' in schema file.
      uasort($states, [
        'Drupal\workflow\Entity\WorkflowState',
        'sort',
      ]);
    }

    // Filter on Wid, if requested, E.g., by Workflow->getStates().
    // Set the ID as array key.
    $result = [];
    // Make parameter $ids more robust.
    $ids ??= [];
    foreach ($states as $sid => $state) {
      if (!$wid || ($wid == $state?->getWorkflowId())) {
        if (empty($ids) || in_array($sid, $ids)) {
          $result[$sid] = $state;
        }
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function save($create_creation_state = TRUE): int {
    // Create the machine_name for new states.
    // N.B. Keep machine_name aligned in WorkflowState and ~ListBuilder.
    $sid = $this->id();
    $wid = $this->getWorkflowId();
    $label = $this->label();

    // Set the workflow-including machine_name.
    if ($sid == self::CREATION_STATE_NAME) {
      // Set the initial sid.
      $sid = implode('_', [$wid, $sid]);
      $this->set('id', $sid);
    }
    elseif (empty($sid)) {
      if ($label) {
        $transliteration = \Drupal::service('transliteration');
        $value = $transliteration->transliterate($label, LanguageInterface::LANGCODE_DEFAULT, '_');
        $value = strtolower($value);
        $value = preg_replace('/[^a-z0-9_]+/', '_', $value);
        $sid = implode('_', [$wid, $value]);
      }
      else {
        workflow_debug(__FILE__, __FUNCTION__, __LINE__);  // @todo D8-port: still test this snippet.
        $sid = "state_$sid";
        $sid = implode('_', [$wid, $sid]);
      }
      $this->set('id', $sid);
    }

    return parent::save();
  }

  /**
   * {@inheritdoc}
   */
  public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b): int {
    /** @var \Drupal\workflow\Entity\WorkflowState $a */
    /** @var \Drupal\workflow\Entity\WorkflowState $b */
    $a_wid = $a->getWorkflowId();
    $b_wid = $b->getWorkflowId();
    $sort_order = $a_wid <=> $b_wid;
    if ($a_wid == $b_wid) {
      $a_weight = $a->getWeight();
      $b_weight = $b->getWeight();
      $sort_order = $a_weight <=> $b_weight;
    }
    return $sort_order;
  }

  /**
   * Deactivate a Workflow State, moving existing content to a given State.
   *
   * @param string $new_sid
   *   The state ID, to which all affected entities must be moved.
   *
   * @return \Drupal\workflow\Entity\WorkflowState
   *   The state.
   */
  public function deactivate($new_sid): static {

    $current_sid = $this->id();
    $force = TRUE;

    // Notify interested modules. We notify first to allow access to data before we zap it.
    // - re-parents any entity that we don't want to orphan, whilst deactivating a State.
    // - delete any lingering entity To state values.
    // \Drupal::moduleHandler()->invokeAll('workflow', ['state delete', $current_sid, $new_sid, NULL, $force]);
    // Invoke the hook.
    $entity_type_id = $this->getEntityTypeId();
    \Drupal::moduleHandler()->invokeAll("entity_{$entity_type_id}_predelete", [$this, $current_sid, $new_sid]);

    // Re-parent any entity that we don't want to orphan, whilst deactivating a State.
    // @todo D8-port: State should not know about Transition: move this to Workflow->DeactivateState.
    if ($new_sid) {
      // A candidate for the batch API.
      // @todo Future updates should seriously consider setting this with batch.
      $comment = $this->t('Previous state deleted');

      foreach (_workflow_info_fields() as $field_info) {
        $entity_type_id = $field_info->getTargetEntityTypeId();
        $field_name = $field_info->getName();

        $result = [];
        // CommentWithWorkflow's are not re-parented upon Deactivate WorkflowState.
        if (!WorkflowManager::isTargetCommentEntity($field_info)) {
          $result = \Drupal::entityQuery($entity_type_id)
            ->condition($field_name, $current_sid, '=')
            ->accessCheck(FALSE)
            ->execute();
        }

        foreach ($result as $entity_id) {
          $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
          $transition = WorkflowTransition::create([
            'from_sid' => $current_sid,
            'field_name' => $field_name,
          ])
            ->setTargetEntity($entity)
            ->setValues($new_sid, NULL, NULL, $comment, TRUE)
            ->force($force);

          // Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history.
          // For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself.
          // Execute transition and update the target entity.
          $new_sid = $transition->executeAndUpdateEntity($force);
        }
      }
    }

    // Delete the transitions this state is involved in.
    $workflow = Workflow::load($this->getWorkflowId());
    /** @var \Drupal\workflow\Entity\WorkflowInterface $workflow */
    /** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
    foreach ($workflow->getTransitionsByStateId($current_sid, '') as $transition) {
      $transition->delete();
    }
    foreach ($workflow->getTransitionsByStateId('', $current_sid) as $transition) {
      $transition->delete();
    }

    // Delete the state. -- We don't actually delete, just deactivate.
    // This is a matter up for some debate, to delete or not to delete, since this
    // causes name conflicts for states. In the meantime, we just stick with what we know.
    // If you really want to delete the states, use workflow_cleanup module, or delete().
    $this->status = FALSE;
    $this->save();

    // Clear the cache.
    self::loadMultiple(NULL, '', TRUE);

    return $this;
  }

  /**
   * Property functions.
   */

  /**
   * {@inheritdoc}
   */
  public function getWeight(): int {
    return $this->weight;
  }

  /**
   * Returns the Workflow object of this State.
   *
   * @return bool
   *   TRUE if state is active, else FALSE.
   */
  public function isActive(): bool {
    return (bool) $this->status;
  }

  /**
   * Checks if the given state is the 'Create' state.
   *
   * @return bool
   *   TRUE if the state is the Creation state, else FALSE.
   */
  public function isCreationState(): bool {
    return $this->sysid == self::CREATION_STATE;
  }

  /**
   * Returns the allowed transitions for the current state.
   *
   * @param \Drupal\Core\Entity\EntityInterface|null $entity
   *   The entity at hand. May be NULL (E.g., on a Field settings page).
   * @param string $field_name
   *   The field name.
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   The user account.
   * @param bool $force
   *   The force indicator.
   *
   * @return \Drupal\workflow\Entity\WorkflowConfigTransition[]
   *   An array of id=>transition pairs with allowed transitions for State.
   */
  public function getTransitions(?EntityInterface $entity = NULL, $field_name = '', ?AccountInterface $account = NULL, $force = FALSE): array {
    $transitions = [];
    $workflow = $this->getWorkflow();

    if (!$workflow) {
      // No workflow, no options ;-)
      return $transitions;
    }

    // Load a User object, since we cannot add Roles to AccountInterface.
    if (!$user = workflow_current_user($account)) {
      // In some edge cases, no user is provided.
      return $transitions;
    }

    /*
     * Get user's permissions.
     */
    // N.B. Keep aligned between WorkflowState, ~Transition, ~HistoryAccess.
    if ($force) {
      // Do nothing.
      $force = TRUE;
    }
    elseif ($user->isSuperUser($this)) {
      // Get permission from admin/people/permissions page.
      // Superuser is special (might be cron).
      // And $force allows Rules to cause transition.
      $force = TRUE;
    }
    elseif ($user->isOwner($entity)) {
      $user->addOwnerRole($entity);
    }

    // Determine if user has Access to each transition.
    $transitions = $workflow->getTransitionsByStateId($this->id(), '');
    foreach ($transitions as $key => $transition) {
      if (!$transition->isAllowed($user, $force)) {
        unset($transitions[$key]);
      }
    }

    // Let custom code add/remove/alter the available transitions,
    // using the new drupal_alter.
    // Modules may veto a choice by removing a transition from the list.
    // Lots of data can be fetched via the $transition object.
    $context = [
      'entity' => $entity, // ConfigEntities do not have entity attached.
      'field_name' => $field_name, // Or field.
      'user' => $user, // User may have the custom WorkflowRole::AUTHOR_RID.
      'workflow' => $workflow,
      'state' => $this,
      'force' => $force,
    ];
    \Drupal::moduleHandler()->alter('workflow_permitted_state_transitions', $transitions, $context);

    return $transitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getWorkflowId(): ?string {
    return $this->wid;
  }

  /**
   * Returns the allowed values for the current state.
   *
   * @param \Drupal\Core\Entity\EntityInterface|null $entity
   *   The entity at hand. May be NULL (E.g., on a Field settings page).
   * @param string $field_name
   *   The field name.
   * @param \Drupal\Core\Session\AccountInterface|null $account
   *   The user object.
   * @param bool $force
   *   The force indicator.
   * @param bool $use_cache
   *   The indicator to use earlier, cached, results.
   *
   * @return array
   *   An array of [sid => transition->label()] pairs.
   *   If $this->id() is set, returns the allowed transitions from this state.
   *   If $this->id() is 0 or FALSE, then labels of ALL states of the State's
   *   Workflow are returned.
   */
  public function getOptions(?EntityInterface $entity, $field_name, ?AccountInterface $account = NULL, $force = FALSE, $use_cache = TRUE): array {
    $options = [];

    $workflow = $this->getWorkflow();
    if (!$workflow) {
      // No workflow, no options ;-)
      return $options;
    }

    $current_sid = $this->id();

    // Define an Entity-specific cache per page load.
    static $cache = [];
    // @todo Use cache only for existing entities;
    // $use_cache &= !$entity->isNew();
    if ($use_cache) {
      // Create a single cache key instead of deep array nesting.
      $entity_type_index = $entity?->getEntityTypeId() ?? '';
      $entity_index = $entity?->id() ?? '';
      $sid_index = $current_sid ?? '';
      $cache_key = "{$entity_type_index}:{$entity_index}:{$sid_index}:{$force}";

      if (isset($cache[$cache_key])) {
        $options = $cache[$cache_key];
        return $options;
      }
    }

    $transitions = [];
    if (!$current_sid) {
      // If no State ID is given (on Field settings page), we return all states.
      // We cannot use getTransitions, since there are no ConfigTransitions
      // from state with ID 0, and we do not want to repeat States.
      // @see https://www.drupal.org/project/workflow/issues/3119998
      // @see WorkflowState::__toString().
      $options = $workflow->getStates(WorkflowInterface::ACTIVE_CREATION_STATES);
    }
    elseif ($current_sid) {
      // This is called by FormatterBase->view();
      // which calls WorkflowItem->getPossibleOptions();
      $transitions = $this->getTransitions($entity, $field_name, $account, $force);
    }
    elseif ($entity->{$field_name}?->getStateId() ?? NULL) {
      // Note: Avoid recursive calling.
      // @todo Is this code now obsolete in v2.x?
      $transition = $entity->{$field_name}?->getTransition();
      $transitions = $this->getTransitions($transition, $field_name, $account, $force);
    }
    else {
      // Empty field. Entity is created before enabling Workflow module.
      $options = $workflow->getStates();
    }

    // Return the transitions (for better label()), with state ID.
    foreach ($transitions as $transition) {
      $to_sid = $transition->to_sid;
      // @see WorkflowConfigTransition::__toString().
      // When Transition->to_sid is 'entity_reference',
      // do string conversion here, to avoid Error:
      // "Call to undefined method WorkflowConfigTransition::render()
      // "in Drupal\Core\Template\Attribute->__toString()
      // (line 329 of \Drupal\Core\Template\Attribute.php).
      $options[$to_sid] = (string) $transition;
    }

    // Save to entity-specific cache.
    if ($use_cache) {
      $cache[$cache_key] = $options;
    }

    return $options;
  }

  /**
   * Returns the number of entities with this state.
   *
   * @return int
   *   Counted number.
   *
   * @todo Add $options to select on entity type, etc.
   */
  public function count(): int {
    $count = 0;
    $sid = $this->id();

    foreach (_workflow_info_fields() as $field_info) {
      $field_name = $field_info->getName();
      $query = \Drupal::entityQuery($field_info->getTargetEntityTypeId());
      $count += $query
        ->condition($field_name, $sid, '=')
        ->accessCheck(FALSE)
        ->count()
        ->execute();
    }

    return $count;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc