field_collection.module 87 KB
Newer Older
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
1
2
3
4
<?php

/**
 * @file
5
 * Module implementing field collection field type.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
6
7
8
9
10
 */

/**
 * Implements hook_help().
 */
11
function field_collection_help($path, $arg) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
12
  switch ($path) {
13
    case 'admin/help#field_collection':
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
14
15
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
16
      $output .= '<p>' . t('The field collection module provides a field, to which any number of fields can be attached. See the <a href="@field-help">Field module help page</a> for more information about fields.', array('@field-help' => url('admin/help/field'))) . '</p>';
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
17
18
19
20
      return $output;
  }
}

21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * Implements hook_form_alter().
 *
 * Checks for a value set by the embedded widget so fields are not displayed
 * with the 'all languages' hint incorrectly.
 */
function field_collection_form_alter(&$form, &$form_state) {
  if (!empty($form['#field_collection_translation_fields'])) {
    foreach ($form['#field_collection_translation_fields'] as $address) {
      drupal_array_set_nested_value($form, array_merge($address, array('#multilingual')), TRUE);
    }
  }
}

35
36
37
38
39
40
41
42
/**
 * Implements hook_form_FORM_ID_alter() for field_ui_field_overview_form().
 *
 * Make the names of the field collection fields into links to edit the fields
 * for that field collection on the host's field edit page.
 */
function field_collection_form_field_ui_field_overview_form_alter(&$form, &$form_state) {
  if (count($form['#fields'])) {
Ra Mänd's avatar
Ra Mänd committed
43
    foreach ($form['fields'] as $fieldname => $field) {
44
45
46
47
48
49
50
51
52
53
54
      if (!isset($field['type']['#title'])) {
        continue;
      }
      if ($field['type']['#title'] == 'Field collection') {
        $form['fields'][$fieldname]['field_name']['#markup'] =
          l($form['fields'][$fieldname]['field_name']['#markup'], 'admin/structure/field-collections/' . str_replace('_', '-', $fieldname) . '/fields');
      }
    }
  }
}

55
56
57
58
59
60
61
62
63
/**
 * Implements hook_ctools_plugin_directory().
 */
function field_collection_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools') {
    return 'ctools/' . $plugin;
  }
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
64
65
66
/**
 * Implements hook_entity_info().
 */
67
function field_collection_entity_info() {
68
  $return['field_collection_item'] = array(
69
    'label' => t('Field collection item'),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
70
71
    'label callback' => 'entity_class_label',
    'uri callback' => 'entity_class_uri',
72
    'entity class' => 'FieldCollectionItemEntity',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
73
    'controller class' => 'EntityAPIController',
74
    'base table' => 'field_collection_item',
75
    'revision table' => 'field_collection_item_revision',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
76
    'fieldable' => TRUE,
77
78
79
    // For integration with Redirect module.
    // @see http://drupal.org/node/1263884
    'redirect' => FALSE,
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
80
    'entity keys' => array(
81
      'id' => 'item_id',
82
      'revision' => 'revision_id',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
83
84
      'bundle' => 'field_name',
    ),
85
    'module' => 'field_collection',
86
87
88
89
    'view modes' => array(
      'full' => array(
        'label' => t('Full content'),
        'custom settings' => FALSE,
Ra Mänd's avatar
Ra Mänd committed
90
      ),
91
    ),
92
    'access callback' => 'field_collection_item_access',
93
    'deletion callback' => 'field_collection_item_delete',
94
95
96
97
98
99
    'metadata controller class' => 'FieldCollectionItemMetadataController',
    'translation' => array(
      'entity_translation' => array(
        'class' => 'EntityTranslationFieldCollectionItemHandler',
      ),
    ),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
100
101
  );

102
103
104
105
106
  // Add info about the bundles. We do not use field_info_fields() but directly
  // use field_read_fields() as field_info_fields() requires built entity info
  // to work.
  foreach (field_read_fields(array('type' => 'field_collection')) as $field_name => $field) {
    $return['field_collection_item']['bundles'][$field_name] = array(
107
      'label' => t('Field collection @field', array('@field' => $field_name)),
108
109
110
111
      'admin' => array(
        'path' => 'admin/structure/field-collections/%field_collection_field_name',
        'real path' => 'admin/structure/field-collections/' . strtr($field_name, array('_' => '-')),
        'bundle argument' => 3,
112
        'access arguments' => array('administer field collections'),
113
114
      ),
    );
115
116
117
118
119
120
121
122
123
124
125
126
127

    $path = field_collection_field_get_path($field) . '/%field_collection_item';
    // Enable the first available path scheme as default one.
    if (!isset($return['field_collection_item']['translation']['entity_translation']['base path'])) {
      $return['field_collection_item']['translation']['entity_translation']['base path'] = $path;
      $return['field_collection_item']['translation']['entity_translation']['path wildcard'] = '%field_collection_item';
      $return['field_collection_item']['translation']['entity_translation']['default_scheme'] = $field_name;
    }
    else {
      $return['field_collection_item']['translation']['entity_translation']['path schemes'][$field_name] = array(
        'base path' => $path,
      );
    }
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
128
  }
129

130
131
132
133
134
  if (module_exists('entitycache')) {
    $return['field_collection_item']['field cache'] = FALSE;
    $return['field_collection_item']['entity cache'] = TRUE;
  }

135
  return $return;
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
136
137
}

