Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drupal 8 orphan paragraphs

I've noticed that Paragraph entities are not deleted from the database. They rather unlink from the parent node.

This is noticeable if you create a view, that lists Paragraphs and attach a contextual filter, that filters by Parent ID.

For now, I've found a workaround, to create a view that lists Content. Attach relationship to a Paragraph. This way it ensures that only linked paragraphs are displayed.

There is still an issue of having hundreds of orphan Paragraphs and field data in the database. Is there a way of cleaning them?

EDIT: This is apparently a major bug and could be found in Paragraph module's issue tracker: Removed paragraph entities are not deleted from database

like image 520
Elaman Avatar asked Apr 25 '18 07:04

Elaman


2 Answers

Now that I've figured out that this is a bug and it is not fixed yet, my main goal is to just clean orphan Paragraphs.

Someone created this module: Paragraph clean, but I'm not a fan of using modules for such purposes.

So, below I will post my first successful attempt to solve it. I must warn this isn't safe, because it deletes Paragraphs!

The solution is not tested for using revisions, content translation, etc. So this might ruin your day. Backup your site.

Using Devel module, go to Development > Execute PHP Code. Paste and execute following code:

// get all paragraphs
$deleted = [];
$paragraph_ids = \Drupal::entityQuery('paragraph')->execute();
$paragraphs = \Drupal::entityTypeManager()->getStorage('paragraph')->loadMultiple($paragraph_ids);
foreach ($paragraphs as $target_id => $paragraph) {
  // get parent entity (node, taxonomy, paragraph, etc.)
  $parent = $paragraph->getParentEntity();
  $field_name = $paragraph->parent_field_name->value;

  // Check if current paragraph exists in parent entity field values
  $exists = FALSE;
  $values = is_null($parent) ? [] : $parent->get($field_name)->getValue();
  foreach($values as $value) {
    if ($value['target_id'] == $target_id) {
      $exists = TRUE;
    }
  }

  // Delete paragraphs that aren't linked to an entity they claim as a parent
  if (!$exists) {
    $paragraph->delete();
    $deleted[] = $target_id;
  }
}

print "Deleted paragraph IDs: " . implode(', ', $deleted);
like image 116
Elaman Avatar answered Sep 22 '22 10:09

Elaman


We created a module called paragraphs_clean that implements hook_entity_update() (be careful: module name must be equal to the prefix of the entity_update() hook function) taking inspiration from here: https://www.drupal.org/project/paragraphs/issues/2741937#comment-13181377.

As it is stated, it seem to work well even if there are revisions and translations. Also we had to adapt the original php code to function in the case when paragraphs have paragraphs, too (i.e. nested paragraphs).

This code is more efficient than the above as it will only load orphan paragraphs of the entity from the context.

Here is the code:

<?php


/**
* @file
* Paragraphs clean module.
*/

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\field\Entity\FieldConfig;

/**
* Implements hook_entity_update().
* 
* When form updates, delete any paragraph entities that were removed.
* 
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function paragraphs_clean_entity_update(EntityInterface $entity) {

  // Only act on content entities.
  if (!($entity instanceof FieldableEntityInterface)) {
    return;
  }

  $fieldManager = \Drupal::service('entity_field.manager');
  $parentEntities = $fieldManager->getFieldMapByFieldType('entity_reference_revisions');

  if (!array_key_exists($entity->getEntityTypeId(), $parentEntities)) {
    return;
  }

  $paragraph_definitions = [];

  // loop through all paragraph types
  foreach ($parentEntities[$entity->getEntityTypeId()] as $field_id => $settings) {
    if ($configField = FieldConfig::loadByName($entity->getEntityTypeId(), $entity->bundle(), $field_id)) {
      $paragraph_definitions[] = $configField;
    }
  }

  if (empty($paragraph_definitions)) {
    return;
  }

  foreach ($paragraph_definitions as $paragraph_definition) {

    //get entity type name to make it work with any kind of parent entity (node, paragraph, etc.)
    $entityTypeName = $entity->getEntityTypeId();

    // Remove orphaned paragraphs.
    $query = \Drupal::database()->select('paragraphs_item_field_data', 'pfd')
      ->fields('pfd', ['id'])
      ->condition('pfd.parent_type', $entityTypeName) 
      ->condition('pfd.parent_id', $entity->id())
      ->condition('pfd.parent_field_name', $paragraph_definition->getName());

    $query->addJoin('left', $entityTypeName.'__'.$paragraph_definition->getName(),'nt','pfd.id=nt.'.$paragraph_definition->getName().'_target_id');
    $query->isNull('nt.'.$paragraph_definition->getName().'_target_id');
    $query->distinct();

    $paragraph_ids = $query->execute()->fetchCol();

    if ($paragraph_ids) {
      $para_storage = \Drupal::entityTypeManager()->getStorage('paragraph');
      foreach ($paragraph_ids as $paragraph_id) {

        if ($para = $para_storage->load($paragraph_id)) {
          $para->delete();
          drupal_set_message(t('Paragraph of type "%type" has been deleted: %id', ['%id' => $paragraph_id, '%type' => $paragraph_definition->getName()]));
        }

      }
    }   

  }

}
like image 22
Lotzy Avatar answered Sep 18 '22 10:09

Lotzy