drowl_paragraphs-8.x-3.9/modules/drowl_paragraphs_container2layout/src/ParagraphsConverter.php

modules/drowl_paragraphs_container2layout/src/ParagraphsConverter.php
<?php

namespace Drupal\drowl_paragraphs_container2layout;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList;
use Drupal\Core\Entity\EntityTypeBundleInfo;

/**
 * @file See https://www.drupal.org/project/drowl_paragraphs/issues/3261150
 */

/**
 * Provides methods for converting container paragraphs to layout paragraphs.
 */
class ParagraphsConverter implements ParagraphsConverterInterface {

  use MessengerTrait;
  use StringTranslationTrait;

  const SUBPARAGRAPHS_FIELDNAME = 'field_paragraphs_paragraphs';
  const PARAGRAPHS_SETTINGS_FIELDNAME = 'field_paragraph_settings';
  const PARAGRAPH_BUNDLE_CONTAINER_NAME = 'container';
  const PARAGRAPH_BUNDLE_LAYOUT_NAME = 'layout';
  const COLUMNS_PER_ROW = 12;

  /**
   * Config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

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

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * Language manager for retrieving the default Langcode.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Database connection.
   *
   * @var Drupal\Core\Database\Connection
   */
  protected $databaseConnection;

  /**
   * Entity type bundle info service.
   *
   * @var Drupal\Core\Entity\EntityTypeBundleInfo
   */
  protected $entityTypeBundleInfo;

  /**
   * Creates a new ParagraphsConverter.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, LanguageManagerInterface $language_manager, Connection $database_connection, EntityTypeBundleInfo $entity_type_bundle_info) {
    $this->configFactory = $config_factory;
    $this->moduleHandler = $module_handler;
    $this->entityTypeManager = $entity_type_manager;
    $this->entityFieldManager = $entity_field_manager;
    $this->languageManager = $language_manager;
    $this->databaseConnection = $database_connection;
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
  }

  /**
   * @inheritDoc
   */
  public function checkRequirements() {
    // Ensure a layout paragraph bundle exists (with layout enabled).
    // Ensure a container paragraph bundle exists.
    $paragraphBundleInfo = $this->entityTypeBundleInfo->getBundleInfo('paragraph');
    if (!array_key_exists(self::PARAGRAPH_BUNDLE_LAYOUT_NAME, $paragraphBundleInfo)) {
      throw new \Exception('No paragraph bundle type "layout" found! Did you create the layout paragraph type?');
    }

    if (!array_key_exists(self::PARAGRAPH_BUNDLE_CONTAINER_NAME, $paragraphBundleInfo)) {
      throw new \Exception('No paragraph bundle type "container" found. This migration is for paragraph containers!');
    }

    // Ensure layout has the same fields as container for conversion:
    $paragraphBundleLayoutFieldDefinitions = $this->entityFieldManager->getFieldDefinitions('paragraph', self::PARAGRAPH_BUNDLE_LAYOUT_NAME);
    $paragraphBundleContainerFieldDefinitions = $this->entityFieldManager->getFieldDefinitions('paragraph', self::PARAGRAPH_BUNDLE_CONTAINER_NAME);

    // array_diff() is not a real diff... just one direction -.-
    $fullDiff = array_merge(array_diff_key($paragraphBundleLayoutFieldDefinitions, $paragraphBundleContainerFieldDefinitions), array_diff_key($paragraphBundleContainerFieldDefinitions, $paragraphBundleLayoutFieldDefinitions));

    // Ensure "layout" at least has the same fields as "container".
    // For the migration we need all fields to be existent in layout.
    // Also "field_paragraphs_paragraphs" must be present while migrating.
    // Afterwards these fields can be removed.
    if (!empty($fullDiff)) {
      throw new \Exception($this->t('For conversion the paragraph type bundle "layout" must have at least all fields from "container" bundle! Even the field_paragraphs_paragraphs is required until conversion is done! The following @count fields are different: @diff', ['@count' => count($fullDiff), '@diff' => implode(', ', array_keys($fullDiff))]));
    }
  }

  // TODO: Work in progress. Batch processing not finished yet!
  // See EntityReferenceRevisionsOrphanPurger.php for a good example!
  // /**
  //  * Batch operation for converting the paragraphs of a parent entity.
  //  *
  //  * @param string $entity_type_id
  //  *   The entity type id, for example 'paragraph'.
  //  * @param Iterable|array $context
  //  *   The context array.
  //  */
  // public function convertEntitiesBatchOperation(ContentEntityInterface $entity, &$context) {
  //   // TODO: Work in progress. Batch processing not finished yet!
  //   $composite_type = $this->entityTypeManager->getDefinition($entity_type_id);
  //   $composite_revision_key = $composite_type->getKey('revision');
  //   /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $composite_storage */
  //   $composite_storage = $this->entityTypeManager->getStorage($entity_type_id);
  //   $batch_size = Settings::get('entity_update_batch_size', 50);

  //   if (empty($context['sandbox']['total'])) {
  //     $context['sandbox']['progress'] = 0;
  //     $context['sandbox']['current_revision_id'] = -1;
  //     $context['sandbox']['total'] = (int) $composite_storage->getQuery()
  //       ->allRevisions()
  //       ->accessCheck(FALSE)
  //       ->count()
  //       ->execute();
  //   }

