field_collection.entity.inc 22.8 KB
Newer Older
1 2 3 4 5 6 7 8
<?php

/**
 * Class for field_collection_item entities.
 */
class FieldCollectionItemEntity extends Entity {

  /**
9
   * Field Collection field info.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
   *
   * @var array
   */
  protected $fieldInfo;

  /**
   * The host entity object.
   *
   * @var object
   */
  protected $hostEntity;

  /**
   * The host entity ID.
   *
   * @var integer
   */
  protected $hostEntityId;

  /**
   * The host entity revision ID if this is not the default revision.
   *
   * @var integer
   */
  protected $hostEntityRevisionId;

  /**
   * The host entity type.
   *
   * @var string
   */
  protected $hostEntityType;

  /**
   * The language under which the field collection item is stored.
   *
   * @var string
   */
  protected $langcode = LANGUAGE_NONE;

  /**
   * Entity ID.
   *
   * @var integer
   */
  public $item_id;

  /**
58
   * Field Collection revision ID.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
   *
   * @var integer
   */
  public $revision_id;

  /**
   * The name of the field-collection field this item is associated with.
   *
   * @var string
   */
  public $field_name;

  /**
   * Whether this revision is the default revision.
   *
   * @var bool
   */
  public $default_revision = TRUE;

  /**
   * Whether the field collection item is archived, i.e. not in use.
   *
   * @see FieldCollectionItemEntity::isInUse()
   * @var bool
   */
  public $archived = FALSE;

  /**
   * Constructs the entity object.
   */
  public function __construct(array $values = array(), $entityType = NULL) {
    parent::__construct($values, 'field_collection_item');
    // Workaround issues http://drupal.org/node/1084268 and
    // http://drupal.org/node/1264440:
    // Check if the required property is set before checking for the field's
    // type. If the property is not set, we are hitting a PDO or a core's bug.
    // FIXME: Remove when #1264440 is fixed and the required PHP version is
    //  properly identified and documented in the module documentation.
    if (isset($this->field_name)) {
      // Ok, we have the field name property, we can proceed and check the field's type
      $field_info = $this->fieldInfo();
      if (!$field_info || $field_info['type'] != 'field_collection') {
        throw new Exception("Invalid field name given: {$this->field_name} is not a Field Collection field.");
      }
    }
  }

  /**
   * Provides info about the field on the host entity, which embeds this
   * field collection item.
   */
  public function fieldInfo() {
    return field_info_field($this->field_name);
  }

  /**
   * Provides info of the field instance containing the reference to this
   * field collection item.
   */
  public function instanceInfo() {
    if ($this->fetchHostDetails()) {
      return field_info_instance($this->hostEntityType(), $this->field_name, $this->hostEntityBundle());
    }
  }

  /**
   * Returns the field instance label translated to interface language.
   */
  public function translatedInstanceLabel($langcode = NULL) {
    if ($info = $this->instanceInfo()) {
      if (module_exists('i18n_field')) {
130
        return i18n_string("field:{$this->field_name}:{$info['bundle']}:label", $info['label'], array('langcode' => $langcode, 'sanitize' => FALSE));
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
      }
      return $info['label'];
    }
  }

  /**
   * Specifies the default label, which is picked up by label() by default.
   */
  public function defaultLabel() {
    if ($this->fetchHostDetails()) {
      $field = $this->fieldInfo();
      $label = $this->translatedInstanceLabel();
      $host  = $this->hostEntity();

      if ($new_label = module_invoke_all('field_collection_item_label', $this, $host, $field, $label)) {
        return array_pop($new_label);
      }
Ra Mänd's avatar
Ra Mänd committed
148 149

      if ($field['cardinality'] == 1) {
150 151
        return $label;
      }
Ra Mänd's avatar
Ra Mänd committed
152 153

      if ($this->item_id) {
154 155
        return t('!instance_label @count', array('!instance_label' => $label, '@count' => $this->delta() + 1));
      }
Ra Mänd's avatar
Ra Mänd committed
156 157

      return t('New !instance_label', array('!instance_label' => $label));
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
    }
    return t('Unconnected field collection item');
  }