138
139
140
141
142
143
144
145
146
147
/**
 * Provide the original entity language.
 *
 * If a language property is defined for the current entity we synchronize the
 * field value using the entity language, otherwise we fall back to
 * LANGUAGE_NONE.
 *
 * @param $entity_type
 * @param $entity
 *
Ra Mänd's avatar
Ra Mänd committed
148
 * @return string
149
150
151
152
153
154
155
156
157
158
159
160
161
 *   A language code
 */
function field_collection_entity_language($entity_type, $entity) {
  if (module_exists('entity_translation') && entity_translation_enabled($entity_type)) {
    $handler = entity_translation_get_handler($entity_type, $entity);
    $langcode = $handler->getLanguage();
  }
  else {
    $langcode = entity_language($entity_type, $entity);
  }
  return !empty($langcode) ? $langcode : LANGUAGE_NONE;
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
162
163
164
/**
 * Menu callback for loading the bundle names.
 */
165
function field_collection_field_name_load($arg) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
166
  $field_name = strtr($arg, array('-' => '_'));
167
  if (($field = field_info_field($field_name)) && $field['type'] == 'field_collection') {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
168
169
170
171
172
    return $field_name;
  }
}

/**
173
 * Loads a field collection item.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
174
 *
Ra Mänd's avatar
Ra Mänd committed
175
 * @return
176
 *   The field collection item entity or FALSE.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
177
 */
178
179
function field_collection_item_load($item_id, $reset = FALSE) {
  $result = field_collection_item_load_multiple(array($item_id), array(), $reset);
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
180
181
182
  return $result ? reset($result) : FALSE;
}

183
184
185
186
/**
 * Loads a field collection revision.
 *
 * @param $revision_id
Ra Mänd's avatar
Ra Mänd committed
187
188
189
190
191
 *   The id of the revision to load.
 *
 * @return
 *   The entity object, or FALSE if there is no entity with the given revision
 *   id.
192
193
194
195
196
 */
function field_collection_item_revision_load($revision_id) {
  return entity_revision_load('field_collection_item', $revision_id);
}

197
/**
198
 * Loads field collection items.
199
 *
Ra Mänd's avatar
Ra Mänd committed
200
201
202
203
204
205
206
207
208
209
 * @param $ids
 *   An array of entity IDs, or FALSE to load all entities.
 * @param $conditions
 *   (deprecated) An associative array of conditions on the base table, where
 *   the keys are the database fields and the values are the values those
 *   fields must have. Instead, it is preferable to use EntityFieldQuery to
 *   retrieve a list of entity IDs loadable by this function.
 * @param $reset
 *   Whether to reset the internal cache for the requested entity type.
 *
210
 * @return
211
 *   An array of field collection item entities.
212
213
214
215
216
 */