  //   if (!isset($context['results'][$entity_type_id])) {
  //     $context['results'][$entity_type_id]['entity_count'] = 0;
  //     $context['results'][$entity_type_id]['revision_count'] = 0;
  //     $context['results'][$entity_type_id]['start'] = $this->time->getRequestTime();
  //   }

  //   // Get the next batch of revision ids from the selected entity type.
  //   // @todo Replace with an entity query on all revisions with a revision ID
  //   //   condition after https://www.drupal.org/project/drupal/issues/2766135.
  //   $revision_table = $composite_type->getRevisionTable();
  //   $entity_revision_ids = $this->database->select($revision_table, 'r')
  //     ->fields('r', [$composite_revision_key])
  //     ->range(0, $batch_size)
  //     ->orderBy($composite_revision_key)
  //     ->condition($composite_revision_key, $context['sandbox']['current_revision_id'], '>')
  //     ->execute()
  //     ->fetchCol();

  //   /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */
  //   foreach ($composite_storage->loadMultipleRevisions($entity_revision_ids) as $composite_revision) {
  //     $context['sandbox']['progress']++;
  //     $context['sandbox']['current_revision_id'] = $composite_revision->getRevisionId();

  //     if ($this->isUsed($composite_revision)) {
  //       continue;
  //     }

  //     if ($this->deleteUnusedRevision($composite_revision)) {
  //       $context['results'][$entity_type_id]['revision_count']++;
  //       if ($composite_revision->isDefaultRevision()) {
  //         $context['results'][$entity_type_id]['entity_count']++;
  //       }
  //     }
  //   }

  //   // This entity type is completed if no new revision ids were found or the
  //   // total is reached.
  //   if ($entity_revision_ids && $context['sandbox']['progress'] < $context['sandbox']['total']) {
  //     $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total'];
  //   }
  //   else {
  //     $context['finished'] = 1;
  //     $context['results'][$entity_type_id]['end'] = $this->time->getRequestTime();
  //   }

  //   $interval = $this->dateFormatter->formatInterval($this->time->getRequestTime() - $context['results'][$entity_type_id]['start']);
  //   $context['message'] = t('Checked @entity_type revisions for orphans: @current of @total in @interval (@deletions deleted)', [
  //     '@entity_type' => $composite_type->getLabel(),
  //     '@current' => $context['sandbox']['progress'],
  //     '@total' => $context['sandbox']['total'],
  //     '@interval' => $interval,
  //     '@deletions' => $context['results'][$entity_type_id]['revision_count'],
  //   ]);
  // }

  // /**
  //  * Batch dispatch submission finished callback.
  //  */
  // public static function batchSubmitFinished($success, $results, $operations) {
  //   // TODO: Work in progress. Batch processing not finished yet!
  //   return \Drupal::service('drowl_paragraphs_container2layout.paragraphs_converter')->doBatchSubmitFinished($success, $results, $operations);
  // }

  // /**
  //  * Sets a batch for executing conversion of container paragraphs to layout paragraphs.
  //  *
  //  * @param array $entity_ids
  //  *   An array of entity ids to process (paragraph parent entities).
  //  */
  // public function setBatch(array $entity_ids) {
  //   // TODO: Work in progress. Batch processing not finished yet!
  //   if (empty($composite_entity_type_ids)) {
  //     return;
  //   }

  //   $operations = [];
  //   foreach ($composite_entity_type_ids as $entity_type_id) {
  //     $operations[] = ['_drowl_paragraphs_container2layout_paragraphs_converter_batch_dispatcher',
  //       [
  //         'drowl_paragraphs_container2layout.paragraphs_converter:convertParagraphsBatchOperation',
  //         $entity_type_id,
  //       ],
  //     ];
  //   }

  //   $batch = [
  //     'operations' => $operations,
  //     'finished' => [self::class, 'batchSubmitFinished'],
  //     'title' => $this->t('Converting container paragraphs to layout paragraphs.'),
  //     'progress_message' => $this->t('Processed @current of @total parent entities potentially containing paragraphs.'),
  //     'error_message' => $this->t('This batch encountered an error.'),
  //   ];
  //   batch_set($batch);
  // }

  // /**
  //  * Finished callback for the batch process.
  //  *
  //  * @param bool $success
  //  *   Whether the batch completed successfully.
  //  * @param array $results
  //  *   The results array.
  //  * @param array $operations
  //  *   The operations array.
  //  */
  // public function doBatchSubmitFinished($success, $results, $operations) {
  //   // TODO: Work in progress. Batch processing not finished yet!
  //   if ($success) {
  //     foreach ($results as $entity_id => $result) {
  //         $this->messenger->addMessage($this->t('@label: Converted @revision_count revisions (@entity_count entities).', [
  //           '@label' => $entity_type->getLabel(),
  //           '@revision_count' => $result['revision_count'],
  //           '@entity_count' => $result['entity_count'],
  //         ]));
  //     }
  //   }
  //   else {
  //     // $operations contains the operations that remained unprocessed.
  //     $error_operation = reset($operations);
  //     $this->messenger->addError($this->t('An error occurred while processing @operation with arguments : @args', [
  //       '@operation' => $error_operation[0],
  //       '@args' => print_r($error_operation[0], TRUE),
  //     ]));
  //   }
  // }