  /**
   * Returns the path used to view the entity.
   */
  public function path() {
    if ($this->item_id) {
      return field_collection_field_get_path($this->fieldInfo()) . '/' . $this->item_id;
    }
  }

  /**
   * Returns the URI as returned by entity_uri().
   */
  public function defaultUri() {
    return array(
      'path' => $this->path(),
    );
  }

  /**
   * Sets the host entity. Only possible during creation of a item.
   *
   * @param $create_link
   *   (optional) Whether a field-item linking the host entity to the field
   *   collection item should be created.
   */
  public function setHostEntity($entity_type, $entity, $langcode = LANGUAGE_NONE, $create_link = TRUE) {
    if (!empty($this->is_new)) {
      $this->hostEntityType = $entity_type;
      $this->hostEntity = $entity;
      $this->langcode = $langcode;

      list($this->hostEntityId, $this->hostEntityRevisionId) = entity_extract_ids($this->hostEntityType, $this->hostEntity);
      // If the host entity is not saved yet, set the id to FALSE. So
      // fetchHostDetails() does not try to load the host entity details.
196
      // Checking value of $this->hostEntityId.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
      if (!isset($this->hostEntityId)) {
        $this->hostEntityId = FALSE;
      }
      // We are create a new field collection for a non-default entity, thus
      // set archived to TRUE.
      if (!entity_revision_is_default($entity_type, $entity)) {
        $this->hostEntityId = FALSE;
        $this->archived = TRUE;
      }
      if ($create_link) {
        $entity->{$this->field_name}[$this->langcode][] = array('entity' => $this);
      }
    }
    else {
      throw new Exception('The host entity may be set only during creation of a field collection item.');
    }
  }

  /**
   * Updates the wrapped host entity object.
   *
218 219
   * @param object $entity
   *   Host entity.
220 221
   * @param string $host_entity_type
   *   The entity type of the entity the field collection is attached to.
222
   */
223
  public function updateHostEntity($entity, $host_entity_type = NULL) {
224
    $this->fetchHostDetails($entity);
225 226 227 228
    // If it isn't possible to retrieve hostEntityType due to the fact that it's
    // not saved in the DB yet then fill in info about the hostEntity manually.
    // This happens when creating a new revision of a field collection entity
    // and it needs to relate to the new revision of the host entity.
229
    if (!$this->hostEntityType || isset($entity->tid)) {
230 231 232 233
      $this->hostEntityType = $host_entity_type;
      $this->hostEntity = $entity;
      list($this->hostEntityId, $this->hostEntityRevisionId) = entity_extract_ids($this->hostEntityType, $this->hostEntity);
    }
234 235
    list($recieved_id) = entity_extract_ids($this->hostEntityType, $entity);

236
    if (!empty($this->hostEntityId) && $this->isInUse()) {
237
      if (is_array($this->hostEntityId)) {
238 239 240
        $current_id = in_array($recieved_id, $this->hostEntityId)
          ? $recieved_id
          : FALSE;
241 242 243 244
      }
      else {
        $current_id = $this->hostEntityId;
      }
245 246 247
    }
    else {
      $current_host = entity_revision_load($this->hostEntityType, $this->hostEntityRevisionId);
248
      list($current_id) = $current_host ? entity_extract_ids($this->hostEntityType, $current_host) : array($recieved_id);
249 250 251 252 253
    }

    if ($current_id == $recieved_id) {
      $this->hostEntity = $entity;
      $delta = $this->delta();
254 255
      if (isset($entity->{$this->field_name}[$this->langcode()][$delta]['entity'])) {
        $entity->{$this->field_name}[$this->langcode()][$delta]['entity'] = $this;
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
      }
    }
    else {
      throw new Exception('The host entity cannot be changed.');
    }
  }

