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.');
}
}
}
