The field collection module allows you to add groups of fields in to a single field on to a node. The group of fields is simply an entity. When you use node clone to clone a node the reference to the entity is preserved. I think it would be better in most cases to create a new field collection entity and clone the existing values, otherwise if you change the field collection in one spot it updates on both nodes.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chalee’s picture

This is quite an important issue. I have experienced the problem here. I cloned a node with a multi-value field_collection field. Just deleting one item of the field in the cloned node made both nodes inaccessible showing the error below. I subscribe to the suggestion made above to create a separate instance of the entity.

-----------------------------------------------------------------------------------------------------------------------
Recoverable fatal error: Argument 2 passed to field_collection_item_access() must be an instance of FieldCollectionItemEntity, boolean given, called in ...\sites\all\modules\field_collection_table\field_collection_table.module on line 76 and defined in field_collection_item_access() (line 488 of ...\sites\all\modules\field_collection\field_collection.module).
-----------------------------------------------------------------------------------------------------------------------

valderama’s picture

Hi, I am also working with field_collections and node_clone here. I would also be happy to have this feature, just as mstrelan desscribed.

Can it be achieved using hook_clone_node_alter in the meanwhile? I think I'll give a try..

chalee’s picture

@valderama: I have managed to do it so yes it can be done.

First you reset the field_collection_item values which have been copied by node_clone using unset function as below:

unset($node->field_c_name['und']);

Then you create your new items using Entity API methods as below:

$field_collection_item = entity_create('field_collection_item', array('field_name' => 'field_c_name'));
$field_collection_item->setHostEntity('node', $node);
$field_collection_item->field_a[LANGUAGE_NONE][]['value'] = 'NEW VALUE';
$field_collection_item->field_b[LANGUAGE_NONE][]['value'] = NEW VALUE';
$field_collection_item->save();	

Remember this: When you save the field_collection_item it also saves the parent node. The consequence of this is that you will have duplicate clones if you are using the "Pre-populate the node form fields " method of node_clone. Therefore you must use the "Save as a new node then edit" method to avoid the duplicates. You can set this in node_clone's configuration page admin/config/content/clone

I hope this will help you.

valderama’s picture

thanks, chalee - that are actually valuable hints!

I think you can avoid saving the host entity by passing TRUE to field_collection_item->save( $skip_host_save = TRUE );

chalee’s picture

@valderama:Thanks for your hint about $skip_host_save = TRUE

clashar’s picture

+1

Anonymous’s picture

Hi chalee,

Thanks for your insights on this issue. This type of functionality is crucial for a future Drupal 7 site that I will be building.

Could you clarify a little further on how you implemented your suggested code?

I'd appreciate any help you can give.

Thanks!

Tony

Anonymous’s picture

Hi chalee,

Thanks for your insights on this issue. This type of functionality is crucial for a future Drupal 7 site that I will be building.

Could you clarify a little further on how you implemented your suggested code?

I'd appreciate any help you can give.

Thanks!

Tony

chalee’s picture

@tonylrm: I added a hook function called MY_MODULE_NAME_clone_node_alter(&$node, $context) inside MY_MODULE_NAME.module file. Inside that function I added the code in #3 above.

Anonymous’s picture

@chalee: Thanks for your assistance.

I have implemented your suggestion but have 2 remaining issues. I would appreciate it if I could get your assistance again...

1) After cloning..the values of the field collection items all state 'NEW VALUE'. I would like to have the values of the cloned field collection items the same as the original values. Did i do something incorrect?

2) The origial node's field collection is set to unlimited values... when cloning it only creates a single set of field collection items. Can you suggest modifications to your code that would enable cloning mulitple values of field collection items?

Thanks again!!!

chalee’s picture

1. The code snippet I gave in #3 is just a sample, that you have to change accordingly to use correct field names and assign correct values.

2. You need a 'foreach' loop to process all the items.

Try the function below which should ideally do everything you requested. Put the function in your .module file and call it inside the MY_MODULE_NAME_clone_node_alter() passing it the correct variable names for node and the field_collection field. Please note that this function is untested. I just wrote it directly here. Give feedback after testing it.

function clone_fc_items($node, $fc_field){
	$node_wrapper = entity_metadata_wrapper('node', $node);
	$old_fc_items =  $node_wrapper->{$fc_field}->value();
	unset($node_wrapper->{$fc_field}->value()); // Not sure if this will work. If not, replace it with unset($node->{$fc_field}['und']);
	foreach ($old_fc_items as $old_fc_item){
		$old_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $old_fc_item);
		$new_fc_item = entity_create('field_collection_item', array('field_name' => $fc_field));
		$new_fc_item->setHostEntity('node', $node);
		$new_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $new_fc_item);
		foreach (element_children($old_fc_item_entity) as $field_name){
			if (is_array($old_fc_item->{$field_name})){ 
				if (!empty($old_fc_item->{$field_name})){
					$new_fc_item_wrapper->{$field_name}->set($old_fc_item_wrapper->{$field_name}->value());
				}
			}	
		}
		$new_fc_item_wrapper->save(); 	
	}
}
kugta’s picture

