Prevent deletion of taxonomy term if associated with a node

08 Sep 2014

Why check if term is associated to a node prior to deletion?

In cases where taxonomy terms are used only for categorizing content on a Drupal powered web page, there should be no harm in deleting them. However sometimes taxonomy is used to store terms critical to the content they are referenced from and in this case steps should be taken to prevent an accidental deletion.

I have encountered such a case on a project I am working on which is soon to become a web platform for university students. When creating a faculty node, its name is being defined by choosing a term from the 'faculties' vocabulary. Deleting a term assigned to such a faculty node would lead to... well undesired effects.

Approach

When looking for the right hook you will find that there is no hook_taxonomy_pre_delete and using the existing hook_taxonomy_term_delete would be too late (the term would be deleted by then). (By the way, this problem persists across other entity types, like nodes - hoping to see some added hooks in D8.)

I will describe an easy way of preventing the deletion of a used taxonomy term, but be warned, this will only prevent the deletion of a term in the UI, it will not react to programmatically deleted terms.

Here is how this is going to look like:

Taxonomy term delete warning.

Some code:

  1. // In our custom module 'mymodule' let's go ahead and implement hook_form_alter() (what else!?) and switch on $form_id.
  2. function mymodule_form_alter(&$form, &$form_state, $form_id) {
  3. switch ($form_id) {
  4.  
  5. // This is the general term form.
  6. case 'taxonomy_form_term':
  7.  
  8. // Checking if we are on the delete confirmation form.
  9. if (isset($form['delete'])) {
  10.  
  11. // Getting the term id.
  12. $tid = $form['#term']->tid;
  13.  
  14. // Limiting the query to 30 to save resources when loading the nodes later on.
  15. $limit = 30;
  16.  
  17. // Getting node ids referencing this term and setting a limit.
  18. $result = taxonomy_select_nodes($tid, FALSE, $limit);
  19.  
  20. if (count($result) > 0) {
  21. $markup = t('This term is being used in nodes and cannot be deleted. Please remove this taxonomy term from the following nodes first:') . '<ul>';
  22.  
  23. // Looping through the node ids, loading nodes to get their names and urls.
  24. foreach($result as $nid) {
  25. $node = node_load($nid); // This is quite resource hungry, so if dealing with loads of nodes, make sure you apply a limit in taxonomy_select_nodes().
  26. if (!$node)
  27. continue;
  28. $markup .= '<li>' . l($node->title, 'node/' . $node->nid, array('attributes' => array('target'=>'_blank'))) . '</li>';
  29. }
  30.  
  31. // Appending some text with ellipsis at the end of list in case there might be more nodes referencing this term than the ones displayed.
  32. if (count($result) >= $limit)
  33. $markup .= '<li>' . t("... only the first @limit results are displayed.", array('@limit' => $limit)) . '</li>';
  34. $markup .= '</ul>';
  35.  
  36. // Using the render array's markup key to display our warning message.
  37. $form['description']['#markup'] = $markup;
  38.  
  39. // Removing the 'delete' button from the form.
  40. $form['actions']['submit']['#access'] = FALSE;
  41. // $form['actions']['submit']['#disabled'] = TRUE; // Disables the button instead of removing it.
  42. }
  43. }
  44. break;
  45. }
  46. }

Summary

TL;DR: We want to prevent people deleting taxonomy terms which are already associated to nodes. We use hook_form_alter on the form taxonomy_form_term and use the function taxonomy_select_nodes to check if a node uses the term in question, then we display links to the nodes and remove the 'delete' button.

Feel invited to comment!

Comments

You might want to remove the listing of nodes (as that can get very long) and change:
taxonomy_select_nodes($tid, FALSE, FALSE);
to
taxonomy_select_nodes($tid, FALSE, 1);

This is because you are really only interested that SOMETHING is using it. Another option is to set the third parameter to something like 3 instead and the list the nodes with an ellipsis.

Your return should be faster and less prone to using up tons of memory.

One last suggestion, you might consider just greying out the delete button: $form['actions']['delete']['#disabled'] = TRUE;

Wow this actually works as advertised, thanks a lot!

Thanks for the first comment on this blog. :)