  /**
   * Returns the host entity, which embeds this field collection item.
   */
  public function hostEntity() {
    if ($this->fetchHostDetails()) {
      if (!isset($this->hostEntity) && $this->isInUse()) {
        $this->hostEntity = entity_load_single($this->hostEntityType, $this->hostEntityId);
      }
      elseif (!isset($this->hostEntity) && $this->hostEntityRevisionId) {
        $this->hostEntity = entity_revision_load($this->hostEntityType, $this->hostEntityRevisionId);
      }
      return $this->hostEntity;
    }
  }

  /**
   * Returns the entity type of the host entity, which embeds this
   * field collection item.
   */
  public function hostEntityType() {
    if ($this->fetchHostDetails()) {
      return $this->hostEntityType;
    }
  }

  /**
   * Returns the id of the host entity, which embeds this field collection item.
   */
  public function hostEntityId() {
    if ($this->fetchHostDetails()) {
      if (!$this->hostEntityId && $this->hostEntityRevisionId) {
        $this->hostEntityId = entity_id($this->hostEntityType, $this->hostEntity());
      }
      return $this->hostEntityId;
    }
  }

  /**
   * Returns the bundle of the host entity, which embeds this field collection
   * item.
   */
  public function hostEntityBundle() {
    if ($entity = $this->hostEntity()) {
      list($id, $rev_id, $bundle) = entity_extract_ids($this->hostEntityType, $entity);
      return $bundle;
    }
  }

311 312 313 314 315 316 317
  /**
   * Collects info about the field collection's host.
   *
   * @param $hostEntity
   *   The host entity object. (optional)
   */
  protected function fetchHostDetails($hostEntity = NULL) {
318
    if (!isset($this->hostEntityId) || (!$this->hostEntityId && $this->hostEntityRevisionId)) {
319 320 321 322
      if ($this->item_id) {
        // For saved field collections, query the field data to determine the
        // right host entity.
        $query = new EntityFieldQuery();
323 324 325 326 327
        $field_info = $this->fieldInfo();
        $query->fieldCondition($field_info, 'revision_id', $this->revision_id);
        if ($hostEntity) {
          $entity_type = key($field_info['bundles']);
          $bundle = current($field_info['bundles'][$entity_type]);
328 329
          $entity_info = entity_get_info($entity_type);
          $key = $entity_info['entity keys']['id'];
330
          $query->entityCondition('entity_type', $entity_type);
331
          $query->entityCondition('entity_id', $hostEntity->{$key});
332
          $query->entityCondition('bundle', $bundle);
333 334 335 336
          // Only filter by language if this entity type has a language key that
          // has a corresponding field in its base table.
          if (!empty($entity_info['entity keys']['language']) && !empty($entity_info['schema_fields_sql']['base table']) && in_array($entity_info['entity keys']['language'], $entity_info['schema_fields_sql']['base table'], TRUE)) {
            $query->propertyCondition($entity_info['entity keys']['language'], $hostEntity->{$entity_info['entity keys']['language']});
337
          }
338
        }
339
        $query->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');
340 341 342 343
        if (!$this->isInUse()) {
          $query->age(FIELD_LOAD_REVISION);
        }
        $result = $query->execute();
344
        if ($result) {
345 346
          $this->hostEntityType = key($result);
          $data = current($result);
347 348 349

          if ($this->isInUse()) {
            $data_array_keys = array_keys($data);
350
            $this->hostEntityId = $data ? end($data_array_keys) : FALSE;
351 352 353 354 355 356 357 358
            $this->hostEntityRevisionId = FALSE;
          }
          // If we are querying for revisions, we get the revision ID.
          else {
            $data_array_keys = array_keys($data);
            $this->hostEntityId = FALSE;
            $this->hostEntityRevisionId = $data ? end($data_array_keys) : FALSE;
          }
359 360
        }
        else {
361
          // No host entity available yet.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
          $this->hostEntityId = FALSE;
        }
      }
      else {
        // No host entity available yet.
        $this->hostEntityId = FALSE;
      }
    }
    return !empty($this->hostEntityId) || !empty($this->hostEntity) || !empty($this->hostEntityRevisionId);
  }

