layout_builder_at-8.x-2.11/src/Plugin/Field/FieldWidget/LayoutBuilderCopyWidget.php
src/Plugin/Field/FieldWidget/LayoutBuilderCopyWidget.php
<?php
namespace Drupal\layout_builder_at\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\block_content\BlockContentInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionComponent;
/**
* A widget to display the copy widget form.
*
* @internal
* Plugin classes are internal.
*/
#[FieldWidget(
id: "layout_builder_at_copy",
label: new TranslatableMarkup("Layout Builder Asymmetric Translation"),
description: new TranslatableMarkup("A field widget for Layout Builder. This exposes a checkbox on the entity form to copy the blocks on translation."),
field_types: [
"layout_section",
],
multiple_values: FALSE,
)]
class LayoutBuilderCopyWidget extends WidgetBase {
/**
* Gets the available appearance options.
*
* @return array
* An associative array of appearance options, where keys are option values
* and values are their corresponding labels.
*/
protected function options(): array {
return [
'unchecked' => $this->t('Unchecked'),
'checked' => $this->t('Checked'),
'checked_hidden' => $this->t('Checked and hidden'),
];
}
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
return [
'appearance' => 'unchecked',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$element['appearance'] = [
'#type' => 'select',
'#title' => $this->t('Checkbox appearance'),
'#options' => $this->options(),
'#default_value' => $this->getSetting('appearance'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
$summary = [];
$summary[] = $this->t('Appearance: @checked', ['@checked' => $this->options()[$this->getSetting('appearance')]]);
return $summary;
}
/**
* {@inheritdoc}
*/
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL): array {
$field_name = $this->fieldDefinition->getName();
$parents = $form['#parents'];
// Store field information in $form_state.
if (!static::getWidgetState($parents, $field_name, $form_state)) {
$field_state = [
'items_count' => count($items),
'array_parents' => [],
];
static::setWidgetState($parents, $field_name, $form_state, $field_state);
}
// Collect widget elements.
$elements = [];
$delta = 0;
$element = [
'#title' => $this->fieldDefinition->getLabel(),
'#description' => FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
];
$element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
if ($element) {
if (isset($get_delta)) {
// If we are processing a specific delta value for a field where the
// field module handles multiples, set the delta in the result.
$elements[$delta] = $element;
}
else {
// For fields that handle their own processing, we cannot make
// assumptions about how the field is structured, just merge in the
// returned element.
$elements = $element;
}
}
// Populate the 'array_parents' information in $form_state->get('field')
// after the form is built, so that we catch changes in the form structure
// performed in alter() hooks.
$elements['#after_build'][] = [get_class($this), 'afterBuild'];
$elements['#field_name'] = $field_name;
$elements['#field_parents'] = $parents;
// Enforce the structure of submitted values.
$elements['#parents'] = array_merge($parents, [$field_name]);
// Most widgets need their internal structure preserved in submitted values.
$elements += ['#tree' => TRUE];
return [
// Aid in theming of widgets by rendering a classified container.
'#type' => 'container',
// Assign a different parent, to keep the main id for the widget itself.
'#parents' => array_merge($parents, [$field_name . '_wrapper']),
'#attributes' => [
'class' => [
'field--type-' . Html::getClass($this->fieldDefinition->getType()),
'field--name-' . Html::getClass($field_name),
'field--widget-' . Html::getClass($this->getPluginId()),
],
],
'widget' => $elements,
];
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$entity = $items->getEntity();
$access = FALSE;
if ($entity instanceof ContentEntityInterface) {
$access = $entity->isNewTranslation() && !$entity->isDefaultTranslation();
}
$element['#layout_builder_at_access'] = $access;
$checked = FALSE;
$v = $this->getSetting('appearance');
if ($v == 'checked' || $v == 'checked_hidden') {
$checked = TRUE;
}
$element['value'] = $element + [
'#access' => TRUE,
'#type' => 'checkbox',
'#default_value' => $checked,
'#title' => $this->t('Copy blocks into translation'),
];
if ($v == 'checked_hidden') {
$element['value']['#access'] = FALSE;
}
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state): void {
// @todo This isn't resilient to being set twice, during validation and
// save https://www.drupal.org/project/drupal/issues/2833682.
if (!$form_state->isValidationComplete()) {
return;
}
$field_name = $this->fieldDefinition->getName();
// We can only copy if the field is set and access is TRUE.
if (isset($form[$field_name]['widget']['#layout_builder_at_access']) && !$form[$field_name]['widget']['#layout_builder_at_access']) {
return;
}
// Extract the values from $form_state->getValues().
$path = array_merge($form['#parents'], [$field_name]);
$key_exists = NULL;
$values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
$values = $this->massageFormValues($values, $form, $form_state);
if (isset($values['value']) && $values['value']) {
// Replicate.
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_entity */
$entity = $items->getEntity();
$sourceLanguage = NULL;
if ($form_state->hasValue('source_langcode')) {
$sourceLanguageArray = $form_state->getValue('source_langcode');
if (isset($sourceLanguageArray['source'])) {
$sourceLanguage = $sourceLanguageArray['source'];
}
}
$default_entity = is_null($sourceLanguage) ? $entity->getUntranslated() : $entity->getTranslation($sourceLanguage);
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $layout */
$layout = $default_entity->get(OverridesSectionStorage::FIELD_NAME);
$uuid = \Drupal::service('uuid');
/** @var \Drupal\layout_builder\Section[] $sections */
$sections = $layout->getSections();
$new_sections = [];
foreach ($sections as $delta => $section) {
$cloned_section = clone $section;
// Remove components from the cloned section.
foreach ($cloned_section->getComponents() as $c) {
$cloned_section->removeComponent($c->getUuid());
}
// Sort the components by weight.
$components = $section->getComponents();
uasort($components, function (SectionComponent $a, SectionComponent $b) {
return $a->getWeight() > $b->getWeight() ? 1 : -1;
});
foreach ($components as $component) {
$add_component = TRUE;
$cloned_component = clone $component;
$configuration = $component->get('configuration');
// Replicate inline block content.
if ($this->isInlineBlock($configuration['id'])) {
/** @var \Drupal\block_content\BlockContentInterface $block */
/** @var \Drupal\block_content\BlockContentInterface $replicated_block */
$block = \Drupal::service('entity_type.manager')->getStorage('block_content')->loadRevision($configuration['block_revision_id']);
$replicated_block = $this->cloneEntity('block_content', $block->id());
if ($replicated_block) {
$values_to_keep = [];
$language_to_keep = $entity->language()->getId();
if ($replicated_block->hasTranslation($entity->language()->getId())) {
// Extract values of translation to keep.
$values_to_keep = $replicated_block->getTranslation($language_to_keep)->toArray();
// The removed translation needs to be saved or we can not
// change the language to that.
$replicated_block->removeTranslation($language_to_keep);
$replicated_block->save();
}
$replicated_block->set('langcode', $language_to_keep);
$replicated_block->save();
// Copy translatable field values.
foreach ($values_to_keep as $field_name => $field_values) {
if ($replicated_block->getFieldDefinition($field_name)->isTranslatable() && !in_array($field_name, ['default_langcode'])) {
$replicated_block->set($field_name, $field_values);
}
}
// Remove other translations.
foreach ($replicated_block->getTranslationLanguages() as $translation_language) {
if ($translation_language->getId() !== $language_to_keep) {
$replicated_block->removeTranslation($translation_language->getId());
}
}
$replicated_block->save();
$configuration = $this->updateComponentConfiguration($configuration, $replicated_block);
$cloned_component->setConfiguration($configuration);
// Store usage.
\Drupal::service('inline_block.usage')->addUsage($replicated_block->id(), $entity);
}
else {
$add_component = FALSE;
$this->messenger()->addMessage($this->t('The inline block "@label" was not duplicated.', ['@label' => $block->label()]));
}
}
// Add component.
if ($add_component) {
$cloned_component->set('uuid', $uuid->generate());
$cloned_section->appendComponent($cloned_component);
}
}
$new_sections[] = $cloned_section;
}
$items->setValue($new_sections);
}
else {
$items->setValue(NULL);
}
}
/**
* Replicates an entity by cloning it.
*
* @param string $entity_type_id
* The entity type ID of the entity to be cloned.
* @param int|string $entity_id
* The ID of the entity to be cloned.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The cloned entity if successful, or NULL on failure.
*/
protected function cloneEntity(string $entity_type_id, mixed $entity_id): ?EntityInterface {
$clone = NULL;
try {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
/** @var \Drupal\Core\Entity\EntityInterface $clone */
$entity = \Drupal::service('entity_type.manager')->getStorage($entity_type_id)->load($entity_id);
$clone = $entity->createDuplicate();
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */
$field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
foreach ($field_definitions as $definition) {
// Support for Entity reference revisions.
if ($definition->getFieldStorageDefinition()->getType() == 'entity_reference_revisions') {
$new_values = [];
$target_type = $definition->getFieldStorageDefinition()->getSetting('target_type');
$values = $clone->get($definition->getName())->getValue();
if (!empty($values)) {
foreach ($values as $value) {
/** @var \Drupal\Core\Entity\EntityInterface $reference */
/** @var \Drupal\Core\Entity\EntityInterface $reference_clone */
$reference = \Drupal::service('entity_type.manager')->getStorage($target_type)->load($value['target_id']);
$reference_clone = $reference->createDuplicate();
$reference_clone->save();
$new_values[] = [
'target_id' => $reference_clone->id(),
'target_revision_id' => $reference_clone->getRevisionId(),
];
}
if (!empty($new_values)) {
$clone->set($definition->getName(), $new_values);
}
}
}
}
}
catch (\Exception $e) {
\Drupal::logger('layout_builder_at')->error('Error cloning entity: @message', ['@message' => $e->getMessage()]);
}
return $clone;
}
/**
* Does the block id represent an inline block.
*
* @param string $block_id
* The block id.
*
* @return bool
* True if this is an inline block else false.
*/
protected function isInlineBlock(string $block_id): bool {
return str_starts_with($block_id, 'inline_block:');
}
/**
* Modify the supplied component configuration based on modified block.
*
* @param array $configuration
* The Layout Builder component configuration array.
* @param \Drupal\block_content\BlockContentInterface $replicated_block
* The cloned block.
*
* @return array
* A modified configuration array.
*/
protected function updateComponentConfiguration(array $configuration, BlockContentInterface $replicated_block): array {
$configuration['block_id'] = $replicated_block->id();
$configuration['block_revision_id'] = $replicated_block->getRevisionId();
// Add support for block UUID export.
// @see https://www.drupal.org/node/3180702
if (!empty($configuration['block_uuid'])) {
$configuration['block_uuid'] = $replicated_block->uuid();
}
return $configuration;
}
}