Subscribe

chalee’s picture

@tonylrm: Did you try my function in #11 above?

Summit’s picture

Subscribing, greetings, Martijn

jox’s picture

@chalee: thanks for the code in #11. Here is the code I ended up while trying to make it work:

function clone_fc_items($node, $fc_field){
  $node_wrapper = entity_metadata_wrapper('node', $node);
  $old_fc_items =  $node_wrapper->{$fc_field}->value();

  // "element_children($old_fc_item_entity)" does not work (element_children expects an array).
  // So we obtain the field names from field_info_instances():
  $field_info_instances = field_info_instances();
  $field_names = element_children($field_info_instances['field_collection_item'][$fc_field]);

  // This indeed does not work:
  // unset($node_wrapper->{$fc_field}->value()); // Not sure if this will work. If not, replace it with unset($node->{$fc_field}['und']);
  // So using the alternative (and using the nodes language instead of 'und'):
  unset($node->{$fc_field}[$node->language]);

  foreach ($old_fc_items as $old_fc_item) {
    $old_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $old_fc_item);
    $new_fc_item = entity_create('field_collection_item', array('field_name' => $fc_field));
    $new_fc_item->setHostEntity('node', $node);
    $new_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $new_fc_item);

    foreach ($field_names as $field_name) {
      if (is_array($old_fc_item->{$field_name})){
        if (!empty($old_fc_item->{$field_name})){
          $new_fc_item_wrapper->{$field_name}->set($old_fc_item_wrapper->{$field_name}->value());
        }
      }
    }

    $new_fc_item_wrapper->save();
  }
}

Note that if the field collection contains date fields, it will produce an error. This is because the Date module does not provide entity property setters for the date fields. See #1153766: provide a property setter callback

At the moment my workaround for this is by copying the raw array instead of using field->set() for my date fields.

Replacing:

          $new_fc_item_wrapper->{$field_name}->set($old_fc_item_wrapper->{$field_name}->value());

With:

          if ($field_name == 'field_time_range') {
            // TODO: This is a workaround. Date module does not provide property setter methods.
            $new_fc_item->{$field_name} = $old_fc_item->{$field_name};
          } else {
            $new_fc_item_wrapper->{$field_name}->set($old_fc_item_wrapper->{$field_name}->value());
          }

(This is customized for my specific date field named 'field_time_range'.)

chalee’s picture

@jox: Thanks for the improvements to the code. I had noted the 3 issues you raised about my code in #11 but had not had time to figure out how to solve them. I guess we have to wait for #1153766: provide a property setter callback for the date issue before we can fully have a generic, not hardcoded, function to use.

jox’s picture

@chalee: I have noticed that node_clone does properly clone date fields, if they are normal fields in the host node. This is because the fields do really just get raw copied (because of "$node = clone $original_node").

So why not doing this for the field_collection data as well. In fact, the entity method "$new->set($old->value())" should end up exactly the same (if not, something is wrong). It is actually an indirection.

Any field that would not get properly copied like this, would need a special handling anyway. That would be any field referencing something that should get cloned as well. A nested field_collection would be an example (if that is possible yet at all).

So, in my opinion, for now, the best generic base solution would be using the copy method all the time:


function clone_fc_items($node, $fc_field){
  $node_wrapper = entity_metadata_wrapper('node', $node);
  $old_fc_items =  $node_wrapper->{$fc_field}->value();

  $field_info_instances = field_info_instances();
  $field_names = element_children($field_info_instances['field_collection_item'][$fc_field]);

  unset($node->{$fc_field}[$node->language]);

  foreach ($old_fc_items as $old_fc_item) {
    $old_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $old_fc_item);
    $new_fc_item = entity_create('field_collection_item', array('field_name' => $fc_field));
    $new_fc_item->setHostEntity('node', $node);
    $new_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $new_fc_item);

    foreach ($field_names as $field_name) {
      if (is_array($old_fc_item->{$field_name})){
        if (!empty($old_fc_item->{$field_name})){
          // copy the field data (which is an array)
          $new_fc_item->{$field_name} = $old_fc_item->{$field_name};
        }
      }
    }
    $new_fc_item_wrapper->save();
  }
}