  /**
   * @inheritDoc
   */
  public function convertAllFromAllEntities() {
    // Ensure all requirements we need are met:
    $this->checkRequirements();

    $fieldMap = $this->entityFieldManager->getFieldMapByFieldType('entity_reference_revisions');
    $bundlesToHandle = [];
    if (!empty($fieldMap)) {
      foreach ($fieldMap as $entity_type => $fields) {
        if (!empty($fields) && $entity_type !== 'paragraph') {
          foreach ($fields as $field => $info) {
            $bundlesToHandle[$entity_type] = $info['bundles'];
          }
        }
      }
    }
    if (!empty($bundlesToHandle)) {
      foreach ($bundlesToHandle as $entity_type => $entity_bundles) {
        foreach ($entity_bundles as $entity_bundle) {
          $this->convertAllFromBundle($entity_type, $entity_bundle);
        }
      }
    }
  }

  /**
   * @inheritDoc
   */
  public function convertAllFromBundle(string $entity_type, string $entity_bundle) {
    // Ensure all requirements we need are met:
    $this->checkRequirements();

    if ($entity_type == 'paragraph') {
      // We do not handle conversions on paragraphs here (subparagraphs)
      return FALSE;
    }

    $entityTypeStorage = $this->entityTypeManager->getStorage($entity_type);
    if ($entity_type == 'taxonomy_term') {
      // Taxonomy Terms use "vid" instead of "type" standard sadly due to historic reasons.
      $entities = $entityTypeStorage->loadByProperties(['vid' => $entity_bundle]);
    } else {
      $entities = $entityTypeStorage->loadByProperties(['type' => $entity_bundle]);
    }

    if (!empty($entities)) {
      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
      foreach ($entities as $entity) {
        $this->convertFromParentEntity($entity);
      }
    }
  }

  /**
   * @inheritDoc
   */
  public function convertFromParentEntity(ContentEntityInterface $entity) {
    // Ensure all requirements we need are met:
    $this->checkRequirements();

    $fieldDefinitions = $entity->getFieldDefinitions();
    if (!empty($fieldDefinitions)) {
      foreach ($fieldDefinitions as $fieldName => $fieldDefinition) {
        /** @var \Drupal\field\Entity\FieldConfig $fieldDefinition */
        if ($fieldDefinition->getType() == 'entity_reference_revisions') {
          $this->convertMasterEntityParagraphs($entity, $fieldName);
        }
      }
    }
  }

  /**
   * Converts all entity_reference_revisions field entries of the given $entity
   * from container to layout paragraphs.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   * @param string $fieldName
   * @return false|void
   * @throws \Exception
   */
  protected function convertMasterEntityParagraphs(ContentEntityInterface $entity, string $fieldName) {
    if ($entity->getEntityTypeId() == 'paragraph') {
      // We do not handle conversions on paragraphs here (subparagraphs)
      return FALSE;
    }
    // Get the $entityReferenceRevisions (paragraph) field values:
    /** @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList $referencedParagraphs */
    $masterEntityReferenceRevisions = $entity->get($fieldName);
    $this->iterateBaselevelParagraphs($masterEntityReferenceRevisions);
  }

  /**
   * Helper function to iterate and convert all base level paragraphs.
   *
   * @param EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions
   */
  protected function iterateBaselevelParagraphs(EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions) {
    $subParagraphs = $masterEntityReferenceRevisions->referencedEntities();
    $wasChanged = FALSE;
    foreach ($subParagraphs as $index => $paragraph) {
      $replacement = NULL;
      $bundle = $paragraph->bundle();

      if (!empty($paragraph->getAllBehaviorSettings()['layout_paragraphs'])) {
        // Skip this paragraph it already has layout paragraphs behavior settings.
        // We may assume it has already been processed before then.
        continue;
      }

      switch ($bundle) {
        case self::PARAGRAPH_BUNDLE_CONTAINER_NAME:
          $replacement = $this->handleBaselevelContainer($paragraph, $masterEntityReferenceRevisions, $index);
          break;

        case self::PARAGRAPH_BUNDLE_LAYOUT_NAME:
        case 'anchor':
          // We don't do anything with layouts, anchors, ...
          break;

        default:
          $replacement = $this->handleBaselevelNonContainer($paragraph, $masterEntityReferenceRevisions, $index);
          break;
      }
      if (!empty($replacement)) {
        $masterEntityReferenceRevisions->set($index, $replacement);
        $wasChanged = TRUE;
      }
    }
    if ($wasChanged) {
      // Only save if required.
      $masterEntityReferenceRevisions->getEntity()->save();
    }
  }

  /**
   * Converts a "container" paragraph on the base level of an
   * entity_reference_revisions field to a "layout" paragraph.
   *
   * @param Paragraph $containerParagraph
   * @param EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions
   * @param int $index
   *
   * @return Paragraph
   */
  protected function handleBaselevelContainer(Paragraph $containerParagraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, int $index) {
    if ($containerParagraph->bundle() != self::PARAGRAPH_BUNDLE_CONTAINER_NAME) {
      throw new \Exception('Expected "' . self::PARAGRAPH_BUNDLE_CONTAINER_NAME . '" paragraph type, but "' . $containerParagraph->bundle() . '" paragraph type given.');
    }
    $this->convertContainerParagraphToLayoutParagraph($containerParagraph, $masterEntityReferenceRevisions, '', '');
    return $containerParagraph;
  }

