Problem/Motivation

Sometimes, we want to import default content and then disable the module, keeping the content.

Sometimes we want to bulk delete the content provided by this module.

Sometimes, we want to reimport again the content to refresh it.

Our preference is not to auto-delete on uninstall. Consider this use-case:

* Create custom_content module to import content to production
* custom_content requires default_content which requires hal and serialization
* We want the content, but do not want these 4 modules enabled
* We enable the module via drush and then disable it (and its dependencies)
* Content is present in production

The approach in #2844276: Option to automatically delete demo content would break that workflow.

During testing, however, we may want to import and delete the custom_content at will.

Steps to reproduce

n/a

Proposed resolution

* Create a drush command to read the info.yml files of default_content modules.
* Allow the drush command to delete all content or content of a specific entity type.

e.g. `drush default-content:delete my_test_content image`

Remaining tasks

Write the drush commands.

User interface changes

None, though this approach could be used to build a form later.

API changes

None anticipated.

Data model changes

None anticipated.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

agentrickard created an issue. See original summary.

agentrickard’s picture

Issue summary: View changes
agentrickard’s picture

Interesting -- this works, but it creates a pretty ugly loop.

- default_content must be enabled for the command to be found.
- The content module must be enabled for content to be found.
- Trying to uninstall the two after delete disables the command.

We may be better off putting this in a custom module, except that we're using the State element to prevent re-import.

/**
 * Implements hook_modules_installed().
 */
function default_content_modules_installed($modules) {
  // @todo Move this to an event once we have HookEvent.
  $state = \Drupal::state();
  foreach ($modules as $module) {
    $delete_list = $state->get('default_content_delete') ?? [];
    if (!\Drupal::isConfigSyncing() && !in_array($module, $delete_list, TRUE)) {
      \Drupal::service('default_content.importer')->importContent($module);
    }
  }
}

-- Drush command --

  /**
   * Deletes all the content defined in a module info file.
   *
   * @param string $module_name
   *   The name of the module.
   * @param string $entity_type
   *   The entity type to delete (optional).
   *
   * @command default-content:delete-content
   * @aliases dcdel
   */
  public function contentDeleteModule($module_name, $entity_type = NULL) {
    // Store install / uninstall in state to stop import.
    $state = \Drupal::state();

    // The module must be installed to proceed.
    $module_list = \Drupal::moduleHandler()->getModuleList();
    if (!isset($module_list[$module_name])) {
      $state->set('default_content_delete', [$module_name]);
      \Drupal::service("module_installer")->install([$module_name]);
    }

    // Parse module info.
    $module = \Drupal::moduleHandler()->getModule($module_name);
    $info_file = $module->getPathname();
    $info = \Drupal::service('info_parser')->parse($info_file);
    // @todo Make this nicer later.
    if (!$info) {
      echo 'The module could not be found';
    }

    // Get the content list.
    $default_content = $info['default_content'];
    if ($entity_type) {
      $default_content = $info['default_content'][$entity_type] ?? [];
    }

    // Validate that items to be deleted exist as file definitions.
    $delete_list = [];
    $module_folder = $module->getPath() . '/content';
    foreach ($default_content as $type => $list) {
      $files = [];
      $file_folder = $module_folder . '/' . $type;
      foreach ($list as $filename) {
        $file = $file_folder . '/' . $filename . '.yml';
        if (file_exists($file)) {
          $files[$filename] = $file;
        }
      }
      $delete_list[$type] = array_keys($files);
    }
    if (empty($delete_list)) {
      echo 'No files were found that could be deleted.';
    }

    // Run the delete and get a count of affected entities.
    foreach ($delete_list as $entity_type_key => $uuid_list) {
      $count[$entity_type_key] = 0;
      foreach ($uuid_list as $uuid) {
        $entity = \Drupal::service('entity.repository')->loadEntityByUuid($entity_type_key, $uuid);
        if ($entity) {
          $entity->delete();
          $count[$entity_type_key]++;
        }
      }
    }

    // Report actions taken.
    echo "\nCompleted deletion of $module_name content.\n";
    foreach ($count as $key => $value) {
      echo "- Deleted $value entities of type $key.\n";
    }

    // Uninstall the module in all cases.
    \Drupal::service("module_installer")->uninstall([$module_name, 'default_content']);
    echo "Uninstalled $module_name and default_content.\n You may wish to uninstall hal and serializer as well.\n";
    echo "Run `drush pmu hal serializer -y` to do so.";
    // Delete state variable.
    $state->delete('default_content_delete');
  }
agentrickard’s picture