  /**
   * Determines the $delta of the reference pointing to this field collection
   * item.
   */
  public function delta() {
    if (($entity = $this->hostEntity()) && isset($entity->{$this->field_name})) {
      foreach ($entity->{$this->field_name} as $langcode => &$data) {
380 381 382 383 384 385
        if (!empty($data)) {
          foreach ($data as $delta => $item) {
            if (isset($item['value']) && $item['value'] == $this->item_id) {
              $this->langcode = $langcode;
              return $delta;
            }
Ra Mänd's avatar
Ra Mänd committed
386 387

            if (isset($item['entity']) && $item['entity'] === $this) {
388 389 390
              $this->langcode = $langcode;
              return $delta;
            }
391
          }
392 393 394 395 396 397 398 399 400 401 402 403
        }
      }
      // If we don't find the delta in the current values (cause the item
      // is being deleted, for example), we search the delta in the originalcontent.
      if (!empty($entity->original)) {
        foreach ($entity->original->{$this->field_name} as $langcode => &$data) {
          if (!empty($data)) {
            foreach ($data as $delta => $item) {
              if (isset($item['value']) && $item['value'] == $this->item_id) {
                $this->langcode = $langcode;
                return $delta;
              }
Ra Mänd's avatar
Ra Mänd committed
404 405

              if (isset($item['entity']) && $item['entity'] === $this) {
406 407 408 409
                $this->langcode = $langcode;
                return $delta;
              }
            }
410 411 412 413 414 415 416 417 418 419
          }
        }
      }
    }
  }

  /**
   * Determines the language code under which the item is stored.
   */
  public function langcode() {
420
    if (empty($this->langcode) || $this->delta() === NULL) {
421 422 423 424 425
      $this->langcode = field_collection_entity_language('field_collection_item', $this);
    }

    if (empty($this->langcode) || ($this->langcode != LANGUAGE_NONE && (!module_exists('entity_translation') || !entity_translation_enabled('field_collection_item')))) {
      $this->langcode = LANGUAGE_NONE;
426
    }
427 428

    return $this->langcode;
429 430 431 432 433
  }

  /**
   * Determines whether this field collection item revision is in use.
   *
434
   * Field Collection items may be contained in from non-default host entity
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 460 461 462 463 464 465 466
   * revisions. If the field collection item does not appear in the default
   * host entity revision, the item is actually not used by default and so
   * marked as 'archived'.
   * If the field collection item appears in the default revision of the host
   * entity, the default revision of the field collection item is in use there
   * and the collection is not marked as archived.
   */
  public function isInUse() {
    return $this->default_revision && !$this->archived;
  }