  /**
   * Helper function to handle base level non-container paragraphs.
   *
   * @param Paragraph $paragraph
   * @param EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions
   * @param int $index
   *
   * @return Paragraph
   */
  protected function handleBaselevelNonContainer(Paragraph $paragraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, int $index) {
    if (in_array(
      $paragraph->bundle(),
      [
        self::PARAGRAPH_BUNDLE_CONTAINER_NAME,
        self::PARAGRAPH_BUNDLE_LAYOUT_NAME,
      ]
    )) {
      throw new \Exception('Expected NON "' . self::PARAGRAPH_BUNDLE_CONTAINER_NAME . '" / "' . self::PARAGRAPH_BUNDLE_LAYOUT_NAME . '" paragraph type, but "' . $paragraph->bundle() . '" paragraph type given.');
    }

    // We're wrapping all base level paragraphs into layouts:
    $wrapperLayoutParagraph = $this->createWrapperLayoutParagraph('', '', $index);
    // Layout paragraphs have the master entity as their parent. We have to explicitly set this backreference and we have to set it first due to hook_presave() in layout_paragraphs.module:
    $wrapperLayoutParagraph->setParentEntity($masterEntityReferenceRevisions->getEntity(), $masterEntityReferenceRevisions->getFieldDefinition()->getName());
    $wrapperLayoutParagraph->save();

    // Region is always main here:
    $region = 'main';

    // Delta is always 0 as it's the only element:
    $delta = 0;

    // Now we move the base level paragraph into the layout:
    $this->moveParagraphIntoLayoutParagraph($paragraph, $wrapperLayoutParagraph, $masterEntityReferenceRevisions, $region);

    // Append the original paragraph at the end of the parent entity paragraph field as the original will be replaced by the layout paragrah wrapper.
    $masterEntityReferenceRevisions->appendItem($paragraph);

    return $wrapperLayoutParagraph;
  }

  /**
   * Creates a new wrapper layout paragraphs
   */
  private function createWrapperLayoutParagraph(string $parent_uuid = '', string $region = '', int $parent_delta = 0) {
    $newWrapperLayoutParagraph = Paragraph::create(['type' => self::PARAGRAPH_BUNDLE_LAYOUT_NAME]);
    $newWrapperLayoutParagraph->setBehaviorSettings(
      'layout_paragraphs',
      [
        'parent_uuid' => $parent_uuid,
        'region' => $region,
        // 'parent_delta' => $parent_delta,
        // The following values are static for wrapper paragraphs:
        'layout' => 'drowl_layouts_1col',
        'config' => [
          'label' => 'Layout (Auto-wrapper from container migration)',
          'layout_section_width' => 'viewport-width-cp',
          'layout_align_cells_vertical' => 'stretch',
          'layout_align_cells_horizontal' => 'left',
          'layout_remove_grid_gutter' => [],
          'extra_classes' => 'dp-layout-migrated',
          'layout_variant' => '',
        ],
      ],
    );

    return $newWrapperLayoutParagraph;
  }

  private function moveParagraphIntoLayoutParagraph(Paragraph $paragraph, Paragraph $parentLayoutParagraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, string $intoParentRegion = 'main') {
    if ($parentLayoutParagraph->bundle() !== self::PARAGRAPH_BUNDLE_LAYOUT_NAME) {
      throw new \Exception('Expected the paragraph to move the paragraph into via parent_uuid ' . $parentLayoutParagraph->uuid() . ' to be "' . self::PARAGRAPH_BUNDLE_LAYOUT_NAME . '" but was of type: ' . $parentLayoutParagraph->bundle());
    }
    // Layout paragraphs have the master entity as their parent. We have to explicitly set this backreference and we have to set it first due to hook_presave() in layout_paragraphs.module:
    $paragraph->setParentEntity($masterEntityReferenceRevisions->getEntity(), $masterEntityReferenceRevisions->getFieldDefinition()->getName());

    $paragraph->setBehaviorSettings(
      'layout_paragraphs',
      [
        'parent_uuid' => $parentLayoutParagraph->uuid(),
        'region' => $intoParentRegion,
        // 'parent_delta' => $parentDelta,
        'layout' => '', // Always empty for non-layouts.
        'config' => [], // Always empty for non-layouts.
      ]
    );
    // Save the changes:
    $paragraph->save();

    return $paragraph;
  }

