#! code: Drupal 11: Don’t Create Node During Content Import

If you import an item and haven’t imported its parent item then there is potential for errors or database consistency issues where content isn’t linked up correctly. This can create orphan content that can never be deleted as there is no way to administer it or detect that it is orphaned.First, let’s do a quick look at the default content import process.At the end of the content import process an event called Drupaldefault_contentEventDefaultContentEvents::IMPORT is triggered and all of the created content items are passed to this event.The process is more complicated than this, but much of the complexity is involved with ensuring that dependent content is connected together correctly and therefore imported in the right order.Funnily enough, after writing this code and setting it to work on a demo site I found another module in the wild doing exactly the same thing. It’s always good to find the solution, implement it, and then see that someone else has gone through the same thought process and done the same thing.There is a good reason why the import event is called at the end of the import process and gets passed a list of the entities created and I didn’t quite realise at the time. The content to be imported is not a simple list of nodes, it’s usually an interlinked graph of nodes, taxonomy terms, media items, and potentially any number of other content entities. Removing one item from this list before it is written to the database can (probably will, really) have catastrophic consequences on the import process as dependent entities then can’t be linked up correctly.

Default Content Import Process

I initially tried to use the node_presave hook to intercept the node save event and do something about it. This hook is run as the node is saved to the database, but before the node is actually written so there’s a potential to prevent the node from being saved.Here’s an example using the title property of nodes to delete two items from the imported content.Since the default content creation event happens after everything has been imported I first looked at mechanisms to prevent the node being created inside the loop. It made sense to do this at first as it would prevent the data from reaching the database. #[Hook('node_presave')]
public function nodePresave(EntityInterface $node): void {
// Check if this is coming from default_content import.
if (!$this->isDefaultContentImport()) {
return;
}
}

The solution in the end was to use the content import event to delete content after it has been imported. Specifically, the event is called Drupaldefault_contentEventDefaultContentEvents::IMPORT and can be subscribed to using a normal Drupal event subscriber.After some thinking I kept hitting the same issue where I would always be unable to prevent the node from being inserted and allow the default content process to continue normally.Any default content you have will be stored in the module or recipe content directory, which is imported as the module is installed or the recipe applied.

  • You could throw an exception to prevent the node from being saved, but this will cause the entire default content import process to fail. This is not what I wanted to happen.
  • You can’t change the node variables to be “invalid” since this will tend to cause errors when saving the node, or just straight up throw SQL errors and crash the process.
  • I attempted to set the published status of the node to false and then remove any unpublished nodes from the site once the import has finished. This has other problems though as it assumes that all of your nodes are published when imported, which might not be the case. If I went down this route it may cause entities to be imported that sit along side normal content, which is a pain to pick through afterwards. I did think about adding a value to another field, but I would either need to create the field or ensure that no content had the same value before the import. There’s multiple problems with this approach so I didn’t continue here.

As a side node, I did find a (rather hacky) way of detecting if the content was being created by the default content in the node_presave hook.Default content is really useful when figuring out how a system works as it can show relationships between content types that are not always simple to discern through the site itself. It also has a place when deploying content between sites as you can package up some default content and include that in a deployment step.Unfortunately, this didn’t work. There are a couple of reasons for this.When you kick off the content import process the default content module will look in the directory for the content it must import and collect it together into a sorted list of items, this is handled by the default_content.importer service. A loop is then run that imports all of the content found, keeping track of each item created. $uuid = $entity->uuid();
$bannedUuids = [
// Page titles to remove.
'0e06a33e-8cd1-4eed-b102-485fb0af1b03',
'76f5ab83-d6d5-432f-a36e-fc59d1f0fb9d',
];
if (in_array($uid, $bannedUuids)) {
// If this is one of our problem nodes, delete it.
$entity->delete();
continue;
}

I needed to do this selected default content import on a recent site as I was installing. A contributed module was used to populate the site with content to create a collection of examples. Although the content was good, I didn’t want to import it all as some of it didn’t make sense for a couple of scenario that I was looking at. That lead me to seeing if I could alter the content automatically as it was imported into the site, rather than manually delete content after the fact.<?php

namespace DrupalmymoduleEventSubscriber;

use DrupalCoreDatabaseQueryPagerSelectExtender;
use Drupaldefault_contentEventDefaultContentEvents;
use Drupaldefault_contentEventImportEvent;
use DrupalnodeNodeInterface;
use SymfonyComponentEventDispatcherEventSubscriberInterface;

class MyModuleDefaultContentEventSubscriber implements EventSubscriberInterface {

public static function getSubscribedEvents() {
return [
DefaultContentEvents::IMPORT => [['importEvent', 1]],
];
}

public function importEvent(ImportEvent $event) {
$entities = $event->getImportedEntities();
foreach ($entities as $entity) {
if (!$entity instanceof NodeInterface) {
continue;
}
$title = $entity->getTitle();
$bannedTitles = [
// Page titles to remove.
'Unique title number 1',
'Unique title number 2',
];
if (in_array($title, $bannedTitles)) {
// If this is one of our problem nodes, delete it.
$entity->delete();
continue;
}
}
}

}

In this article we will look at some of the actions I tried to prevent content from being imported, before moving onto what the actual solution was. If you are following along at home, it is assumed that you already have a default content process setup and are just looking for change what content is imported without altering the source files.Occasionally, you’ll want to tweak the content that is imported for different situations; with the reasons why you would want to do this being numerous. It might just be that you have a demo site and you wanted to tweak the content imported to better tailor it for the user. You could have a single recipe that contains the content and want to alter that for a specific demo.Importing content with the Default Content module is really useful when populating a site with content. There’s an increasing number of recipes and modules that can be used to populate a site with content either for setup or demonstration purposes.All we need to do then is find out a way of identifying the items of content that have been imported. We can’t use the node ID as they will be newly created as the content is imported (and be different every time). Anything else that is unique about the content item is fine to use though, including the title and the uuid.The node_insert hook wasn’t much better here as I would just be acting on the already saved node. Running a delete operation here is sort of dangerous as you can get into this hook cascade failure (feel free to steal that phrase) where Drupal is constantly calling hooks that call other hooks until the code runs out of memory. protected function isDefaultContentImport(): bool {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);

foreach ($backtrace as $trace) {
if (isset($trace['class']) && strpos($trace['class'], 'Drupaldefault_contentImporter') !== FALSE) {
return TRUE;
}
if (isset($trace['function']) && $trace['function'] === 'importContent') {
return TRUE;
}
}

return FALSE;
}

Hopefully, this article will prevent anyone from starting from scratch going through the same thought process as I did and just cut to the chase.

Similar Posts