I'm running this right now with no problems so far. A field_collection with a bunch of field types (including a date range, text, list, float values and a custom field) get cloned perfectly.

One benefit is also, that it will not be affected by any bugs or incompletenesses in the entity field (or metadata) API implementation of fields (which is the problem we had).

chalee’s picture

@jox: Very good improvements again. I tested the code and it works well except that it fails if the field collection (fc) field is single-valued. The reason is that in line 4

$old_fc_items =  $node_wrapper->{$fc_field}->value();

$node_wrapper->{$fc_field}->value() function returns an object and not array for a single-valued fc, yet the following foreach statement is expecting an array. So to correct this I replaced line 4 with the following code to make sure that when we get to the foreach statement the variable $old_fc_items will always be an array.

  $old_fc_items = array();
  $temp_value =  $node_wrapper->{$fc_field}->value();
  if (is_array($temp_value)){
  	$old_fc_items = $temp_value;
  } else {
  	$old_fc_items[] = $temp_value;
  }
jox’s picture

@chalee: Oh, great you discovered that!

Here's a slightly shorter version that does the same:

  $old_fc_items = $node_wrapper->{$fc_field}->value();
  if (!is_array($old_fc_items)) {
    $old_fc_items = array($old_fc_items);
  }

For convenience I'll post the updated function again:

function clone_fc_items($node, $fc_field){
  $node_wrapper = entity_metadata_wrapper('node', $node);
  $old_fc_items = $node_wrapper->{$fc_field}->value();
  if (!is_array($old_fc_items)) {
    $old_fc_items = array($old_fc_items);
  }

  $field_info_instances = field_info_instances();
  $field_names = element_children($field_info_instances['field_collection_item'][$fc_field]);

  unset($node->{$fc_field}[$node->language]);

  foreach ($old_fc_items as $old_fc_item) {
    $old_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $old_fc_item);
    $new_fc_item = entity_create('field_collection_item', array('field_name' => $fc_field));
    $new_fc_item->setHostEntity('node', $node);
    $new_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $new_fc_item);

    foreach ($field_names as $field_name) {
      if (is_array($old_fc_item->{$field_name})){
        if (!empty($old_fc_item->{$field_name})){
          // copy the field data (which is an array)
          $new_fc_item->{$field_name} = $old_fc_item->{$field_name};
        }
      }
    }
    $new_fc_item_wrapper->save();
  }
}
chalee’s picture

@jox: I have made a few more changes to cater for fc embedded within fc.

1. Added few lines to check if a field is a field collection and recursively call the same function to clone it.
2. Added two function parameters: $entity_type and $language. I could not find a uniform way to get these from the passed entity object.
3. Renamed some variables to reflect the fact that any entity not necessarily a node could be passed.
4. I removed the if (is_array($old_fc_item->{$field_name})){ which is not necessary since we are using the field_info_instances function to get the field list.

function clone_fc_items($entity_type, &$entity, $fc_field, $language = LANGUAGE_NONE){
  $entity_wrapper = entity_metadata_wrapper($entity_type, $entity);
  $old_fc_items = $entity_wrapper->{$fc_field}->value();
  if (!is_array($old_fc_items)) {
    $old_fc_items = array($old_fc_items);
  }

  $field_info_instances = field_info_instances();
  $field_names = element_children($field_info_instances['field_collection_item'][$fc_field]);

  unset($entity->{$fc_field}[$language]);

  foreach ($old_fc_items as $old_fc_item) {
    $old_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $old_fc_item);
    $new_fc_item = entity_create('field_collection_item', array('field_name' => $fc_field));
    $new_fc_item->setHostEntity($entity_type, $entity);
    $new_fc_item_wrapper = entity_metadata_wrapper('field_collection_item', $new_fc_item);
	
    foreach ($field_names as $field_name) {
		//if (is_array($old_fc_item->{$field_name})){
		    if (!empty($old_fc_item->{$field_name})){
		    	$new_fc_item->{$field_name} = $old_fc_item->{$field_name};
		    }
		//}	
    }
    $new_fc_item_wrapper->save();
	
	//Now check if any of the fields in the newly cloned fc item is a field collection and recursively call this function to properly clone it.
	foreach ($field_names as $field_name) {
	    if (!empty($new_fc_item->{$field_name})){
	    	$field_info = field_info_field($field_name);
			if ($field_info['type'] == 'field_collection'){
				clone_fc_items('field_collection_item',$new_fc_item, $field_name,$language);
			} 	
	    }
	}
	
  }
}
charlie-s’s picture

@chalee: Can you post a sample of the function call? Is it:

<?php
function mymodule_clone_node_alter(&$node, $context) {
  clone_fc_items('node', $node, 'field_collection_name', LANGUAGE_NONE);
}
?>

Or does entity_type and entity refer to the field collection?

Edit: I was a little hesitant to try this without getting confirmation since I didn't want to mess up any existing nodes (and have a single field collection attached to multiple nodes) but I gave it a shot and it works brilliantly. Here's what I'm running, so others can see.

<?php
function mymodule_clone_node_alter(&$node, $context) {
  clone_fc_items('node', $node, 'field_product_colors', LANGUAGE_NONE);
  clone_fc_items('node', $node, 'field_product_sizes', LANGUAGE_NONE);
}
?>

clone_fc_items() is pulled verbatim from #20.

mrconnerton’s picture

I like the code produced here. I need this functionality right now so I'm going to implement this as a patch to field_collection module as I said here #1304214: Doesn't clone properly

chalee’s picture

@mrconnerton: Sure go ahead and create the patch, but wouldn't it be better to make the patch for Node clone module instead.

mrconnerton’s picture

@chalee I have the code, just have to get the patch online.

I just see it from the perspective of:

  • Clone module creates hook_clone_node_alter so other modules can make changes during the clone process
  • Field Collection needs to do extra stuff during the clone process
  • Thus field collection should implement hook_clone_node_alter

I will try to get this patch up as soon as my schedule allows it.

chalee’s picture

@mrconnerton: It makes sense.

darin73’s picture

I have the same problem (cloning) also with node revision. All the revisions use the same field-collection

mrconnerton’s picture