  /**
   * Converts a container bundle to a layout bundle in the database
   * by hard-replacing the bundle name "container" by "layout"
   * and setting the behavior_settings for layout_paragraphs
   */
  private function convertContainerParagraphToLayoutParagraph(Paragraph $containerParagraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, string $parent_uuid = '', string $region = '') {
    if ($containerParagraph->bundle() != self::PARAGRAPH_BUNDLE_CONTAINER_NAME) {
      throw new \Exception('Expected "container" paragraph type, but "' . $containerParagraph->bundle() . '" paragraph type given.');
    }
    $layoutSettings = $this->determineLayoutSettingsFromSubParagraphs($containerParagraph);
    // Layout paragraphs have the master entity as their parent. We have to explicitly set this backreference and we have to set it first due to hook_presave() in layout_paragraphs.module:
    $containerParagraph->setParentEntity($masterEntityReferenceRevisions->getEntity(), $masterEntityReferenceRevisions->getFieldDefinition()->getName());
    $containerParagraph->setBehaviorSettings(
      'layout_paragraphs',
      [
        'parent_uuid' => $parent_uuid,
        'region' => $region,
        // The following values are dynamically determined
        // from (children paragraph) settings:
        'layout' => $layoutSettings['layout'],
        'config' => $layoutSettings['config'],
      ],
    );
    $containerParagraph->save();

    // This has to be done in database as Entities don't allow this dirty change yet...
    /*
    base_table = "paragraphs_item" ($entity_type->getBaseTable())
    data_table = "paragraphs_item_field_data" ($entity_type->getDataTable())
    revision_table = "paragraphs_item_revision" ($entity_type->getRevisionTable())
    revision_data_table = "paragraphs_item_revision_field_data" ($entity_type->getRevisionDataTable())
    */
    $paragraphEntityTypeDefinition = $this->entityTypeManager->getDefinition('paragraph');
    // Wrap this into a transaction:
    try {
      $transaction = $this->databaseConnection->startTransaction();

      // Update the base table, set type from "container" to layout, keep everything else!
      if ($this->databaseConnection->schema()->tableExists($paragraphEntityTypeDefinition->getBaseTable())) {
        $this->databaseConnection->update($paragraphEntityTypeDefinition->getBaseTable())
          ->condition('id', $containerParagraph->id())
          // Do not respect the revision id or langcode as this conversion isn't backwards compatible at all:
          // ->condition('revision_id', $containerParagraph->getRevisionId())
          // ->condition('langcode', $containerParagraph->language()->getId())
          ->condition('type', self::PARAGRAPH_BUNDLE_CONTAINER_NAME)
          ->fields([
            'type' => self::PARAGRAPH_BUNDLE_LAYOUT_NAME,
          ])->execute();
      } else {
        \Drupal::logger('drowl_paragraphs_container2layout')->notice('@function: Base table @table does not exist. Skipping for update.', [
          '@function' => __FUNCTION__,
          '@table' => $paragraphEntityTypeDefinition->getBaseTable(),
        ]);
      }

      // Update the data table, set type from "container" to layout, keep everything else!
      if ($this->databaseConnection->schema()->tableExists($paragraphEntityTypeDefinition->getDataTable())) {
        $this->databaseConnection->update($paragraphEntityTypeDefinition->getDataTable())
          ->condition('id', $containerParagraph->id())
          // Do not respect the revision id or langcode as this conversion isn't backwards compatible at all:
          // ->condition('revision_id', $containerParagraph->getRevisionId())
          // ->condition('langcode', $containerParagraph->language()->getId())
          ->condition('type', self::PARAGRAPH_BUNDLE_CONTAINER_NAME)
          ->fields([
            'type' => self::PARAGRAPH_BUNDLE_LAYOUT_NAME,
          ])->execute();
      } else {
        \Drupal::logger('drowl_paragraphs_container2layout')->notice('@function: Data table @table does not exist. Skipping for update.', [
          '@function' => __FUNCTION__,
          '@table' => $paragraphEntityTypeDefinition->getDataTable(),
        ]);
      }

      // INFO: paragraphs_item_revision and paragraphs_item_revision_field_data don't contain relevant data.

      // Now we also have to change all fields referencing the bundle:
      $paragraphBundleContainerFieldDefinitions = $this->entityFieldManager->getFieldDefinitions('paragraph', self::PARAGRAPH_BUNDLE_CONTAINER_NAME);
      $table_mapping = $this->entityTypeManager->getStorage('paragraph')->getTableMapping();
      $field_table_names = [];
      foreach ($paragraphBundleContainerFieldDefinitions as $field_key => $field) {
        if ($field->getFieldStorageDefinition()->isBaseField() == FALSE) {
          // We do not convert base fields.
          $field_name = $field->getName();
          $field_table = $table_mapping->getFieldTableName($field_name);
          $field_table_names[$field_name] = $field_table;
          $field_storage_definition = $field->getFieldStorageDefinition();
          $field_revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
          // Field revision tables DO have the bundle!
          $field_table_names[$field_name . '_revision'] = $field_revision_table;
        }
      }
      foreach ($field_table_names as $field_table_name) {
        if ($this->databaseConnection->schema()->tableExists($field_table_name)) {
          $this->databaseConnection->update($field_table_name)
            ->condition('entity_id', $containerParagraph->id())
            // Do not respect the revision id or langcode as this conversion isn't backwards compatible at all:
            // ->condition('revision_id', $containerParagraph->getRevisionId())
            // ->condition('langcode', $containerParagraph->language()->getId())
            ->condition('bundle', self::PARAGRAPH_BUNDLE_CONTAINER_NAME)
            ->fields([
              'bundle' => self::PARAGRAPH_BUNDLE_LAYOUT_NAME,
            ])->execute();
        } else {
          \Drupal::logger('drowl_paragraphs_container2layout')->notice('@function: Field table @table does not exist. Skipping for update.', [
            '@function' => __FUNCTION__,
            '@table' => $field_table_name,
          ]);
        }
      }
    } catch (Exception $e) {
      // Something went wrong somewhere, so roll back now.
      $transaction
        ->rollBack();

      // Rethrow exception:
      throw $e;
    }

    // After this hard database changes, we need to reload the container
    // from the database. Otherwise the DB changes are overwritten later from class.
    $containerParagraph = Paragraph::load($containerParagraph->id());

    $this->handleSublevelParagraphs($containerParagraph, $masterEntityReferenceRevisions);
  }

