9

Drupal version: 7.21
Field collection module version: 7.x-1.0-beta5

Short explanation: I'm busy trying to import field collections programmatically but when deleting some of them there's always some 'bogus' field collection left.

Long explanation: My users have a field collection field on their profile. This field collection contains 3 text fields. I want to import data from a custom sql database into the field collection of the user. This field collection can have multiple values. When I import the data for the first time everything works fine, I see the data in the fields of the field collection. Great.

But here comes the tricky part. Let's say I import for one specific user 5 rows from the custom database. They get added to the field collection, so this field collection has 5 items each containing 3 fields. Then I delete some rows from my custom database so that I have only 3 rows left for this user. I run the import again, updating the first 3 items of the field collection, but then I'm left with 2 items from the previous import. They should be deleted because I have only 3 imported rows but still 5 field collection items.

So I tried to delete these field collection items, but there's always one or more items left. The fields are empty when I look at the user profile but there's still something there. Let's say at this point I add 5 new rows for the user in my custom database, so I have 8 rows in total for this user. Then I run the import again. The first 3 items get updated, but then when I try to add the 4th row it still gets an entity id from the 4th field collection item, tries to update it but fails and returns this error:

 Fatal error: Call to undefined method stdClass::save()

I tried deleting the field collection items with each of these methods below:

// Method 1
entity_delete_multiple('field_collection_item', array($fc_id));

// Method 2
$field_collection_item = field_collection_item_load($fc_id);
$field_collection_item->delete();

// Method 3
$field_collection_item = field_collection_item_load($fc_id);
$field_collection_item->deleteRevision();

This is my full code:

function import_user_field_collection(&$user, $old_user_id) {

  // I do a query to get the rows I want to import for this specific user.
  db_set_active('custom_sql_database');
  $result = db_query("SELECT * FROM {users} WHERE user_id = :user_id", array(':user_id' => $old_user_id));

  db_set_active('default');
  $i = 0; // Keep count of how many rows I imported.
  foreach($result as $row) {
    // Check if the field collection item already exists.
    if(!empty($user->field_profile_diploma_opleiding[LANGUAGE_NONE][$i]['value'])) {
      // If it does exists, update this particular field collection item.
      $fc_id = $user->field_profile_diploma_opleiding[LANGUAGE_NONE][$i]['value'];
      $field_collection_item = entity_load('field_collection_item', array($fc_id));
      // These 3 text fields are children of the field collection field.
      $field_collection_item[$fc_id]->field_profile_diploma_instituut[LANGUAGE_NONE][0]['value'] = $row->instituut;
      $field_collection_item[$fc_id]->field_profile_diploma_vakgebied[LANGUAGE_NONE][0]['value'] = $row->vakgebied;
      $field_collection_item[$fc_id]->field_profile_diploma_jaar[LANGUAGE_NONE][0]['value'] = $row->jaar_diploma;
      $field_collection_item[$fc_id]->save(TRUE);
    } else {
      // If the field collection item doesn't exist I want to create a new field collection item.
      $field_collection_item = entity_create('field_collection_item', array('field_name' => 'field_profile_diploma_opleiding'));
      $field_collection_item->setHostEntity('user', $user);
      $field_collection_item->field_profile_diploma_instituut[LANGUAGE_NONE][0]['value'] = $row->instituut;
      $field_collection_item->field_profile_diploma_vakgebied[LANGUAGE_NONE][0]['value'] = $row->vakgebied;
      $field_collection_item->field_profile_diploma_jaar[LANGUAGE_NONE][0]['value'] = $row->jaar_diploma;
      $field_collection_item->save(TRUE);
    }
    $i++;
  }

  $fc_fields = field_get_items('user', $user, 'field_profile_diploma_opleiding');

  // Check if there are more field collection items than imported rows
  if(count($fc_fields) > $i) {
    for($i; $i <= count($fc_fields); $i++) {
      // Run through each field collection item that's left from the previous import and delete it.
      if(!empty($user->field_profile_diploma_opleiding[LANGUAGE_NONE][$i]['value'])) {
        // Method 1
        $fc_id = $user->field_profile_diploma_opleiding[LANGUAGE_NONE][$i]['value'];
        entity_delete_multiple('field_collection_item', array($fc_id));

        // Method 2
        //$field_collection_item = field_collection_item_load($fc_id);
        //$field_collection_item->delete();

        // Method 3
        //$field_collection_item = field_collection_item_load($fc_id);
        //$field_collection_item->deleteRevision();
      }
    }
  }
}

So my question is: How do I delete field collection items so that they're actually gone?

5
  • 2
    entity_delete_multiple is 100% definitely the right way to do it - have a look at the field_collection_field_delete function, which is what Field Collection itself uses to clean up items when the referenced field is removed
    – Clive
    Commented Apr 4, 2013 at 10:03
  • Thanks alot for your response, I appreciate it. Do you happen to know what arguments I should provide with field_collection_field_delete? I see the signature is field_collection_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) but I don't really know what values to put in: $entity (is this the user or the field collection?), $field (return value from field_collection_item_load?), $instance, $langcode (und?) and $items.
    – Smos
    Commented Apr 4, 2013 at 10:36
  • 1
    That particular function is a hook implementation, basically when any field is deleted the field name is passed to that function, and Field Collection checks if there's an FC entity associated to that field instance. If there is, it deletes it using entity_delete_multiple(). You might need to run cron a couple of times after you delete fields (field data is purged on a schedule so as not to burden a single page load with all that processing to do)
    – Clive
    Commented Apr 4, 2013 at 10:55
  • I tried using entity_delete_multiple again and I noticed that the items get deleted in the table field_collection_item but the fields still exist in the table field_data_field_collection_name. I think this causes the fatal error Call to undefined method stdClass::save() because they're supposed to be fields but they don't have a field collection item linked to it. When I use $field_collection_item->deleteRevision it deletes the data in both tables but when I save the user a row gets added to the table field_data_field_collection_name and that's a blank field collection item.
    – Smos
    Commented Apr 4, 2013 at 11:35
  • @Smos: hey buddy, can you help me with a similar issue(drupal.stackexchange.com/questions/239784/…)? I tried relevant bits of your code but I couldn't get it working.
    – sisko
    Commented Jul 4, 2017 at 0:08