Status: Active » Closed (won't fix)

I have posted a patch for review here: http://drupal.org/node/1304214#comment-5556352

It's basically #20 but implemented in field_collection.module where I think this code belongs.

seaarg’s picture

Solution on #20 worked flawlessly for me, I have several "complex" field collections within a content type using media, date, etc. Thanks so much for this code!

lukus’s picture

Any update on the patch? Could this be committed?

cjtriplett’s picture

Yes it seems like this should be committed? Why wouldn't it be?

marcusx’s picture

Status: Closed (won't fix) » Reviewed & tested by the community

Changing status. Or nobody will pay attention who can commit this.

bluetegu’s picture

Check http://drupal.org/node/1304214#comment-6339298 and the code in that thread.

dstorozhuk’s picture

My variant of cloning function.

/**
 * Clone field_collection field items from $entity in recursive way and attach
 * them to the $recepient_entity.
 *
 * @param type $entity_type
 *  Type of $entity object.
 * @param type $entity
 *  Base $entity fields will be copied from.
 * @param type $recepient_entity
 *  New entity fields will be copied to.
 * @param type $fc_field
 *  field_collection field name
 *
 */
function ai_node_similar_lng_clone_fc_field($entity_type, $entity, &$recepient_entity, $fc_field) {

  $old_fc_items = field_get_items($entity_type, $entity, $fc_field);

  if (!is_array($old_fc_items)) {
    $old_fc_items = array($old_fc_items);
  }

  $field_info_instances = &drupal_static('ai_node_similar_lng_field_info_instances', field_info_instances());
  $field_info_fields = &drupal_static('ai_node_similar_lng_field_info_fields', field_info_fields());

  $field_names = element_children($field_info_instances['field_collection_item'][$fc_field]);

  $language = field_language($entity_type, $entity, $fc_field);

  foreach ($old_fc_items as $delta => $old_fc_field_item) {

    if ($old_fc_item = field_collection_item_load($old_fc_field_item['value'])) {

      $new_fc_item = clone $old_fc_item;

       foreach ($field_names as $field_name) {
          if (!empty($old_fc_item->{$field_name})){
            $field_info = $field_info_fields[$field_name];

            unset($new_fc_item->{$field_name});

            if ($field_info['type'] == 'field_collection'){
                ai_node_similar_lng_clone_fc_field('field_collection_item', $old_fc_item, $new_fc_item, $field_name);
            }
            else {
              $new_fc_item->{$field_name} = $old_fc_item->{$field_name};
            }
          }
      }

      $new_fc_item->item_id = NULL;
      $new_fc_item->save(TRUE);

      $recepient_entity->{$fc_field}[$language][$delta]['value'] = $new_fc_item->item_id;
    }
  }
}

Examlpe of how to clone FC field from existing node to newely created node:

// If you need to create node in other language.
$lng_code = 'en-gb'; 
$source_node = node_load(12325);
$new_node = clone $source_node;
$new_node->language = $lng_code;
$new_node->nid = NULL;
$new_node->vid = NULL;
$new_node->is_new = TRUE;
unset(
     $new_node->nid,
    $new_node->vid,
    $new_node->original,
    $new_node->menu
);
ai_node_similar_lng_clone_fc_field('node', $source_node , $new_node, 'field_collection_name');
node_save($new_node);

The main difference between this function and listed above - my function can clone FC to blank node object.

dstorozhuk’s picture

Also who look for solution to clone field collection can check this module:
http://drupal.org/project/replicate_field_collection

gregory_kapustin’s picture

After using #33, which successfully cloned my collections fields on entities, I had a problem : entity form showed empty rows for each field collection item.

I bypassed it with the following patch.

pwolanin’s picture

Status: Reviewed & tested by the community » Needs work

There is no complete patch here.

In general I don't like adding custom support to clone for wacky non-core field modules so this has a high bar to meet.

psychobyte’s picture

Hi,

I'm affected by this issue is there an actual fix or would you suggest using this module https://drupal.org/project/field_collection_node_clone

Thanks.

Renee S’s picture

Issue summary: View changes

@psychobyte, what is the difference between that module and "replicate field collection" module? The latter seems more powerful as it's entity-agnostic.

psychobyte’s picture

@Renee S

I believe that is the solution I'm looking for. Thx.

jmuzz’s picture

Status: Needs work » Closed (duplicate)

Cloning nodes with field collections is supported since the application of the patch in #1316162: Support content translation and host entity cloning. My understanding is this issue should be solved now, but please reopen with an explanation if it is not.

roynilanjan’s picture

Some work around as,

function [MODULE]__field_collection(&$node, $context) {
  foreach ($node as $field_name => $field_value) {
    $field_information = field_info_field($field_name);
    if ($field_information['type'] == 'field_collection') {
      $name = $field_information['field_name'];
      $count = count($node->{$name}[LANGUAGE_NONE]);
      for ($i = 0; $i < $count; $i++) {
        $old_item_collection = field_collection_item_load($node->{$name}[LANGUAGE_NONE][$i]['value']);
        $new_item_collection = entity_create('field_collection_item', array('field_name' => $name));
        foreach ($old_item_collection as $field => $value) {
          if ($field!= 'item_id' && $field!= 'revision_id' && $field!= 'field_name'
            && $field!= 'default_revision' && $field!= 'archived') {
            $new_item_collection->{$field} = $old_item_collection->{$field};
            $field_information_array = field_info_field($field);
            if ($field_information_array['type'] == 'field_collection') {
              [MODULE]__field_collection(&$node, $context);
            }
          }
        }
        unset($node->{$name}[LANGUAGE_NONE][$i]['value']);
        $new_item_collection->setHostEntity('node', $node);
      }
    }
  }
}


/**
 * Implements hook_clone_node_alter().
 */
function [MODULE]_clone_node_alter(&$node, $context) {
  [MODULE]__field_collection(&$node, $context);
}

Now after this there is always a blank item added in the field collection(which is render from form API) & work around along with
*form_alter*

function MODULE_form_alter(&$form, &$form_state, $form_id) {
if (arg(2) == 'clone') {
    foreach ($form_state['field'] as $field_name => $field_config) {
      if ($field_config[LANGUAGE_NONE]['field']['type'] == 'field_collection') {
        foreach ($form[$field_name][LANGUAGE_NONE] as $key => $entity) {
          $entity_elements = get_object_vars($entity['#entity']);

          // Generally field collection minimum contains the six elements i.e.
          // item_id,revision_id,field_name,archived,is_new so we remove the fields those
          // doesn't have child elements at-all i.e. blank field during clone.
          if (count($entity_elements) == 6) {
            unset($form[$field_name][LANGUAGE_NONE][$key]);
          }
        }
      }
    }
  }
}

roynilanjan’s picture

Status: Closed (duplicate) » Needs review
colan’s picture

Status: Needs review » Closed (duplicate)

Please read #40. The previous comment contains neither an explanation, nor code in patch format.

roynilanjan’s picture

Status: Closed (duplicate) » Needs review

As far #40, it's being explained regrading the problem of *field_collection*, not related to *node clone*..
#41 tries to explain the probable solution(not a *patch*) using *node-clone* api and *form_alter*, as the way #3 use the *unset* it makes an improper behavior with context of node-clone, it's actually not cloning the field values rather it delete the existing field_collection values from the form during the clone.

pwolanin’s picture

Status: Needs review » Fixed

I think this module solves the problem: https://www.drupal.org/project/field_collection_node_clone

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

pratip.ghosh’s picture

Hi,

Is there a way to update the already cloned nodes whose field collection ID is now the same as the one from which it was cloned?