  private function determineLayoutSettingsFromSubParagraphs(Paragraph $containerParagraph) {
    // These are the defaults from drowl_layouts:
    $containerParagraphSectionWith = 'viewport-width-cp';
    $containerParagraphAlignChildrenVertical = 'stretch';
    $containerParagraphAlignChildrenHorizontal = 'left';

    // Determine some settings from the container paragraph settings field:
    if ($containerParagraph->hasField(self::PARAGRAPHS_SETTINGS_FIELDNAME)) {
      $containerParagraphSettingsArray = $containerParagraph->get(self::PARAGRAPHS_SETTINGS_FIELDNAME)->getValue();
      // We can use all these values 1:1 as they did not change from 1.x to 4.x (drowl_layouts):
      $containerParagraphSectionWith = $containerParagraphSettingsArray[0]['layout_section_width'];
      $containerParagraphAlignChildrenVertical = $containerParagraphSettingsArray[0]['layout_align_children_vertical'];
      $containerParagraphAlignChildrenHorizontal = $containerParagraphSettingsArray[0]['layout_align_children_horizontal'];
    } else {
      throw new \Exception('Container paragraph (ID: ' . $containerParagraph->id() . ') has no ' . self::PARAGRAPHS_SETTINGS_FIELDNAME . ' which is required to determine layout settings.');
    }

    [$layoutName, $positionsArray, $columnWidths, $rows] = $this->determineLayoutAndPositions($containerParagraph);
    $layoutSettings = [
      'layout' => $layoutName,
      'config' => [
        'label' => 'Layout (auto-generated from container migration)',
        'layout_section_width' => $containerParagraphSectionWith,
        'layout_align_cells_vertical' => $containerParagraphAlignChildrenVertical,
        'layout_align_cells_horizontal' => $containerParagraphAlignChildrenHorizontal,
        'layout_remove_grid_gutter' => [], // Always empty.
        'extra_classes' => 'dp-sublayout-migrated', // Indicate migrated.
        'layout_variant' => '', // Always empty.
      ],
    ];
    // Add column_widths if used:
    if (!empty($columnWidths)) {
      $layoutSettings['config']['column_widths'] = $columnWidths;
    }

    return $layoutSettings;
  }

  protected function handleSublevelParagraphs(Paragraph $containerParagraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions) {
    [$layoutName, $positionsArray, $columnWidths, $rows] = $this->determineLayoutAndPositions($containerParagraph);

    $subParagraphs = [];

    /** @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList $referencedParagraphs */
    if ($containerParagraph->hasField(self::SUBPARAGRAPHS_FIELDNAME)) {
      $containerSubparagraphsReferenceRevisions = $containerParagraph->get(self::SUBPARAGRAPHS_FIELDNAME);
      $subParagraphs = $containerSubparagraphsReferenceRevisions->referencedEntities();
    }
    foreach ($subParagraphs as $delta => $subParagraph) {
      if (!empty($subParagraph->getAllBehaviorSettings()['layout_paragraphs'])) {
        // Skip this paragraph if it already has layout paragraphs behavior settings.
        // We may assume it has already been processed before then.
        continue;
      }

      $bundle = $subParagraph->bundle();
      if (!isset($positionsArray[$subParagraph->uuid()])) {
        throw new \Exception('Missing position in $positionsArray for paragraph "' . $bundle . '" with UUID: "' . $subParagraph->uuid() . '"');
      }
      $subParagraphColumnIndex = $positionsArray[$subParagraph->uuid()];

      // Now assign the new layout paragraph to the layout:
      $targetRegion = $this->getLayoutRegionByColumnIndex($layoutName, $subParagraphColumnIndex);
      if (in_array($targetRegion, ['top', 'bottom']) && !str_contains($layoutName, '_stacked')) {
        throw new \Exception('The the paragraph type "' . $bundle . '" with UUID: "' . $subParagraph->uuid() . '" should be moved into stacked region "' . $targetRegion . '", but the detected parent container layout ' . $layoutName . ' is not stacked.');
      }

      switch ($bundle) {
        case self::PARAGRAPH_BUNDLE_CONTAINER_NAME:
          $this->handleSublevelContainer($subParagraph, $masterEntityReferenceRevisions, $targetRegion, $containerParagraph);
          break;

        case self::PARAGRAPH_BUNDLE_LAYOUT_NAME:
        case 'anchor':
          // We don't do anything with layouts, anchors, ...
          break;

        default:
          $this->handleSublevelNonContainer($subParagraph, $masterEntityReferenceRevisions, $targetRegion, $containerParagraph);
          break;
      }

      // DO NOT DO THE FOLLOWING HERE, IT LEADS TO DUPLICATES!:
      // $masterEntityReferenceRevisions->appendItem($paragraph);
      // END

      // As someone thought it was a clever idea to rekey the indexes at each remove
      // We have to create this workaround and delete from the end to not
      // affect the index numbers -.-
      // Otherwise we get "Unable to remove item at non-existing index."
      // @see https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21TypedData%21Plugin%21DataType%21ItemList.php/function/ItemList%3A%3AremoveItem/8.2.x
      $deltasToRemove[] = $delta;
    }

    if (!empty($deltasToRemove)) {
      $deltasToRemoveReversed = array_reverse($deltasToRemove);
      foreach ($deltasToRemoveReversed as $deltaToRemove) {
        // Now remove the sublevel paragraph from field_paragraphs_paragraphs.
        $containerSubparagraphsReferenceRevisions->removeItem($deltaToRemove);
      }

      // We need to save the parent entity to finally remove the elements from the field:
      $containerSubparagraphsReferenceRevisions->getEntity()->save();
    }
  }