5 Answers 5

14

I ran into a similar use case where I wanted to map some data into a field collection during hook_feeds_presave() since the source structure was too complex for Feeds. I found that entity_delete_multiple() removed the field collection items, but when I edited the node, there were still a bunch of empty field collection there. Unsetting and deleting did the trick, which I found here: https://drupal.stackexchange.com/a/31820/2762

If the feeds source has changed, I delete all field collection items and recreate. Hope this is helpful.

foreach ($node->field_international_activity[LANGUAGE_NONE] as $key => $value) {
  // Build array of field collection values.
  $field_collection_item_values[] = $value['value'];

  // Unset them.  
  unset($node->field_international_activity[LANGUAGE_NONE][$key]);
}

// Delete field collection items.
entity_delete_multiple('field_collection_item', $field_collection_item_values);
2
  • This 2 step approach is the only way which works AFAIK. Do not forget to node_save($node) your node. Commented Nov 13, 2013 at 11:53
  • @BernhardFürst actually we don't need node_save($node), DrupalEntityController will do this job
    – coffeduong
    Commented Jan 19, 2019 at 3:01
9

The best way to do this now is call $field_collection->delete() and that will handle everything.

 <?php
    /**
     * Deletes the field collection item and the reference in the host entity.
     */
    public function delete() {
      parent::delete();
      $this->deleteHostEntityReference();
    }

    /**
     * Deletes the host entity's reference of the field collection item.
     */
    protected function deleteHostEntityReference() {
      $delta = $this->delta();
      if ($this->item_id && isset($delta)) {
        unset($this->hostEntity->{$this->field_name}[$this->langcode][$delta]);
        entity_save($this->hostEntityType, $this->hostEntity);
      }
    }
 ?>
0

The above answers are not the best way, with unset all the other items disappeared fron the field collection, and the other way with ->delete() throws a bug with the Entity module.

Correct way. Well what I did was this:

In my case I wanted to delete the last item within the field collection

take a look to this code, (for beginers: "remember that you must have the $entity already loaded")

// Remove the field value
unset($entity->field_salida_mercanc_a[LANGUAGE_NONE][count($entity->field_salida_mercanc_a[LANGUAGE_NONE])-1]);

// Reset the array to zero-based sequential keys
$entity->field_salida_mercanc_a[LANGUAGE_NONE] = array_values($entity->field_salida_mercanc_a[LANGUAGE_NONE]);

// Save the entity
entity_save($entity_type, $entity);

thats all! the other way is

//Get the node wapper
$wrapper = entity_metadata_wrapper($entity_type, $entity);
//Get the last position of the array items, thanks to the ->value() statement you can get the complete Array properties.
$field_collection_item_value = $wrapper->field_collection_mine[count($wrapper->field_collection_mine->value())-1]->value();
//Do not use 'unset' to delete the item due to is not the correct way, I use the entity_delete_multiple provided by the entity API
entity_delete_multiple('field_collection_item', array($field_collection_item_value->item_id));

The way above is good using entity_metadata_wrapper function but with that way there is a complex bug that I do not know how to solve it, you can check it at https://drupal.org/node/1880312 and after apply the patch in #9 you get the next issue, check it here https://drupal.org/node/2186689 this bug is also if you use ->delete() function.

Hope it helps someone.

0

using vbo to delete the field collection items. it will auto remove the field relation with the field collection item host entity.

0

I had a similar issue, where I am importing data from a feed into a FC item. When an update to a host entity is made from the feed, and I'm importing those changes, I wanted to ensure that any existing FC items that no longer exist from the feed source were removed.

My solution:

// Load host entity that I'm updating.
$host_entity = node_load($nid);
$host_entity_wrapper = entity_metadata_wrapper('node', $host_entity);

// Clear out the references to the existing FC items so that I have a fresh start.
$host_entity_wrapper->field_my_fc_items->set(NULL);

// Re-create the FC items from the source feed data.
foreach ($feed_data->items as $feed_item) {
  $fc = entity_create('field_collection_item', array('field_name' => 'field_my_fc_items'));
  $fc->setHostEntity($host_entity);
  // Some some fields on the FC item from the feed data.
  $fc_wrapper = entity_metadata_wrapper('field_collection_item', $fc);
  $fc_wrapper->field_some_fc_item_field->set($feed_item->data);
}

// Sync other field info onto host entity.
...

$host_entity_wrapper->save();

And that's it. Field Collection's hook_field_update (field_collection_field_update) will take care of actually deleting any existing FC items that have been de-referenced.

The only downside to this is if there was no change in the FC data, it's deleted and re-created anyway. But that's not a big deal for me.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.