function field_collection_item_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) {
  return entity_load('field_collection_item', $ids, $conditions, $reset);
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
217
218
219
/**
 * Implements hook_menu().
 */
220
function field_collection_menu() {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
221
  $items = array();
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
222
  if (module_exists('field_ui')) {
223
    $items['admin/structure/field-collections'] = array(
224
225
      'title' => 'Field collections',
      'description' => 'Manage fields on field collections.',
226
      'page callback' => 'field_collections_overview',
227
      'access arguments' => array('administer field collections'),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
228
      'type' => MENU_NORMAL_ITEM,
229
      'file' => 'field_collection.admin.inc',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
230
231
232
    );
  }

233
  // Add menu paths for viewing/editing/deleting field collection items.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
234
  foreach (field_info_fields() as $field) {
235
236
    if ($field['type'] == 'field_collection') {
      $path = field_collection_field_get_path($field);
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
237
238
      $count = count(explode('/', $path));

239
240
      $items[$path . '/%field_collection_item'] = array(
        'page callback' => 'field_collection_item_page_view',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
241
        'page arguments' => array($count),
242
243
        'access callback' => 'entity_access',
        'access arguments' => array('view', 'field_collection_item', $count),
244
        'file' => 'field_collection.pages.inc',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
245
      );
246
      $items[$path . '/%field_collection_item/view'] = array(
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
247
248
249
250
        'title' => 'View',
        'type' => MENU_DEFAULT_LOCAL_TASK,
        'weight' => -10,
      );
251
      $items[$path . '/%field_collection_item/edit'] = array(
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
252
        'page callback' => 'drupal_get_form',
253
        'page arguments' => array('field_collection_item_form', $count),
254
255
        'access callback' => 'entity_access',
        'access arguments' => array('update', 'field_collection_item', $count),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
256
257
258
        'title' => 'Edit',
        'type' => MENU_LOCAL_TASK,
        'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
259
        'file' => 'field_collection.pages.inc',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
260
      );
261
      $items[$path . '/%field_collection_item/delete'] = array(
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
262
        'page callback' => 'drupal_get_form',
263
        'page arguments' => array('field_collection_item_delete_confirm', $count),
264
265
        'access callback' => 'entity_access',
        'access arguments' => array('delete', 'field_collection_item', $count),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
266
267
268
        'title' => 'Delete',
        'type' => MENU_LOCAL_TASK,
        'context' => MENU_CONTEXT_INLINE,
269
        'file' => 'field_collection.pages.inc',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
270
      );
271
272
      // Add entity type and the entity id as additional arguments.
      $items[$path . '/add/%/%'] = array(
273
        'page callback' => 'field_collection_item_add',
274
        'page arguments' => array($field['field_name'], $count + 1, $count + 2),
275
276
        // The pace callback takes care of checking access itself.
        'access callback' => TRUE,
277
        'file' => 'field_collection.pages.inc',
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
278
      );
279
280
281
282
      // Add menu items for dealing with revisions.
      $items[$path . '/%field_collection_item/revisions/%field_collection_item_revision'] = array(
        'page callback' => 'field_collection_item_page_view',
        'page arguments' => array($count + 2),
283
284
        'access callback' => 'entity_access',
        'access arguments' => array('view', 'field_collection_item', $count + 2),
285
286
        'file' => 'field_collection.pages.inc',
      );
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
287
288
    }
  }
289

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
290
291
292
  return $items;
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
293
/**
294
 * Implements hook_menu_alter() to fix the field collections admin UI tabs.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
295
 */
296
function field_collection_menu_alter(&$items) {
297
  if (module_exists('field_ui') && isset($items['admin/structure/field-collections/%field_collection_field_name/fields'])) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
298
    // Make the fields task the default local task.
299
    $items['admin/structure/field-collections/%field_collection_field_name'] = $items['admin/structure/field-collections/%field_collection_field_name/fields'];
300
301
302
303
304
305
    $item = &$items['admin/structure/field-collections/%field_collection_field_name'];
    $item['type'] = MENU_NORMAL_ITEM;
    $item['title'] = 'Manage fields';
    $item['title callback'] = 'field_collection_admin_page_title';
    $item['title arguments'] = array(3);

306
    $items['admin/structure/field-collections/%field_collection_field_name/fields'] = array(
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
307
308
309
310
311
312
313
      'title' => 'Manage fields',
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => 1,
    );
  }
}

314
315
/**
 * Menu title callback.
Ra Mänd's avatar
Ra Mänd committed
316
317
 *
 * return string
318
319
320
321
322
 */
function field_collection_admin_page_title($field_name) {
  return t('Field collection @field_name', array('@field_name' => $field_name));
}

323
324
/**
 * Implements hook_admin_paths().
Ra Mänd's avatar
Ra Mänd committed
325
326
 *
 * @return array
327
328
329
330
331
332
333
334
335
336
337
 */
function field_collection_admin_paths() {
  if (variable_get('node_admin_theme')) {
    return array(
      'field-collection/*/*/edit' => TRUE,
      'field-collection/*/*/delete' => TRUE,
      'field-collection/*/add/*/*' => TRUE,
    );
  }
}

338
339
340
341
342
/**
 * Implements hook_permission().
 */
function field_collection_permission() {
  return array(
Ra Mänd's avatar
Ra Mänd committed
343
    'administer field collections' => array(
344
345
      'title' => t('Administer field collections'),
      'description' => t('Create and delete fields on field collections.'),
346
347
348
349
    ),
  );
}

350
351
352
353
354
355
356
357
358
359
/**
 * Determines whether the given user has access to a field collection.
 *
 * @param $op
 *   The operation being performed. One of 'view', 'update', 'create', 'delete'.
 * @param $item
 *   Optionally a field collection item. If nothing is given, access for all
 *   items is determined.
 * @param $account
 *   The user to check for. Leave it to NULL to check for the global user.
Ra Mänd's avatar
Ra Mänd committed
360
 *
361
362
363
364
 * @return boolean
 *   Whether access is allowed or not.
 */
function field_collection_item_access($op, FieldCollectionItemEntity $item = NULL, $account = NULL) {
365
366
367
368
369
370
  // We do not support editing field collection revisions that are not used at
  // the hosts default revision as saving the host might result in a new default
  // revision.
  if (isset($item) && !$item->isInUse() && $op != 'view') {
    return FALSE;
  }
371
  if (user_access('administer field collections', $account)) {
372
373
    return TRUE;
  }
374
  if (!isset($item)) {
375
    return FALSE;
376
377
378
  }
  $op = $op == 'view' ? 'view' : 'edit';
  // Access is determined by the entity and field containing the reference.
379
  $field = field_info_field($item->field_name);
380
  $entity_access = entity_access($op == 'view' ? 'view' : 'update', $item->hostEntityType(), $item->hostEntity(), $account);
381
  return $entity_access && field_access($op, $field, $item->hostEntityType(), $item->hostEntity(), $account);
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
382
383
}

384
385
386
387
388
/**
 * Deletion callback
 */
function field_collection_item_delete($id) {
  $fci = field_collection_item_load($id);
389
  if (!empty($fci)) {
390
391
    $fci->delete();
  }
392
393
}

394
395
396
397
398
399
400
/**
 * Implements hook_theme().
 */
function field_collection_theme() {
  return array(
    'field_collection_item' => array(
      'render element' => 'elements',
401
      'template' => 'field-collection-item',
402
    ),
403
404
405
    'field_collection_view' => array(
      'render element' => 'element',
    ),
406
407
408
  );
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
409
410
411
/**
 * Implements hook_field_info().
 */
412
function field_collection_field_info() {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
413
  return array(
414
    'field_collection' => array(
415
      'label' => t('Field collection'),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
416
417
      'description' => t('This field stores references to embedded entities, which itself may contain any number of fields.'),
      'instance_settings' => array(),
418
      'default_widget' => 'field_collection_hidden',
419
      'default_formatter' => 'field_collection_view',
420
      // As of now there is no UI for setting the path.
421
422
423
      'settings' => array(
        'path' => '',
        'hide_blank_items' => TRUE,
424
        'hide_initial_item' => FALSE,
425
      ),
426
      // Add entity property info.
427
428
      'property_type' => 'field_collection_item',
      'property_callbacks' => array('field_collection_entity_metadata_property_callback'),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
429
430
431
432
    ),
  );
}

433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
/**
 * Implements hook_field_instance_settings_form().
 */
function field_collection_field_instance_settings_form($field, $instance) {

  $element['fieldset'] = array(
    '#type' => 'fieldset',
    '#title' => t('Default value'),
    '#collapsible' => FALSE,
    // As field_ui_default_value_widget() does, we change the #parents so that
    // the value below is writing to $instance in the right location.
    '#parents' => array('instance'),
  );
  // Be sure to set the default value to NULL, e.g. to repair old fields
  // that still have one.
  $element['fieldset']['default_value'] = array(
    '#type' => 'value',
    '#value' => NULL,
  );
  $element['fieldset']['content'] = array(
    '#pre' => '<p>',
    '#markup' => t('To specify a default value, configure it via the regular default value setting of each field that is part of the field collection. To do so, go to the <a href="!url">Manage fields</a> screen of the field collection.', array('!url' => url('admin/structure/field-collections/' . strtr($field['field_name'], array('_' => '-')) . '/fields'))),
    '#suffix' => '</p>',
  );
  return $element;
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
460
/**
461
 * Returns the base path to use for field collection items.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
462
 */
463
function field_collection_field_get_path($field) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
464
  if (empty($field['settings']['path'])) {
465
    return 'field-collection/' . strtr($field['field_name'], array('_' => '-'));
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
466
467
468
469
  }
  return $field['settings']['path'];
}

470
471
472
473
474
475
476
477
/**
 * Implements hook_field_settings_form().
 */
function field_collection_field_settings_form($field, $instance) {
  $form['hide_blank_items'] = array(
    '#type' => 'checkbox',
    '#title' => t('Hide blank items'),
    '#default_value' => $field['settings']['hide_blank_items'],
478
    '#description' => t('Ordinarily a new blank item will be added to unlimited cardinality fields whenever they appear in a form.  Checking this will prevent the blank item from appearing if the field already contains data.'),
479
480
    '#weight' => 10,
    '#states' => array(
481
482
483
      // Show the setting if the cardinality is -1.
      'visible' => array(
        ':input[name="field[cardinality]"]' => array('value' => '-1'),
484
485
486
      ),
    ),
  );
487
488
489
490
491
492
493
494
495
496
497
498
499
500
  $form['hide_initial_item'] = array(
    '#type' => 'checkbox',
    '#title' => t('Hide initial item'),
    '#default_value' => $field['settings']['hide_initial_item'],
    '#description' => t('Prevent the default blank item from appearing even if the field has no data yet.  If checked, the user must explicitly add the field collection.'),
    '#weight' => 11,
    '#states' => array(
      // Show the setting if the cardinality is -1 and hide_blank_items is checked.
      'visible' => array(
        ':input[name="field[cardinality]"]' => array('value' => '-1'),
        ':input[name="field[settings][hide_blank_items]"]' => array('checked' => TRUE),
      ),
    ),
  );
501
502
503
  return $form;
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
504
/**
505
506
507
508
 * Implements hook_field_insert().
 */
function field_collection_field_insert($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) {
  foreach ($items as &$item) {
509
510
511
512
513
514
515
516
517
518
519
520
    if ($entity = field_collection_field_get_entity($item)) {
      if (!empty($host_entity->is_new) && empty($entity->is_new)) {
        // If the host entity is new but we have a field_collection that is not
        // new, it means that its host is being cloned. Thus we need to clone
        // the field collection entity as well.
        $new_entity = clone $entity;
        $new_entity->item_id = NULL;
        $new_entity->revision_id = NULL;
        $new_entity->is_new = TRUE;
        $entity = $new_entity;
      }
      if (!empty($entity->is_new)) {
521
        $entity->setHostEntity($host_entity_type, $host_entity, field_collection_entity_language($host_entity_type, $host_entity), FALSE);
522
      }
523
524
525
526
527
      $entity->save(TRUE);
      $item = array(
        'value' => $entity->item_id,
        'revision_id' => $entity->revision_id,
      );
528
529
530
531
532
533
    }
  }
}

/**
 * Implements hook_field_update().
534
 *
535
 * Care about removed field collection items.
536
 *
537
538
 * Support saving field collection items in @code $item['entity'] @endcode. This
 * may be used to seamlessly create field collection items during host-entity
539
 * creation or to save changes to the host entity and its collections at once.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
540
 */
541
function field_collection_field_update($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) {
542
543
544
545
546
547
548
549
  // When entity language is changed field values are moved to the new language
  // and old values are marked as removed. We need to avoid processing them in
  // this case.
  $entity_langcode = field_collection_entity_language($host_entity_type, $host_entity);
  $original = isset($host_entity->original) ? $host_entity->original : $host_entity;
  $original_langcode = field_collection_entity_language($host_entity_type, $original);
  $langcode = $langcode == $original_langcode ? $entity_langcode : $langcode;

550
551
552
553
554
555
556
557
558
  $top_host = $host_entity;
  while (method_exists($top_host, 'hostEntity')) {
    $top_host = $top_host->hostEntity();
  }

  // Prevent workbench moderation from deleting field collections or paragraphs
  // on node_save() during workbench_moderation_store(), when
  // $host_entity->revision == 0.
  if (!empty($top_host->workbench_moderation['updating_live_revision'])) {
559
560
561
    return;
  }

562
563
  // Load items from the original entity.
  $items_original = !empty($original->{$field['field_name']}[$langcode]) ? $original->{$field['field_name']}[$langcode] : array();
564
565
  $original_by_id = array_flip(field_collection_field_item_to_ids($items_original));

566
  foreach ($items as $delta => &$item) {
567
568
569
570
571
572
    // In case the entity has been changed / created, save it and set the id.
    // If the host entity creates a new revision, save new item-revisions as
    // well.
    if (isset($item['entity']) || !empty($host_entity->revision)) {
      if ($entity = field_collection_field_get_entity($item)) {
        // If the host entity is saved as new revision, do the same for the item.
573
        if (!empty($host_entity->revision) || !empty($host_entity->is_new_revision)) {
574
          $entity->revision = TRUE;
575
576
577
578
579
          // Without this cache clear entity_revision_is_default will
          // incorrectly return false here when creating a new published revision
          if (!isset($cleared_host_entity_cache)) {
            list($entity_id) = entity_extract_ids($host_entity_type, $host_entity);
            entity_get_controller($host_entity_type)->resetCache(array($entity_id));
Ra Mänd's avatar
Ra Mänd committed
580
            $cleared_host_entity_cache = TRUE;
581
          }
582
583
584
585
586
587
588
          $is_default = entity_revision_is_default($host_entity_type, $host_entity);
          // If an entity type does not support saving non-default entities,
          // assume it will be saved as default.
          if (!isset($is_default) || $is_default) {
            $entity->default_revision = TRUE;
            $entity->archived = FALSE;
          }
589
590
591
          else {
            $entity->default_revision = FALSE;
          }
592
        }
593
594
595
596
597

        if (!empty($entity->is_new)) {
          $entity->setHostEntity($host_entity_type, $host_entity, $langcode, FALSE);
        }
        else {
598
          $entity->updateHostEntity($host_entity, $host_entity_type);
599
600
        }

601
602
603
604
605
606
        $entity->save(TRUE);

        $item = array(
          'value' => $entity->item_id,
          'revision_id' => $entity->revision_id,
        );
607
      }
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
608
    }
609

610
611
612
613
614
615
616
617
618
    unset($original_by_id[$item['value']]);
  }

  // If there are removed items, care about deleting the item entities.
  if ($original_by_id) {
    $ids = array_flip($original_by_id);

    // If we are creating a new revision, the old-items should be kept but get
    // marked as archived now.
619
    if (!empty($host_entity->revision)) {
620
621
622
623
624
625
      db_update('field_collection_item')
        ->fields(array('archived' => 1))
        ->condition('item_id', $ids, 'IN')
        ->execute();
    }
    else {
626
627
628
629
630
631
632
633
634
      // Load items from the original entity from all languages checking which
      // are the unused items.
      $current_items = array();
      $languages = language_list();
      foreach ($languages as $langcode_value) {
        $current_items += !empty($host_entity->{$field['field_name']}[$langcode_value->language]) ? $host_entity->{$field['field_name']}[$langcode_value->language] : array();
        $current_by_id = field_collection_field_item_to_ids($current_items);
      }
      $items_to_remove = array_diff($ids, $current_by_id);
635
      // Delete unused field collection items now.
636
      foreach (field_collection_item_load_multiple($items_to_remove) as $un_item) {
637
        $un_item->updateHostEntity($host_entity);
638
        $un_item->deleteRevision(TRUE);
639
640
641
642
643
      }
    }
  }
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
644
645
646
/**
 * Implements hook_field_delete().
 */
647
function field_collection_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
648
  $ids = field_collection_field_item_to_ids($items);
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
649
  // Also delete all embedded entities.
650
  if ($ids && field_info_field($field['field_name'])) {
651
652
653
    // We filter out entities that are still being referenced by other
    // host-entities. This should never be the case, but it might happened e.g.
    // when modules cloned a node without knowing about field-collection.
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
    $entity_info = entity_get_info($entity_type);
    $entity_id_name = $entity_info['entity keys']['id'];
    $field_column = key($field['columns']);

    foreach ($ids as $id_key => $id) {
      $query = new EntityFieldQuery();
      $entities = $query
        ->fieldCondition($field['field_name'], $field_column, $id)
        ->execute();
      unset($entities[$entity_type][$entity->$entity_id_name]);

      if (!empty($entities[$entity_type])) {
        // Filter this $id out.
        unset($ids[$id_key]);
      }
669
670
671
672
673
674
675
676
677
678
679
680
681

      // Set a flag to remember that the host entity is being deleted. See
      // FieldCollectionItemEntity::deleteHostEntityReference().
      // Doing this on $entity is not sufficient because the cache for $entity
      // may have been reset since it was loaded.  That would cause
      // hostEntity() to load it from the database later without the flag.
      $field_collection_item = field_collection_item_load($id);
      if ($field_collection_item) {
        $hostEntity = $field_collection_item->hostEntity();
        if (!empty($hostEntity)) {
          $hostEntity->field_collection_deleting = TRUE;
        }
      }
682
683
    }

684
685
    entity_delete_multiple('field_collection_item', $ids);
  }
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
686
687
}

688
689
690
691
692
693
694
695
696
697
698
699
700
/**
 * Implements hook_field_delete_revision().
 */
function field_collection_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $item) {
    if (!empty($item['revision_id'])) {
      if ($entity = field_collection_item_revision_load($item['revision_id'])) {
        $entity->deleteRevision(TRUE);
      }
    }
  }
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
701
/**
702
 * Get an array of field collection item IDs stored in the given field items.
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
703
 */
704
function field_collection_field_item_to_ids($items) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
705
706
707
708
709
710
711
712
713
714
715
716
  $ids = array();
  foreach ($items as $item) {
    if (!empty($item['value'])) {
      $ids[] = $item['value'];
    }
  }
  return $ids;
}

/**
 * Implements hook_field_is_empty().
 */
717
function field_collection_field_is_empty($item, $field) {
718
719
720
721
722
723
724
725
726
727
  if (!empty($item['value'])) {
    return FALSE;
  }
  elseif (isset($item['entity'])) {
    return field_collection_item_is_empty($item['entity']);
  }
  return TRUE;
}

/**
728
 * Determines whether a field collection item entity is empty based on the collection-fields.
729
730
731
 */
function field_collection_item_is_empty(FieldCollectionItemEntity $item) {
  $instances = field_info_instances('field_collection_item', $item->field_name);
732
  $is_empty = TRUE;
733

734
  // Check whether all fields are booleans.
735
  $all_boolean = $instances && !(bool) array_filter($instances, '_field_collection_field_is_not_boolean');
736

737
738
739
740
  foreach ($instances as $instance) {
    $field_name = $instance['field_name'];
    $field = field_info_field($field_name);

741
742
743
744
745
746
    // Boolean fields as those are always considered non-empty, thus their
    // information is not useful and can be skipped by default.
    if (!$all_boolean && $field['type'] == 'list_boolean') {
      continue;
    }

747
748
749
750
751
752
    // Determine the list of languages to iterate on.
    $languages = field_available_languages('field_collection_item', $field);

    foreach ($languages as $langcode) {
      if (!empty($item->{$field_name}[$langcode])) {
        // If at least one collection-field is not empty; the
753
        // field collection item is not empty.
754
755
        foreach ($item->{$field_name}[$langcode] as $field_item) {
          if (!module_invoke($field['module'], 'field_is_empty', $field_item, $field)) {
756
            $is_empty = FALSE;
757
758
759
760
761
          }
        }
      }
    }
  }
762
763
764
765

  // Allow other modules a chance to alter the value before returning.
  drupal_alter('field_collection_is_empty', $is_empty, $item);
  return $is_empty;
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
766
767
}

768
769
770
771
772
773
774
775
/**
 * Callback used by array_filter in field_collection_is_empty.
 */
function _field_collection_field_is_not_boolean($instance) {
  $field = field_info_field($instance['field_name']);
  return $field['type'] != 'list_boolean';
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
776
777
778
/**
 * Implements hook_field_formatter_info().
 */
779
function field_collection_field_formatter_info() {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
780
  return array(
781
    'field_collection_list' => array(
782
      'label' => t('Links to field collection items'),
783
      'field types' => array('field_collection'),
Ra Mänd's avatar
Ra Mänd committed
784
      'settings' => array(
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
785
        'edit' => t('Edit'),
786
        'translate' => t('Translate'),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
787
788
        'delete' => t('Delete'),
        'add' => t('Add'),
789
        'description' => TRUE,
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
790
791
      ),
    ),
792
    'field_collection_view' => array(
793
      'label' => t('Field collection items'),
794
      'field types' => array('field_collection'),
Ra Mänd's avatar
Ra Mänd committed
795
      'settings' => array(
796
        'edit' => t('Edit'),
797
        'translate' => t('Translate'),
798
799
        'delete' => t('Delete'),
        'add' => t('Add'),
800
        'description' => TRUE,
801
        'view_mode' => 'full',
802
803
      ),
    ),
804
805
806
    'field_collection_fields' => array(
      'label' => t('Fields only'),
      'field types' => array('field_collection'),
Ra Mänd's avatar
Ra Mänd committed
807
      'settings' => array(
808
809
810
        'view_mode' => 'full',
      ),
    ),
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
811
812
813
814
815
816
  );
}

/**
 * Implements hook_field_formatter_settings_form().
 */
817
function field_collection_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
818
819
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
820
821
  $elements = array();

822
  if ($display['type'] != 'field_collection_fields') {
823
824
825
826
827
828
    $elements['add'] = array(
      '#type' => 'textfield',
      '#title' => t('Add link title'),
      '#default_value' => $settings['add'],
      '#description' => t('Leave the title empty, to hide the link.'),
    );
829
830
831
832
833
834
    $elements['edit'] = array(
      '#type' => 'textfield',
      '#title' => t('Edit link title'),
      '#default_value' => $settings['edit'],
      '#description' => t('Leave the title empty, to hide the link.'),
    );
835
    $elements['translate'] = array(
836
      '#type' => 'textfield',
837
      '#title' => t('Translate link title'),
838
      '#default_value' => isset($settings['translate']) ? $settings['translate'] : '',
839
      '#description' => t('Leave the title empty, to hide the link.'),
840
      '#access' => field_collection_item_is_translatable(),
841
    );
842
    $elements['delete'] = array(
843
      '#type' => 'textfield',
844
845
      '#title' => t('Delete link title'),
      '#default_value' => $settings['delete'],
846
847
848
849
850
851
852
853
854
      '#description' => t('Leave the title empty, to hide the link.'),
    );
    $elements['description'] = array(
      '#type' => 'checkbox',
      '#title' => t('Show the field description beside the add link.'),
      '#default_value' => $settings['description'],
      '#description' => t('If enabled and the add link is shown, the field description is shown in front of the add link.'),
    );
  }
855
856

  // Add a select form element for view_mode if viewing the rendered field_collection.
857
  if ($display['type'] !== 'field_collection_list') {
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873

    $entity_type = entity_get_info('field_collection_item');
    $options = array();
    foreach ($entity_type['view modes'] as $mode => $info) {
      $options[$mode] = $info['label'];
    }

    $elements['view_mode'] = array(
      '#type' => 'select',
      '#title' => t('View mode'),
      '#options' => $options,
      '#default_value' => $settings['view_mode'],
      '#description' => t('Select the view mode'),
    );
  }

874
  return $elements;
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
875
876
877
878
879
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
880
function field_collection_field_formatter_settings_summary($field, $instance, $view_mode) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
881
882
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
883
  $output = array();
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
884

885
  if ($display['type'] !== 'field_collection_fields') {
886
    $links = field_collection_get_operations($settings, TRUE);
887
888
889
890
891
892
    if ($links) {
      $output[] = t('Links: @links', array('@links' => check_plain(implode(', ', $links))));
    }
    else {
      $output[] = t('Links: none');
    }
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
893
  }
894

895
896
897
  if ($display['type'] !== 'field_collection_list') {
    $entity_type = entity_get_info('field_collection_item');
    if (!empty($entity_type['view modes'][$settings['view_mode']]['label'])) {
Ra Mänd's avatar
Ra Mänd committed
898
      $output[] = t('View mode: @mode', array('@mode' => $entity_type['view modes'][$settings['view_mode']]['label']));
899
    }
900
901
  }

902
  return implode('<br>', $output);
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
903
904
}

905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
function field_collection_entity_preload($entities, $langcode, &$items, $fields) {
  static $local_entity_cache = array();

  $fc_ids = array();

  // Collect every possible term attached to any of the fieldable entities.
  foreach ($entities as $id => $entity) {
    foreach ($items[$id] as $delta => $item) {

      // Check if this item is in our local entity cache
      if (isset($local_entity_cache[$item['value']])) {
        $items[$id][$delta]['field_collection'] = $local_entity_cache[$item['value']];
        continue;
      }

      // Force the array key to prevent duplicates.
      $fc_ids[$item['value']] = $item['value'];
    }
  }

  if ($fc_ids) {
    $new_entities = array();
    $terms = field_collection_item_load_multiple($fc_ids);

    // Iterate through the fieldable entities again to attach the loaded
    // field collection data.
    foreach ($entities as $id => $entity) {
      $rekey = FALSE;

      foreach ($items[$id] as $delta => $item) {
        // Check whether the field collection field instance value could be loaded.
        if (isset($terms[$item['value']])) {
          // Replace the instance value with the term data.
          $e = $terms[$item['value']];
          $local_entity_cache[$item['value']] = $e;
          $items[$id][$delta]['field_collection'] = $e;

	  foreach ($fields as $field_name => $field) {
            if (isset($e->$field_name)) {
              $field_data = $e->$field_name;
              if (isset($field_data[$langcode])) {
                $new_entities[$e->item_id] = $e->item_id;
                $items[$e->item_id] = $field_data[$langcode];
              }
            }
          }
        }
        // Otherwise, unset the instance value, since the field colletion entity does not exist.
        else {
          unset($items[$id][$delta]);
          $rekey = TRUE;
        }
      }

      if ($rekey) {
        // Rekey the items array.
        $items[$id] = array_values($items[$id]);
      }
    }

    field_collection_entity_preload($new_entities, $langcode, $items, $fields);
  }
}

function field_collection_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  $fields = field_read_fields(array('type' => 'field_collection'));
  field_collection_entity_preload($entities, $langcode, $items, $fields);
}

Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
974
975
976
/**
 * Implements hook_field_formatter_view().
 */
977
function field_collection_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
978
979
980
981
  $element = array();
  $settings = $display['settings'];

  switch ($display['type']) {
982
    case 'field_collection_list':
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
983
984

      foreach ($items as $delta => $item) {
985
        if ($field_collection = field_collection_field_get_entity($item)) {
986
          $output = l($field_collection->label(), $field_collection->path());
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
987
          $links = array();
988
          foreach (field_collection_get_operations($settings) as $op => $label) {
989
            if ($settings[$op] && entity_access($op == 'edit' ? 'update' : $op, 'field_collection_item', $field_collection)) {
990
991
              $title = entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_$op", $settings[$op]);
              $links[] = l($title, $field_collection->path() . '/' . $op, array('query' => drupal_get_destination()));
Wolfgang Ziegler's avatar
Wolfgang Ziegler committed
992
993
994
995
996
997
998
999
            }
          }
          if ($links) {
            $output .= ' (' . implode('|', $links) . ')';
          }
          $element[$delta] = array('#markup' => $output);
        }
      }
1000
      field_collection_field_formatter_links($element, $entity_type, $entity, $field, $instance, $langcode, $items, $display);