You are here

InlineBlockEntityOperations.php in Drupal 8

Same filename and directory in other branches
  1. 9 core/modules/layout_builder/src/InlineBlockEntityOperations.php

File

core/modules/layout_builder/src/InlineBlockEntityOperations.php
View source
<?php

namespace Drupal\layout_builder;

use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\layout_builder\Plugin\Block\InlineBlock;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines a class for reacting to entity events related to Inline Blocks.
 *
 * @internal
 *   This is an internal utility class wrapping hook implementations.
 */
class InlineBlockEntityOperations implements ContainerInjectionInterface {
  use LayoutEntityHelperTrait;

  /**
   * Inline block usage tracking service.
   *
   * @var \Drupal\layout_builder\InlineBlockUsageInterface
   */
  protected $usage;

  /**
   * The block content storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $blockContentStorage;

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

  /**
   * Constructs a new EntityOperations object.
   *
   * @todo This constructor has one optional parameter, $section_storage_manager
   *    and one totally unused $database parameter. Deprecate the current
   *    constructor signature in https://www.drupal.org/node/3031492 after the
   *    general policy for constructor backwards compatibility is determined in
   *    https://www.drupal.org/node/3030640.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager service.
   * @param \Drupal\layout_builder\InlineBlockUsageInterface $usage
   *   Inline block usage tracking service.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
   *   (optional) The section storage manager.
   *
   * @todo The current constructor signature is deprecated:
   *   - The $section_storage_manager parameter is optional, but should become
   *   required.
   *   - The $database parameter is unused and should be removed.
   *   Deprecate in https://www.drupal.org/node/3031492.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsageInterface $usage, Connection $database, SectionStorageManagerInterface $section_storage_manager = NULL) {
    $this->entityTypeManager = $entityTypeManager;
    $this->blockContentStorage = $entityTypeManager
      ->getStorage('block_content');
    $this->usage = $usage;
    if ($section_storage_manager === NULL) {
      @trigger_error('The plugin.manager.layout_builder.section_storage service must be passed to \\Drupal\\layout_builder\\InlineBlockEntityOperations::__construct(). It was added in Drupal 8.7.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
      $section_storage_manager = \Drupal::service('plugin.manager.layout_builder.section_storage');
    }
    $this->sectionStorageManager = $section_storage_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity_type.manager'), $container
      ->get('inline_block.usage'), $container
      ->get('database'), $container
      ->get('plugin.manager.layout_builder.section_storage'));
  }

  /**
   * Remove all unused inline blocks on save.
   *
   * Entities that were used in prevision revisions will be removed if not
   * saving a new revision.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The parent entity.
   */
  protected function removeUnusedForEntityOnSave(EntityInterface $entity) {

    // If the entity is new or '$entity->original' is not set then there will
    // not be any unused inline blocks to remove.
    // If this is a revisionable entity then do not remove inline blocks. They
    // could be referenced in previous revisions even if this is not a new
    // revision.
    if ($entity
      ->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) {
      return;
    }

    // If the original entity used the default storage then we cannot remove
    // unused inline blocks because they will still be referenced in the
    // defaults.
    if ($this
      ->originalEntityUsesDefaultStorage($entity)) {
      return;
    }

    // Delete and remove the usage for inline blocks that were removed.
    if ($removed_block_ids = $this
      ->getRemovedBlockIds($entity)) {
      $this
        ->deleteBlocksAndUsage($removed_block_ids);
    }
  }