As you can see in the screenshot, our use case needed the function to display the list of nodes which are using the term in order for the admin to decide, if they want to alter the nodes and then delete, or not to bother with the deletion. That's why we needed an accurate list showing the nodes.
However in case we are talking hundreds of nodes, your idea of limiting the query and adding an ellipsis is an elegant solution.
Disabling the button is the obvious alternative, would be $form['actions']['submit']['#disabled'] = TRUE; though.
Going to add a comment in the code to indicate that as well.

Thanks for posting this, works nicely. I probably need to figure out how to amend it to also check for other entities like profile2 that are using the term.
Steve

I am not a drupal expert or php expert, but have a basic knowledge in PHP. The code works well and thank you for this. It really helps. I'm trying to understand the code. I have two questions and hoping to get a reply from you soon.

1. What if I delete the children instead of deleting the parent term? I think it was been skipped from the code.
2. At line 32, you've used "if statement" but there is no curly bracket after that?

Hi Madelyn, glad the code is helpful!

1) This code is not meant to prevent deletion of parent/child terms. It prevents deletion of terms that are referenced by a node. Eg. if at least one of your nodes was tagged with a taxonomy term named 'tag1', you will not be able to delete 'tag1'.

However I just realized that there is a possible scenario of unintentional deletion even with this code in place. Before deleting a parent item, this code checks whether it is referenced by nodes, but does not check the term's children. The problem is Drupal will delete children automatically when deleting a parent term. My use case doesn't require parent/children relationships, so I haven't encountered this issue yet. If you find a clean solution for this, let me know.

2) When there is only one statement, you can skip the brackets to save a line of code.

  1. if ($show_insides)
  2. print 'I am inside the if statement.';
  3. print 'I am outside.';

Not sure if this adheres to Drupal coding standards. Either way correct indentation is critical for readability.

1. That is what I would like to prevent. Deleting the children when parent term is deleted.

I'm currently building an e-commerce website which requires multi level category, so I have to use the function taxonomy_get_children() to be able to get the children. I've disabled the button and set a message to delete the child first before deleting the parent term if the parent term has children. I've used taxonomy_select_nodes() from your code to check if there's a node attached to it. That is the easiest way for me. Please check below to check my code. I have removed loading the nodes and just added in the instruction to see the list of nodes in the view tab to prevent too much memory usage though you've set a limit already.

2. Thank you for explaining that to me. That was explained well. Thank you.

Here is my code:
function mymodule_form_taxonomy_form_term_alter(&$form, &$form_state) {
// Checking if we are on the delete confirmation form.
if (isset($form['delete'])) {

// Getting the term id.
$tid = $form['#term']->tid;

// Check if term has children
$children = taxonomy_get_children($tid);
if(count($children) > 0) {
drupal_set_message(t("This term has children and cannot be deleted until the children of this term has been deleted."), 'error');
$form['description']['#markup'] = '';
$form['actions']['submit']['#disabled'] = TRUE;
}

// Getting node ids referencing this term.
$results = taxonomy_select_nodes($tid, FALSE);
if (count($results) > 0 && !empty($results)) {
drupal_set_message(t("This term is being used and cannot be deleted. Please remove this taxonomy term from the node first."), 'error');
$form['description']['#markup'] = 'Click the view tab to be able to see the list of nodes.';
$form['actions']['submit']['#disabled'] = TRUE;
}
}
}

The code looks good, do some thorough testing and that should be it.

The two statements on this line

  1. if (count($results) > 0 && !empty($results)) {

both do the same. empty() works on an array and returns TRUE if the array has no elements. You can remove one of these checks.

Also I would probably split the two checks into their own functions for readability. One would be testing for referenced nodes, the other one would check for child terms. If this functionality is fragment of a bigger whole, I would go further and advise to start using objects (yes, you can code OOP in D7) as this is much cleaner and will start preparing you for Drupal 8. See this article to see where D8 is at.

Thanks for your great post!

Add new comment

The content of this field is kept private and will not be shown publicly.

Restricted HTML

  • Allowed HTML tags: <a href hreflang target> <em> <strong> <cite> <blockquote cite> <pre> <ul type> <ol start type> <li> <dl> <dt> <dd> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Get a quote in 24 hours

Wether a huge commerce system, or a small business website, we will quote the project within 24h of you pressing the following button: Get quote