  protected function handleSublevelContainer(Paragraph $containerParagraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, string $targetRegion, Paragraph $parentContainerParagraph) {
    if ($containerParagraph->bundle() != self::PARAGRAPH_BUNDLE_CONTAINER_NAME) {
      throw new \Exception('Expected "' . self::PARAGRAPH_BUNDLE_CONTAINER_NAME . '" paragraph type, but "' . $containerParagraph->bundle() . '" paragraph type given.');
    }
    $this->convertContainerParagraphToLayoutParagraph($containerParagraph, $masterEntityReferenceRevisions, $parentContainerParagraph->uuid(), $targetRegion);
  }

  protected function handleSublevelNonContainer(Paragraph $paragraph, EntityReferenceRevisionsFieldItemList $masterEntityReferenceRevisions, string $targetRegion, Paragraph $parentLayoutParagraph) {
    if (in_array(
      $paragraph->bundle(),
      [
        self::PARAGRAPH_BUNDLE_CONTAINER_NAME,
        self::PARAGRAPH_BUNDLE_LAYOUT_NAME,
      ]
    )) {
      throw new \Exception('Expected NON "' . self::PARAGRAPH_BUNDLE_CONTAINER_NAME . '" / "' . self::PARAGRAPH_BUNDLE_LAYOUT_NAME . '" paragraph type, but "' . $paragraph->bundle() . '" paragraph type given.');
    }
    $this->moveParagraphIntoLayoutParagraph($paragraph, $parentLayoutParagraph, $masterEntityReferenceRevisions, $targetRegion);

    // Append the original paragraph at the end of the master entity paragraph field as the original will be replaced by the layout paragrah wrapper.
    $masterEntityReferenceRevisions->appendItem($paragraph);
  }

  /**
   * Helper function to determine the layout of the parent $containerParagraph
   * and the positions of the subparagraphs in it.
   *
   * The function determines an array of
   * [$layoutName, $positionsArray, $columnWidths, $rows] where
   * - the $layoutName is the name of the determined layout
   * - the $positionsArray is an array of paragraph UUIDs and their position numbers (as we can first determine the layout after all iterations, so their positions have to be mapped later)
   * - the $columnWidths is NULL (if unused for the layout) or the string of the column width like '50-50' or '33-33-33'.
   * - the $rows is the number of total rows with columns excluding top and bottom.
   */
  protected function determineLayoutAndPositions(Paragraph $containerParagraph) {
    $layoutStacked = FALSE;
    $layoutColumns = 1;
    $positionsArray = [];

    $rowColSizeSum = 0;
    $rows = 0;
    $colPos = 0;

    $subParagraphs = [];
    if ($containerParagraph->hasField(self::SUBPARAGRAPHS_FIELDNAME)) {
      /** @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList $referencedParagraphs */
      $containerSubparagraphsReferenceRevisions = $containerParagraph->get(self::SUBPARAGRAPHS_FIELDNAME);
      $subParagraphs = $containerSubparagraphsReferenceRevisions->referencedEntities();
    }
    $subParagraphsCount = count($subParagraphs);

    // Following is only required if we have more than one subparagraph.
    // Otherwise it's simply a 1col unstacked layout.
    foreach ($subParagraphs as $delta => $subParagraph) {
      if (!empty($subParagraph->getAllBehaviorSettings()['layout_paragraphs'])) {
        // Skip this paragraph if it already has layout paragraphs behavior settings.
        // We may assume it has already been processed before then.
        continue;
      }

      $subParagraphsColumnSize = $this->getParagraphColumnSize($subParagraph);

      // Calculate the true size of auto columns.
      // NOTE: This will return a wrong result if the container contains multiple
      // rows, but that should be seen as edge-case.
      if ($subParagraphsColumnSize == -1) {
        // This is set to auto-layout.
        $subParagraphsColumnSize = (self::COLUMNS_PER_ROW / $subParagraphsCount);
      }

      if (in_array($subParagraphsColumnSize, [0, self::COLUMNS_PER_ROW, FALSE])) {
        // We're separating between full row paragraphs (handled here) and
        // multiple paragraphs per row (handled below).
        // Single line paragraphs are typically put at top / bottom
        // and lead to a stacked layout, while multiple paragraphs per row
        // determine the layout.
        // As we do NOT handle cases where multiple layouts are used
        // in the same container, the maximum number of paragraphs
        // in a row is used to determine the layout.

        if ($layoutStacked) {
          // Move this one to bottom region, as this is already stacked,
          // which means there were other paragraphs (in top) before
          $positionsArray[$subParagraph->uuid()] = 99;
          continue;
        } else {
          // Not stacked yet, so move to top.
          $positionsArray[$subParagraph->uuid()] = 0;
          // and set stacked:
          $layoutStacked = TRUE;
          continue;
        }
      } else {
        // As we start with "0" position, we increment first
        // as the first paragraph in a multi-paragraph-row has pos 1!
        $colPos++;
        $rowColSizeSum += $subParagraphsColumnSize;
        $positionsArray[$subParagraph->uuid()] = $colPos;
        // Save the maximum column position in this container to
        // determine the layout required.
        $layoutColumns = max($layoutColumns, $colPos);

        // Reset if a new row is required...
        if ($rowColSizeSum >= self::COLUMNS_PER_ROW) {
          $rowColSizeSum = 0;
          $rows++;
          $colPos = 0;
        }
      }
    }
    $columnWidths = NULL;
    switch ($layoutColumns) {
        // column_widths is only used for two and three column layouts:
      case 2:
        $columnWidths = '50-50';
        // Try to approximate column with from first column size:
        if (!empty($firstColumnSize)) {
          if ($firstColumnSize < 6) {
            $columnWidths = '33-66';
          } elseif ($firstColumnSize > 6) {
            $columnWidths = '66-33';
          }
        }
        break;

      case 3:
        // Default:
        $columnWidths = '33-33-33';
        // Try to approximate column with from first column size:
        if (!empty($firstColumnSize)) {
          if ($firstColumnSize == 6) {
            $columnWidths = '50-25-25';
          } elseif ($firstColumnSize == 3) {
            // Use the more used 25-25-50 instead of also possible 25-50-25:
            $columnWidths = '25-25-50';
          }
        }
        break;
    }

    $layoutName = 'drowl_layouts_' . $layoutColumns . 'col' . ($layoutStacked ? '_stacked' : '');

    return [$layoutName, $positionsArray, $columnWidths, $rows];
  }

