You are here

EntityOperations.php in Workbench Moderation 8

Same filename and directory in other branches
  1. 8.2 src/EntityOperations.php

File

src/EntityOperations.php
View source
<?php

namespace Drupal\workbench_moderation;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\workbench_moderation\Event\WorkbenchModerationEvents;
use Drupal\workbench_moderation\Event\WorkbenchModerationTransitionEvent;
use Drupal\workbench_moderation\Form\EntityModerationForm;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Defines a class for reacting to entity events.
 */
class EntityOperations {

  /**
   * Moderation interface.
   *
   * @var \Drupal\workbench_moderation\ModerationInformationInterface
   */
  protected $moderationInfo;

  /**
   * An entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * The form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * Revision tracker.
   *
   * @var \Drupal\workbench_moderation\RevisionTrackerInterface
   */
  protected $tracker;

  /**
   * The current user service.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new EntityOperations object.
   *
   * @param \Drupal\workbench_moderation\ModerationInformationInterface $moderation_info
   *   Moderation information service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager service.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The form builder.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\workbench_moderation\RevisionTrackerInterface $tracker
   *   The revision tracker.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user service.
   */
  public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EventDispatcherInterface $event_dispatcher, RevisionTrackerInterface $tracker, AccountInterface $current_user) {
    $this->moderationInfo = $moderation_info;
    $this->entityTypeManager = $entity_type_manager;
    $this->eventDispatcher = $event_dispatcher;
    $this->formBuilder = $form_builder;
    $this->tracker = $tracker;
    $this->currentUser = $current_user;
  }

  /**
   * Hook bridge.
   *
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
   *   An array of entity objects that have just been loaded.
   * @param string $entity_type_id
   *   The type of entity being loaded, such as "node" or "user".
   *
   * @see hook_entity_storage_load()
   */
  public function entityStorageLoad(array $entities, $entity_type_id) {

    // Ensure that all moderatable entities always have a moderation_state field
    // with data, in all translations. That avoids us needing to have a thousand
    // NULL checks elsewhere in the code.
    // Quickly exclude any non-moderatable entities.
    $to_check = array_filter($entities, [
      $this->moderationInfo,
      'isModeratableEntity',
    ]);
    if (!$to_check) {
      return;
    }

    // @todo make this more functional, less iterative.
    foreach ($to_check as $entity) {
      foreach ($entity
        ->getTranslationLanguages() as $language) {
        $translation = $entity
          ->getTranslation($language
          ->getId());
        if ($translation->moderation_state->target_id == NULL) {
          $translation->moderation_state->target_id = $this
            ->getDefaultLoadStateId($translation);
        }
      }
    }
  }

  /**
   * Determines the default moderation state on load for an entity.
   *
   * This method is only applicable when an entity is loaded that has
   * no moderation state on it, but should. In those cases, failing to set
   * one may result in NULL references elsewhere when other code tries to check
   * the moderation state of the entity.
   *
   * The amount of indirection here makes performance a concern, but
   * given how Entity API works I don't know how else to do it.
   * This reliably gets us *A* valid state. However, that state may be
   * not the ideal one. Suggestions on how to better select the default
   * state here are welcome.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity for which we want a default state.
   *
   * @return string
   *   The default state for the given entity.
   */
  protected function getDefaultLoadStateId(ContentEntityInterface $entity) {
    return $this->moderationInfo
      ->loadBundleEntity($entity
      ->getEntityType()
      ->getBundleEntityType(), $entity
      ->bundle())
      ->getThirdPartySetting('workbench_moderation', 'default_moderation_state');
  }

  /**
   * Acts on an entity and set published status based on the moderation state.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   */
  public function entityPresave(EntityInterface $entity) {
    if (!$this->moderationInfo
      ->isModeratableEntity($entity)) {
      return;
    }
    if ($entity->moderation_state->entity) {
      $published_state = $entity->moderation_state->entity
        ->isPublishedState();

      // This entity is default if it is new, the default revision, or the
      // default revision is not published.
      $update_default_revision = $entity
        ->isNew() || $entity->moderation_state->entity
        ->isDefaultRevisionState() || !$this
        ->isDefaultRevisionPublished($entity);

      // Fire per-entity-type logic for handling the save process.
      $this->entityTypeManager
        ->getHandler($entity
        ->getEntityTypeId(), 'moderation')
        ->onPresave($entity, $update_default_revision, $published_state);

      // There's currently a bug in core where $entity->original always points
      // to the default revision, for now work around this by loading the latest
      // revision.
      $latest_revision = $this->moderationInfo
        ->getLatestRevision($entity
        ->getEntityTypeId(), $entity
        ->id());
      $state_before = !empty($latest_revision) ? $latest_revision->moderation_state->target_id : NULL;

      // @todo: Revert to this simpler version when https://www.drupal.org/node/2700747 is fixed.
      // $state_before = isset($entity->original) ?
      // $entity->original->moderation_state->target_id : NULL;
      $state_after = $entity->moderation_state->target_id;

      // Allow other modules to respond to the transition. Note that this
      // does not provide any mechanism to cancel the transition, since
      // Entity API doesn't allow hook_entity_presave to short-circuit a save.
      $event = new WorkbenchModerationTransitionEvent($entity, $state_before, $state_after);
      $this->eventDispatcher
        ->dispatch(WorkbenchModerationEvents::STATE_TRANSITION, $event);
    }
  }

  /**
   * Hook bridge.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that was just saved.
   *
   * @see hook_entity_insert()
   */
  public function entityInsert(EntityInterface $entity) {
    if (!$this->moderationInfo
      ->isModeratableEntity($entity)) {
      return;
    }

    /* ContentEntityInterface $entity */

    // Update our own record keeping.
    $this->tracker
      ->setLatestRevision($entity
      ->getEntityTypeId(), $entity
      ->id(), $entity
      ->language()
      ->getId(), $entity
      ->getRevisionId());
  }

  /**
   * Hook bridge.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that was just saved.
   *
   * @see hook_entity_update()
   */
  public function entityUpdate(EntityInterface $entity) {
    if (!$this->moderationInfo
      ->isModeratableEntity($entity)) {
      return;
    }

    /* ContentEntityInterface $entity */

    // Update our own record keeping.
    $this->tracker
      ->setLatestRevision($entity
      ->getEntityTypeId(), $entity
      ->id(), $entity
      ->language()
      ->getId(), $entity
      ->getRevisionId());
  }

  /**
   * Act on entities being assembled before rendering.
   *
   * This is a hook bridge.
   *
   * @see hook_entity_view()
   * @see EntityFieldManagerInterface::getExtraFields()
   */
  public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
    if (!$this->moderationInfo
      ->isModeratableEntity($entity)) {
      return;
    }
    if (!$this->moderationInfo
      ->isLatestRevision($entity)) {
      return;
    }
    if ($this->moderationInfo
      ->isLiveRevision($entity)) {
      return;
    }
    $component = $display
      ->getComponent('workbench_moderation_control');
    if ($component) {
      $build['workbench_moderation_control'] = $this->formBuilder
        ->getForm(EntityModerationForm::class, $entity);
      $build['workbench_moderation_control']['#weight'] = $component['weight'];
      $build['workbench_moderation_control']['#access'] = $this->currentUser
        ->hasPermission('moderate entities that cannot edit') || $entity
        ->access('update');
    }
  }

  /**
   * Check if the default revision for the given entity is published.
   *
   * The default revision is the same as the entity retrieved by "default" from
   * the storage handler. If the entity is translated, use the default revision
   * of the same language as the given entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   *
   * @return bool
   *   TRUE if the default revision is published. FALSE otherwise.
   */
  protected function isDefaultRevisionPublished(EntityInterface $entity) {
    $storage = $this->entityTypeManager
      ->getStorage($entity
      ->getEntityTypeId());
    $default_revision = $storage
      ->load($entity
      ->id());

    // Ensure we are comparing the same translation as the current entity.
    if ($default_revision instanceof TranslatableInterface && $default_revision
      ->isTranslatable()) {

      // If there is no translation, then there is no default revision and is
      // therefore not published.
      if (!$default_revision
        ->hasTranslation($entity
        ->language()
        ->getId())) {
        return FALSE;
      }
      $default_revision = $default_revision
        ->getTranslation($entity
        ->language()
        ->getId());
    }
    return $default_revision && $default_revision->moderation_state->entity
      ->isPublishedState();
  }

}

Classes

Namesort descending Description
EntityOperations Defines a class for reacting to entity events.