Chris@17: entityTypeManager = $entityTypeManager; Chris@17: $this->blockContentStorage = $entityTypeManager->getStorage('block_content'); Chris@17: $this->usage = $usage; Chris@18: if ($section_storage_manager === NULL) { Chris@18: @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); Chris@18: $section_storage_manager = \Drupal::service('plugin.manager.layout_builder.section_storage'); Chris@18: } Chris@18: $this->sectionStorageManager = $section_storage_manager; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public static function create(ContainerInterface $container) { Chris@17: return new static( Chris@17: $container->get('entity_type.manager'), Chris@17: $container->get('inline_block.usage'), Chris@18: $container->get('database'), Chris@18: $container->get('plugin.manager.layout_builder.section_storage') Chris@17: ); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Remove all unused inline blocks on save. Chris@17: * Chris@17: * Entities that were used in prevision revisions will be removed if not Chris@17: * saving a new revision. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@17: * The parent entity. Chris@17: */ Chris@17: protected function removeUnusedForEntityOnSave(EntityInterface $entity) { Chris@17: // If the entity is new or '$entity->original' is not set then there will Chris@17: // not be any unused inline blocks to remove. Chris@17: // If this is a revisionable entity then do not remove inline blocks. They Chris@17: // could be referenced in previous revisions even if this is not a new Chris@17: // revision. Chris@17: if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) { Chris@17: return; Chris@17: } Chris@18: // If the original entity used the default storage then we cannot remove Chris@18: // unused inline blocks because they will still be referenced in the Chris@18: // defaults. Chris@18: if ($this->originalEntityUsesDefaultStorage($entity)) { Chris@17: return; Chris@17: } Chris@17: Chris@17: // Delete and remove the usage for inline blocks that were removed. Chris@17: if ($removed_block_ids = $this->getRemovedBlockIds($entity)) { Chris@17: $this->deleteBlocksAndUsage($removed_block_ids); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets the IDs of the inline blocks that were removed. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@17: * The layout entity. Chris@17: * Chris@17: * @return int[] Chris@17: * The block content IDs that were removed. Chris@17: */ Chris@17: protected function getRemovedBlockIds(EntityInterface $entity) { Chris@17: $original_sections = $this->getEntitySections($entity->original); Chris@17: $current_sections = $this->getEntitySections($entity); Chris@17: // Avoid un-needed conversion from revision IDs to block content IDs by Chris@17: // first determining if there are any revisions in the original that are not Chris@17: // also in the current sections. Chris@17: $current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections); Chris@17: $original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections); Chris@17: if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) { Chris@17: // If there are any revisions in the original that aren't in the current Chris@17: // there may some blocks that need to be removed. Chris@17: $current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids); Chris@17: $unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids); Chris@17: return array_diff($unused_original_block_content_ids, $current_block_content_ids); Chris@17: } Chris@17: return []; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Handles entity tracking on deleting a parent entity. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@17: * The parent entity. Chris@17: */ Chris@17: public function handleEntityDelete(EntityInterface $entity) { Chris@18: // @todo In https://www.drupal.org/node/3008943 call Chris@18: // \Drupal\layout_builder\LayoutEntityHelperTrait::isLayoutCompatibleEntity(). Chris@18: $this->usage->removeByLayoutEntity($entity); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Handles saving a parent entity. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@17: * The parent entity. Chris@17: */ Chris@17: public function handlePreSave(EntityInterface $entity) { Chris@17: if (!$this->isLayoutCompatibleEntity($entity)) { Chris@17: return; Chris@17: } Chris@17: $duplicate_blocks = FALSE; Chris@17: Chris@17: if ($sections = $this->getEntitySections($entity)) { Chris@18: if ($this->originalEntityUsesDefaultStorage($entity)) { Chris@18: // This is a new override from a default and the blocks need to be Chris@18: // duplicated. Chris@18: $duplicate_blocks = TRUE; Chris@17: } Chris@17: $new_revision = FALSE; Chris@17: if ($entity instanceof RevisionableInterface) { Chris@17: // If the parent entity will have a new revision create a new revision Chris@17: // of the block. Chris@17: // @todo Currently revisions are never created for the parent entity. Chris@17: // This will be fixed in https://www.drupal.org/node/2937199. Chris@17: // To work around this always make a revision when the parent entity Chris@17: // is an instance of RevisionableInterface. After the issue is fixed Chris@17: // only create a new revision if '$entity->isNewRevision()'. Chris@17: $new_revision = TRUE; Chris@17: } Chris@17: Chris@17: foreach ($this->getInlineBlockComponents($sections) as $component) { Chris@17: $this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks); Chris@17: } Chris@17: } Chris@17: $this->removeUnusedForEntityOnSave($entity); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets a block ID for an inline block plugin. Chris@17: * Chris@17: * @param \Drupal\layout_builder\Plugin\Block\InlineBlock $block_plugin Chris@17: * The inline block plugin. Chris@17: * Chris@17: * @return int Chris@17: * The block content ID or null none available. Chris@17: */ Chris@17: protected function getPluginBlockId(InlineBlock $block_plugin) { Chris@17: $configuration = $block_plugin->getConfiguration(); Chris@17: if (!empty($configuration['block_revision_id'])) { Chris@17: $revision_ids = $this->getBlockIdsForRevisionIds([$configuration['block_revision_id']]); Chris@17: return array_pop($revision_ids); Chris@17: } Chris@17: return NULL; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Delete the inline blocks and the usage records. Chris@17: * Chris@17: * @param int[] $block_content_ids Chris@17: * The block content entity IDs. Chris@17: */ Chris@17: protected function deleteBlocksAndUsage(array $block_content_ids) { Chris@17: foreach ($block_content_ids as $block_content_id) { Chris@17: if ($block = $this->blockContentStorage->load($block_content_id)) { Chris@17: $block->delete(); Chris@17: } Chris@17: } Chris@17: $this->usage->deleteUsage($block_content_ids); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Removes unused inline blocks. Chris@17: * Chris@17: * @param int $limit Chris@17: * The maximum number of inline blocks to remove. Chris@17: */ Chris@17: public function removeUnused($limit = 100) { Chris@17: $this->deleteBlocksAndUsage($this->usage->getUnused($limit)); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets blocks IDs for an array of revision IDs. Chris@17: * Chris@17: * @param int[] $revision_ids Chris@17: * The revision IDs. Chris@17: * Chris@17: * @return int[] Chris@17: * The block IDs. Chris@17: */ Chris@17: protected function getBlockIdsForRevisionIds(array $revision_ids) { Chris@17: if ($revision_ids) { Chris@17: $query = $this->blockContentStorage->getQuery(); Chris@17: $query->condition('revision_id', $revision_ids, 'IN'); Chris@17: $block_ids = $query->execute(); Chris@17: return $block_ids; Chris@17: } Chris@17: return []; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Saves an inline block component. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@17: * The entity with the layout. Chris@17: * @param \Drupal\layout_builder\SectionComponent $component Chris@17: * The section component with an inline block. Chris@17: * @param bool $new_revision Chris@17: * Whether a new revision of the block should be created. Chris@17: * @param bool $duplicate_blocks Chris@17: * Whether the blocks should be duplicated. Chris@17: */ Chris@17: protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) { Chris@17: /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ Chris@17: $plugin = $component->getPlugin(); Chris@17: $pre_save_configuration = $plugin->getConfiguration(); Chris@17: $plugin->saveBlockContent($new_revision, $duplicate_blocks); Chris@17: $post_save_configuration = $plugin->getConfiguration(); Chris@17: if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) { Chris@17: $this->usage->addUsage($this->getPluginBlockId($plugin), $entity); Chris@17: } Chris@17: $component->setConfiguration($post_save_configuration); Chris@17: } Chris@17: Chris@17: }