  /**
   * Returns the layout region to use by $layout_name and $columnIndex.
   *
   * A column index less than zero indicates, the column is not layouted and
   * placed in a fallback region (bottom at stacked or first at unstacked).
   * The same happens for column indexes larger than the number of columns.
   * Both should be rare edge cases!
   *
   * @param string $layout_name
   * @param int $columnIndex
   * @return string
   * @throws Exception
   */
  private function getLayoutRegionByColumnIndex(string $layout_name, int $columnIndex) {
    switch ($layout_name) {
      case 'drowl_layouts_1col':
      case 'drowl_layouts_1col_stacked':
        return 'main';

      case 'drowl_layouts_2col':
      case 'drowl_layouts_2col_stacked':
        if ($columnIndex === 0) {
          return 'top';
        } elseif ($columnIndex === 1) {
          return 'left';
        } elseif ($columnIndex === 2) {
          return 'right';
        } else {
          return 'bottom';
        }

      case 'drowl_layouts_3col':
      case 'drowl_layouts_3col_stacked':
        if ($columnIndex === 0) {
          return 'top';
        } elseif ($columnIndex === 1) {
          return 'cell_1';
        } elseif ($columnIndex === 2) {
          return 'cell_2';
        } elseif ($columnIndex === 3) {
          return 'cell_3';
        } else {
          return 'bottom';
        }

      case 'drowl_layouts_4col':
      case 'drowl_layouts_4col_stacked':
        if ($columnIndex === 0) {
          return 'top';
        } elseif ($columnIndex === 1) {
          return 'cell_1';
        } elseif ($columnIndex === 2) {
          return 'cell_2';
        } elseif ($columnIndex === 3) {
          return 'cell_3';
        } elseif ($columnIndex === 4) {
          return 'cell_4';
        } else {
          return 'bottom';
        }

      case 'drowl_layouts_5col':
      case 'drowl_layouts_5col_stacked':
        if ($columnIndex === 0) {
          return 'top';
        } elseif ($columnIndex === 1) {
          return 'cell_1';
        } elseif ($columnIndex === 2) {
          return 'cell_2';
        } elseif ($columnIndex === 3) {
          return 'cell_3';
        } elseif ($columnIndex === 4) {
          return 'cell_4';
        } elseif ($columnIndex === 5) {
          return 'cell_5';
        } else {
          return 'bottom';
        }

      case 'drowl_layouts_6col':
      case 'drowl_layouts_6col_stacked':
        if ($columnIndex === 0) {
          return 'top';
        } elseif ($columnIndex === 1) {
          return 'cell_1';
        } elseif ($columnIndex === 2) {
          return 'cell_2';
        } elseif ($columnIndex === 3) {
          return 'cell_3';
        } elseif ($columnIndex === 4) {
          return 'cell_4';
        } elseif ($columnIndex === 5) {
          return 'cell_5';
        } else {
          return 'bottom';
        }

      default:
        throw new \Exception('Unknown layout: "' . $layout_name . '", unable to determine the region.');
    }
  }

  /**
   * Helper function to retrieve the column size setting from the paragraph.
   *
   * Returns the selected column size or FALSE if not part of a layout.
   *
   * @param Drupal\paragraphs\Entity\Paragraph $paragraph
   *   The paragraph.
   *
   * @return int|bool
   *   Returns the paragraph column size if layout, else false.
   */
  private function getParagraphColumnSize(Paragraph $paragraph) {
    if ($paragraph->hasField(self::PARAGRAPHS_SETTINGS_FIELDNAME)) {
      $paragraphSettingsArray = $paragraph->get(self::PARAGRAPHS_SETTINGS_FIELDNAME)->getValue();
      if (!empty($paragraphSettingsArray[0]['layout_sm_columns']) || !empty($paragraphSettingsArray[0]['layout_md_columns']) || !empty($paragraphSettingsArray[0]['layout_lg_columns'])) {
        // This is in a layout. Get column size:
        return $paragraphSettingsArray[0]['layout_lg_columns'] ?: $paragraphSettingsArray[0]['layout_md_columns'] ?: $paragraphSettingsArray[0]['layout_sm_columns'];
      } else {
        // layout_lg_columns is empty. This is not in a layout.
        return FALSE;
      }
    } else {
      if ($paragraph->bundle() === 'anchor') {
        // Anchors have no settings field and should be skipped.
        return FALSE;
      }
      throw new \Exception('Paragraph (ID: ' . $paragraph->id() . ' (' . $paragraph->bundle() . ')) has no field: ' . self::PARAGRAPHS_SETTINGS_FIELDNAME . ' which is required to determine layout settings.');
    }
  }
}

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

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