diff --git a/src/EntityClone/Content/ContentEntityCloneBase.php b/src/EntityClone/Content/ContentEntityCloneBase.php index 58fa2396424c0c584984fb142590f27397f23720..9302151584f081a89ba3fc670a96bd6585ddc610 100644 --- a/src/EntityClone/Content/ContentEntityCloneBase.php +++ b/src/EntityClone/Content/ContentEntityCloneBase.php @@ -13,6 +13,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\TranslatableInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldConfigInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -60,6 +61,13 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter */ protected $currentUser; + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + /** * Constructs a new ContentEntityCloneBase. * @@ -73,13 +81,16 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter * The current user. * @param \Drupal\entity_clone\EntityCloneClonableFieldInterface $entity_clone_clonable_field * The entity clone clonable field service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, string $entity_type_id, TimeInterface $time_service, AccountProxyInterface $current_user, EntityCloneClonableFieldInterface $entity_clone_clonable_field) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, string $entity_type_id, TimeInterface $time_service, AccountProxyInterface $current_user, EntityCloneClonableFieldInterface $entity_clone_clonable_field, ModuleHandlerInterface $module_handler) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeId = $entity_type_id; $this->timeService = $time_service; $this->currentUser = $current_user; $this->entityCloneClonableField = $entity_clone_clonable_field; + $this->moduleHandler = $module_handler; } /** @@ -91,7 +102,8 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter $entity_type->id(), $container->get('datetime.time'), $container->get('current_user'), - $container->get('entity_clone.clonable_field') + $container->get('entity_clone.clonable_field'), + $container->get('module_handler') ); } @@ -99,6 +111,25 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter * {@inheritdoc} */ public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) { + // Clone books entities. + $bid = isset($entity->book) && empty($entity->book['bid']) ? 0 : $entity->book['bid']; + if (!empty($properties['clone_book']) && !empty($bid) && $entity->book['bid'] == $entity->id()) { + // This is the parent book. + $link = [ + 'nid' => $cloned_entity->id(), + 'bid' => 'new', + 'pid' => 0, + 'has_children' => $entity->book['has_children'], + 'weight' => 0, + 'depth' => 1, + ]; + $cloned_entity->book = $link; + $cloned_entity->save(); + + // Clone book children. + $this->cloneBookPages($entity, $cloned_entity, $properties); + } + // Clone referenced entities. $already_cloned[$entity->getEntityTypeId()][$entity->id()] = $cloned_entity; if ($cloned_entity instanceof FieldableEntityInterface && $entity instanceof FieldableEntityInterface) { @@ -122,7 +153,10 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter } } - $this->setClonedEntityLabel($entity, $cloned_entity, $properties); + // Only change the label on single nodes, fixing the title on entire books is too cumbersome. + if (empty($properties['clone_book'])) { + $this->setClonedEntityLabel($entity, $cloned_entity, $properties); + } $this->setCreatedAndChangedDates($cloned_entity); if ($this->hasTranslatableModerationState($cloned_entity)) { @@ -365,4 +399,66 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter } } + /** + * Clone the book pages within a book. + * + * @param \Drupal\Core\Entity\EntityInterface $original_entity + * The original entity. + * @param \Drupal\Core\Entity\EntityInterface $cloned_entity + * The entity cloned from the original. + * @param array $properties + * The properties argument containing the bid and nid map. + */ + protected function cloneBookPages(EntityInterface $original_entity, EntityInterface $cloned_entity, array $properties) { + $bid = $cloned_entity->id(); + $map = [ + $original_entity->id() => $cloned_entity->id(), + ]; + + $children = \Drupal::service('book.outline_storage')->loadBookChildren($original_entity->id()); + $this->cloneBookChildren($children, $properties, $bid, $map); + } + + /** + * Recursive method to walk the children and clone. + * + * @param array $children + * The current pages children. + * @param array $properties + * The properties argument containing the bid and nid map. + * @param int $bid + * The book id. + * @param array $map + * The map argument containing the old nid to new map to set the new parent properly. + */ + protected function cloneBookChildren(array $children, array $properties, int $bid, array &$map) { + foreach ($children as $nid => $child) { + $entity = $this->entityTypeManager->getStorage('node')->load($nid); + $duplicate = $entity->createDuplicate(); + $cloned_entity = $this->cloneEntity($entity, $duplicate, $properties); + + // Set the moderation state in children books according to user's choice. + if ($this->moduleHandler->moduleExists('content_moderation') && isset($properties['moderation_state']) && $duplicate->hasField('moderation_state')) { + $duplicate->set('moderation_state', $properties['moderation_state']); + } + + $link = [ + 'nid' => $cloned_entity->id(), + 'bid' => $bid, + 'pid' => !empty($map[$entity->book['pid']]) ? $map[$entity->book['pid']] : $bid, + 'weight' => $entity->book['weight'], + 'depth' => $entity->book['depth'], + ]; + $cloned_entity->book = $link; + $cloned_entity->save(); + + $map[$entity->id()] = $cloned_entity->id(); + + if ($child['has_children'] == 1) { + $next_level = \Drupal::service('book.outline_storage')->loadBookChildren($nid); + $this->cloneBookChildren($next_level, $properties, $bid, $map); + } + } + } + } diff --git a/src/EntityClone/Content/ContentEntityCloneFormBase.php b/src/EntityClone/Content/ContentEntityCloneFormBase.php index fe5963926c7d03a87e6dd0fb19f69f68258897da..ddaa28008184085dc1760494c4b3f511fc91eb8b 100644 --- a/src/EntityClone/Content/ContentEntityCloneFormBase.php +++ b/src/EntityClone/Content/ContentEntityCloneFormBase.php @@ -125,6 +125,25 @@ class ContentEntityCloneFormBase implements EntityHandlerInterface, EntityCloneF '#access' => TRUE, ], ], $form); + + $bid = !empty($entity->book['bid']) ? $entity->book['bid'] : 0; + if (!empty($bid) && $entity->id() == $bid) { + $form['book'] = [ + '#type' => 'fieldset', + '#weight' => -5, + ]; + + $form['book']['description'] = [ + '#type' => 'markup', + '#markup' => t('This is a top-level book. Select the checkbox below to clone all of the pages within the book.'), + ]; + + $form['book']['clone_book'] = [ + '#type' => 'checkbox', + '#title' => t('Clone entire book including child pages'), + '#default_value' => 0, + ]; + } } }