  /**
   * Save the field collection item.
   *
   * By default, always save the host entity, so modules are able to react
   * upon changes to the content of the host and any 'last updated' dates of
   * entities get updated.
   *
   * For creating an item a host entity has to be specified via setHostEntity()
   * before this function is invoked. For the link between the entities to be
   * fully established, the host entity object has to be updated to include a
   * reference on this field collection item during saving. So do not skip
   * saving the host for creating items.
   *
   * @param $skip_host_save
   *   (internal) If TRUE is passed, the host entity is not saved automatically
   *   and therefore no link is created between the host and the item or
   *   revision updates might be skipped. Use with care.
   */
  public function save($skip_host_save = FALSE) {
    // Make sure we have a host entity during creation.
    if (!empty($this->is_new) && !(isset($this->hostEntityId) || isset($this->hostEntity) || isset($this->hostEntityRevisionId))) {
Ra Mänd's avatar
Ra Mänd committed
467
      throw new Exception('Unable to create a field collection item without a given host entity.');
468 469
    }

470
    // Copy the values of translatable fields for a new field collection item.
471
    if (!empty($this->is_new) && field_collection_item_is_translatable() && $this->langcode() == LANGUAGE_NONE) {
472 473 474
      $this->copyTranslations();
    }

475 476 477 478 479 480
    // Only save directly if we are told to skip saving the host entity. Else,
    // we always save via the host as saving the host might trigger saving
    // field collection items anyway (e.g. if a new revision is created).
    if ($skip_host_save) {
      return entity_get_controller($this->entityType)->save($this);
    }
481

Ra Mänd's avatar
Ra Mänd committed
482 483 484 485 486 487 488 489 490 491
    $host_entity = $this->hostEntity();
    if (!$host_entity) {
      throw new Exception('Unable to save a field collection item without a valid reference to a host entity.');
    }
    // If this is creating a new revision, also do so for the host entity.
    if (!empty($this->revision) || !empty($this->is_new_revision)) {
      $host_entity->revision = TRUE;
      if (!empty($this->default_revision)) {
        entity_revision_set_default($this->hostEntityType, $host_entity);
      }
492
    }
Ra Mänd's avatar
Ra Mänd committed
493 494 495 496 497 498 499 500 501 502 503
    // Set the host entity reference, so the item will be saved with the host.
    // @see field_collection_field_presave()
    $delta = $this->delta();
    if (isset($delta)) {
      $host_entity->{$this->field_name}[$this->langcode()][$delta] = array('entity' => $this);
    }
    else {
      $host_entity->{$this->field_name}[$this->langcode()][] = array('entity' => $this);
    }

    return entity_save($this->hostEntityType, $host_entity);
504 505 506 507 508
  }

  /**
   * Deletes the field collection item and the reference in the host entity.
   */
509
  public function delete($skip_host_save = FALSE) {
510
    parent::delete();
511 512 513
    if (!$skip_host_save) {
      $this->deleteHostEntityReference();
    }
514 515
  }

516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
  /**
   * Copies text to all languages the collection item has a translation for.
   *
   * @param $source_language
   *   Language code to copy the text from.
   */
  public function copyTranslations($source_language = NULL) {
    // Get a handler for Entity Translation if there is one.
    $host_et_handler = NULL;
    if (module_exists('entity_translation')) {
      $host_et_handler = entity_translation_get_handler($this->hostEntityType(), $this->hostEntity());
    }
    if (is_null($host_et_handler)) {
      return;
    }

    $host_languages = array_keys($host_et_handler->getTranslations()->data);
    if (empty($host_languages)) {
      $host_languages = array(entity_language($this->hostEntityType(), $this->hostEntity()));
    }
    $source_language = isset($source_language) ? $source_language : $host_et_handler->getLanguage();
    $target_languages = array_diff($host_languages, array($source_language));
    $fields_instances = array_keys(field_info_instances('field_collection_item', $this->field_name));
    $fields = field_info_fields();

    foreach ($fields_instances as $translatable_field) {
      if ($fields[$translatable_field]['translatable'] == 1) {
        foreach ($target_languages as $langcode) {
Ra Mänd's avatar
Ra Mänd committed
544 545 546 547 548 549 550
          // Source (translatable_field) is set, therefore continue
          // processing.
          if (isset($this->{$translatable_field}[$source_language])
            && !isset($this->{$translatable_field}[$langcode])) {
            // Destination (translatable_field) is not set, therefore safe to
            // copy the translation.
            $this->{$translatable_field}[$langcode] = $this->{$translatable_field}[$source_language];
551 552 553 554 555 556 557 558 559
          }
        }
        if ($source_language == LANGUAGE_NONE && count($this->{$translatable_field}) > 1) {
          $this->{$translatable_field}[$source_language] = NULL;
        }
      }
    }
  }

560 561 562 563 564 565
  /**
   * Deletes the host entity's reference of the field collection item.
   */
  protected function deleteHostEntityReference() {
    $delta = $this->delta();
    if ($this->item_id && isset($delta)) {
566
      unset($this->hostEntity->{$this->field_name}[$this->langcode()][$delta]);
567 568 569
      // Do not save when the host entity is being deleted. See
      // field_collection_field_delete().
      if (empty($this->hostEntity->field_collection_deleting)) {
570
        entity_save($this->hostEntityType(), $this->hostEntity());
571
      }
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
    }
  }