  /**
   * Gets the IDs of the inline blocks that were removed.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The layout entity.
   *
   * @return int[]
   *   The block content IDs that were removed.
   */
  protected function getRemovedBlockIds(EntityInterface $entity) {
    $original_sections = $this
      ->getEntitySections($entity->original);
    $current_sections = $this
      ->getEntitySections($entity);

    // Avoid un-needed conversion from revision IDs to block content IDs by
    // first determining if there are any revisions in the original that are not
    // also in the current sections.
    $current_block_content_revision_ids = $this
      ->getInlineBlockRevisionIdsInSections($current_sections);
    $original_block_content_revision_ids = $this
      ->getInlineBlockRevisionIdsInSections($original_sections);
    if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) {

      // If there are any revisions in the original that aren't in the current
      // there may some blocks that need to be removed.
      $current_block_content_ids = $this
        ->getBlockIdsForRevisionIds($current_block_content_revision_ids);
      $unused_original_block_content_ids = $this
        ->getBlockIdsForRevisionIds($unused_original_revision_ids);
      return array_diff($unused_original_block_content_ids, $current_block_content_ids);
    }
    return [];
  }

  /**
   * Handles entity tracking on deleting a parent entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The parent entity.
   */
  public function handleEntityDelete(EntityInterface $entity) {

    // @todo In https://www.drupal.org/node/3008943 call
    //   \Drupal\layout_builder\LayoutEntityHelperTrait::isLayoutCompatibleEntity().
    $this->usage
      ->removeByLayoutEntity($entity);
  }

  /**
   * Handles saving a parent entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The parent entity.
   */
  public function handlePreSave(EntityInterface $entity) {
    if (!$this
      ->isLayoutCompatibleEntity($entity)) {
      return;
    }
    $duplicate_blocks = FALSE;
    if ($sections = $this
      ->getEntitySections($entity)) {
      if ($this
        ->originalEntityUsesDefaultStorage($entity)) {

        // This is a new override from a default and the blocks need to be
        // duplicated.
        $duplicate_blocks = TRUE;
      }
      $new_revision = FALSE;
      if ($entity instanceof RevisionableInterface) {

        // If the parent entity will have a new revision create a new revision
        // of the block.
        // @todo Currently revisions are never created for the parent entity.
        //   This will be fixed in https://www.drupal.org/node/2937199.
        //   To work around this always make a revision when the parent entity
        //   is an instance of RevisionableInterface. After the issue is fixed
        //   only create a new revision if '$entity->isNewRevision()'.
        $new_revision = TRUE;
      }
      foreach ($this
        ->getInlineBlockComponents($sections) as $component) {
        $this
          ->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks);
      }
    }
    $this
      ->removeUnusedForEntityOnSave($entity);
  }

  /**
   * Gets a block ID for an inline block plugin.
   *
   * @param \Drupal\layout_builder\Plugin\Block\InlineBlock $block_plugin
   *   The inline block plugin.
   *
   * @return int
   *   The block content ID or null none available.
   */
  protected function getPluginBlockId(InlineBlock $block_plugin) {
    $configuration = $block_plugin
      ->getConfiguration();
    if (!empty($configuration['block_revision_id'])) {
      $revision_ids = $this
        ->getBlockIdsForRevisionIds([
        $configuration['block_revision_id'],
      ]);
      return array_pop($revision_ids);
    }
    return NULL;
  }

  /**
   * Delete the inline blocks and the usage records.
   *
   * @param int[] $block_content_ids
   *   The block content entity IDs.
   */
  protected function deleteBlocksAndUsage(array $block_content_ids) {
    foreach ($block_content_ids as $block_content_id) {
      if ($block = $this->blockContentStorage
        ->load($block_content_id)) {
        $block
          ->delete();
      }
    }
    $this->usage
      ->deleteUsage($block_content_ids);
  }

  /**
   * Removes unused inline blocks.
   *
   * @param int $limit
   *   The maximum number of inline blocks to remove.
   */
  public function removeUnused($limit = 100) {
    $this
      ->deleteBlocksAndUsage($this->usage
      ->getUnused($limit));
  }

  /**
   * Gets blocks IDs for an array of revision IDs.
   *
   * @param int[] $revision_ids
   *   The revision IDs.
   *
   * @return int[]
   *   The block IDs.
   */
  protected function getBlockIdsForRevisionIds(array $revision_ids) {
    if ($revision_ids) {
      $query = $this->blockContentStorage
        ->getQuery();
      $query
        ->condition('revision_id', $revision_ids, 'IN');
      $block_ids = $query
        ->execute();
      return $block_ids;
    }
    return [];
  }

  /**
   * Saves an inline block component.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity with the layout.
   * @param \Drupal\layout_builder\SectionComponent $component
   *   The section component with an inline block.
   * @param bool $new_revision
   *   Whether a new revision of the block should be created.
   * @param bool $duplicate_blocks
   *   Whether the blocks should be duplicated.
   */
  protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) {

    /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */
    $plugin = $component
      ->getPlugin();
    $pre_save_configuration = $plugin
      ->getConfiguration();
    $plugin
      ->saveBlockContent($new_revision, $duplicate_blocks);
    $post_save_configuration = $plugin
      ->getConfiguration();
    if ($duplicate_blocks || empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id'])) {
      $this->usage
        ->addUsage($this
        ->getPluginBlockId($plugin), $entity);
    }
    $component
      ->setConfiguration($post_save_configuration);
  }

}

Classes

Namesort descending Description
InlineBlockEntityOperations Defines a class for reacting to entity events related to Inline Blocks.