I figured out how to bypass the module enabled bit, patch coming.

agentrickard’s picture

OK, I officially hate the git workflow. It's not what I use everyday and I have screwed it up three times -- and I can't delete the branches?!?

Patch coming.

agentrickard’s picture

Status: Active » Needs work
StatusFileSize
new7.98 KB

Still needs work, since the drush output is non-standard.

agentrickard’s picture

And thanks to GIT defaulting to the 8.x-1.x branch, this patch is malformed.

Please update the project.

agentrickard’s picture

Status: Needs work » Needs review
StatusFileSize
new5.98 KB

New version against 2.0.x.

Notes:

* Still needs better message handling
* Requires that default_content be enabled but not the module that created the content.

ressa’s picture

Thanks @agentrickard, I have created #3282736: Set default Git branch to 2.0.x about your comment in #7.

c_archer’s picture

Status: Needs review » Needs work

Patch in #8 does not work I get the error: ArgumentCountError: Too few arguments to function Drupal\default_content\Commands\DefaultContentCommands::__construct()

tasc’s picture

Patch #8 works fine for me so far.

There are however some questions arising...

  1. During installation of a module everything residing in content/ gets imported, regardless of a definition-section in the info.yml.
    Wouldn't it make more sense to delete the content in the same way: regardless of a definition, like its done in the 8.1 patch?
    It would be also more practical in case one is using drush dcer entity_type_id --folder and doesn't fiddle around with uuids.
  2. Both approaches aren't deleting paragraphs and so a following re-import will fail. Either you need to run cron in order to get rid of the orphans or the exported definition of the entity must be parsed to check if there is a paragraph-entity somewhere to be pre-delete.
  3. Might be another issue, but deleting files don't delete their entries from file_usage-table.

tasc’s picture

Status: Needs work » Needs review

The patch in MR23 is another approach which deletes everything present in the content-folder of a module, without parsing the .info-file. Furthermore also all entity_reference_revisions attached to an entity are deleted.

drush dcdm <module>

eduardo morales alberti’s picture

In our case, we tried to find all the UUIDs definitions on the default_content and remove them, it will remove all entities defined by default_content.
We tried the drush command and the paragraphs were not removed for example.

$entity_types = [
  'node',
  'paragraph',
  'taxonomy_term',
  'media',
  'file',
  'menu_link_content',
];

$uuids = [];
$module_folder = \Drupal::moduleHandler()
      ->getModule($module)
      ->getPath() . '/content';
$dirs = array_filter(glob($module_folder . '/content/*'), 'is_dir');

$uuids = [];
$pattern = '/uuid: (\w.*-\w.*-\w.*-\w.*-\w.*)/m';
foreach ($dirs as $dir) {
  $entity_type = basename($dir);
  if (!in_array($entity_type, $entity_types)) {
    continue;
  }
  $files = glob($dir . '/*.yml');

  foreach ($files as $file) {
    $contents = file_get_contents($file);

    if (preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER, 0)) {
      foreach ($matches as $match) {
        if (!in_array($match[1], $uuids)) {
          $uuids[] = $match[1];
        }
      }
    }

  }

}

foreach ($entity_types as $entity_type) {
  $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadByProperties(['uuid' => $uuids]);
  \Drupal::entityTypeManager()->getStorage($entity_type)->delete($entities);
}

eduardo morales alberti’s picture

We created a new branch "3282547-drush-delete-content-reimport" to delete all entities from default_content based on all the uuids defined on the default_content module.
Also we add a reimport command (deletes and import again the content).
MR https://git.drupalcode.org/project/default_content/-/merge_requests/45/d...

delacosta456’s picture

hi @eduardo-morales-alberti
I was looking for something like this.
The patch applied correctly but looks to have breaks another patch Drush comand to export all items from entity with defined criteria that i was using to export at once node+references to folder .

Example : drush dcer node 34 --folder=modules/custom/mycustom_default_content/content

after the merge request 45.patch

the above command doesn't work and send the following error

In LegacyServiceInstantiator.php line 282:
                                                                          
  You have requested a non-existent parameter "default_content.deleter".  
                                                                          

Can you help
Thanks

eduardo morales alberti’s picture

@delacosta456 Did you clear the caches after applying the patch?

The service is defined on https://git.drupalcode.org/project/default_content/-/merge_requests/45/d...

It is possible that Drupal did not detect the service without clearing caches first.

eduardo morales alberti’s picture

Title: Drush command to delete default content » Drush command to delete and reimporting default content
Issue summary: View changes
eduardo morales alberti’s picture

Refactor code to delete also inline entities