  /**
   * Intelligently delete a field collection item revision.
   *
   * If a host entity is revisioned with its field collection items, deleting
   * a field collection item on the default revision of the host should not
   * delete the collection item from archived revisions too. Instead, we delete
   * the current default revision and archive the field collection.
   */
  public function deleteRevision($skip_host_update = FALSE) {
    if (!$this->revision_id) {
      return;
    }

    if (!$skip_host_update) {
      // Just remove the item from the host, which cares about deleting the
      // item (depending on whether the update creates a new revision).
      $this->deleteHostEntityReference();
    }

    if (!$this->isDefaultRevision()) {
      entity_revision_delete('field_collection_item', $this->revision_id);
    }
    // If deleting the default revision, take care!
    else {
      $row = db_select('field_collection_item_revision', 'r')
        ->fields('r')
        ->condition('item_id', $this->item_id)
        ->condition('revision_id', $this->revision_id, '<>')
        ->execute()
        ->fetchAssoc();
605 606

      if ($row) {
607 608 609 610 611
        // Make the other revision the default revision and archive the item.
        db_update('field_collection_item')
          ->fields(array('archived' => 1, 'revision_id' => $row['revision_id']))
          ->condition('item_id', $this->item_id)
          ->execute();
612 613 614 615

        // Let other modules know about the archived item.
        entity_get_controller('field_collection_item')->invoke('archive', $this);

616 617 618
        entity_get_controller('field_collection_item')->resetCache(array($this->item_id));
        entity_revision_delete('field_collection_item', $this->revision_id);
      }
619
      else {
620
        // Delete if there is no existing revision or translation to be saved.
621
        $this->delete($skip_host_update);
622
      }
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
    }
  }

  /**
   * Export the field collection item.
   *
   * Since field collection entities are not directly exportable (i.e., do not
   * have 'exportable' set to TRUE in hook_entity_info()) and since Features
   * calls this method when exporting the field collection as a field attached
   * to another entity, we return the export in the format expected by
   * Features, rather than in the normal Entity::export() format.
   */
  public function export($prefix = '') {
    // Based on code in EntityDefaultFeaturesController::export_render().
    $export = "entity_import('" . $this->entityType() . "', '";
    $export .= addcslashes(parent::export(), '\\\'');
    $export .= "')";
    return $export;
  }

643 644 645 646 647 648 649 650 651
  /**
   * Generate an array for rendering the field collection item.
   */
  public function view($view_mode = 'full', $langcode = NULL, $page = NULL) {
    // Allow modules to change the view mode.
    $view_mode = key(entity_view_mode_prepare($this->entityType, array($this->item_id => $this), $view_mode, $langcode));
    return parent::view($view_mode, $langcode, $page);
  }

652 653 654 655 656
  /**
   * Magic method to only serialize what's necessary.
   */
  public function __sleep() {
    $vars = get_object_vars($this);
Ra Mänd's avatar
Ra Mänd committed
657
    unset($vars['entityInfo'], $vars['idKey'], $vars['nameKey'], $vars['statusKey'], $vars['fieldInfo']);
658 659 660 661 662 663 664 665 666 667 668
    // Also do not serialize the host entity, but only if it has already an id.
    if ($this->hostEntity && ($this->hostEntityId || $this->hostEntityRevisionId)) {
      unset($vars['hostEntity']);
    }

    // Also key the returned array with the variable names so the method may
    // be easily overridden and customized.
    return drupal_map_assoc(array_keys($vars));
  }

}