EntityReferenceItem.php 12.3 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem.
6 7
 */

8
namespace Drupal\Core\Field\Plugin\Field\FieldType;
9

10
use Drupal\Core\Entity\EntityInterface;
11
use Drupal\Core\Entity\EntityTypeInterface;
12
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
13
use Drupal\Core\Field\FieldDefinitionInterface;
14
use Drupal\Core\Field\FieldItemBase;
15
use Drupal\Core\Field\FieldStorageDefinitionInterface;
16
use Drupal\Core\StringTranslation\TranslatableMarkup;
17
use Drupal\Core\TypedData\DataReferenceDefinition;
18
use Drupal\Core\TypedData\DataReferenceTargetDefinition;
19 20

/**
21
 * Defines the 'entity_reference' entity field type.
22
 *
23 24 25 26 27
 * Supported settings (below the definition's 'settings' key) are:
 * - target_type: The entity type to reference. Required.
 * - target_bundle: (optional): If set, restricts the entity bundles which may
 *   may be referenced. May be set to an single bundle, or to an array of
 *   allowed bundles.
28
 *
29 30 31
 * @FieldType(
 *   id = "entity_reference",
 *   label = @Translation("Entity reference"),
32
 *   description = @Translation("An entity field containing an entity reference."),
33
 *   category = @Translation("Reference"),
34
 *   no_ui = TRUE,
35
 *   default_widget = "entity_reference_autocomplete",
36
 *   default_formatter = "entity_reference_label",
37
 *   list_class = "\Drupal\Core\Field\EntityReferenceFieldItemList",
38
 *   constraints = {"ValidReference" = {}}
39
 * )
40 41 42
 */
class EntityReferenceItem extends FieldItemBase {

43 44 45
  /**
   * {@inheritdoc}
   */
46
  public static function defaultStorageSettings() {
47 48
    return array(
      'target_type' => \Drupal::moduleHandler()->moduleExists('node') ? 'node' : 'user',
49
    ) + parent::defaultStorageSettings();
50 51 52 53 54
  }

  /**
   * {@inheritdoc}
   */
55
  public static function defaultFieldSettings() {
56
    return array(
57 58
      'handler' => 'default:' . (\Drupal::moduleHandler()->moduleExists('node') ? 'node' : 'user'),
      'handler_settings' => array(),
59
    ) + parent::defaultFieldSettings();
60 61
  }

62
  /**
63
   * {@inheritdoc}
64
   */
65
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
66 67
    $settings = $field_definition->getSettings();
    $target_type_info = \Drupal::entityManager()->getDefinition($settings['target_type']);
68

69
    $target_id_data_type = 'string';
70
    if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
71 72 73 74 75 76 77
      $id_definition = \Drupal::entityManager()->getBaseFieldDefinitions($settings['target_type'])[$target_type_info->getKey('id')];
      if ($id_definition->getType() === 'integer') {
        $target_id_data_type = 'integer';
      }
    }

    if ($target_id_data_type === 'integer') {
78
      $target_id_definition = DataReferenceTargetDefinition::create('integer')
79
        ->setLabel(new TranslatableMarkup('@label ID', ['@label' => $target_type_info->getLabel()]))
80
        ->setSetting('unsigned', TRUE);
81
    }
82
    else {
83
      $target_id_definition = DataReferenceTargetDefinition::create('string')
84
        ->setLabel(new TranslatableMarkup('@label ID', ['@label' => $target_type_info->getLabel()]));
85
    }
86
    $target_id_definition->setRequired(TRUE);
87
    $properties['target_id'] = $target_id_definition;
88

89
    $properties['entity'] = DataReferenceDefinition::create('entity')
90
      ->setLabel($target_type_info->getLabel())
91
      ->setDescription(new TranslatableMarkup('The referenced entity'))
92 93 94
      // The entity object is computed out of the entity ID.
      ->setComputed(TRUE)
      ->setReadOnly(FALSE)
95
      ->setTargetDefinition(EntityDataDefinition::create($settings['target_type']))
96 97 98
      // We can add a constraint for the target entity type. The list of
      // referenceable bundles is a field setting, so the corresponding
      // constraint is added dynamically in ::getConstraints().
99
      ->addConstraint('EntityType', $settings['target_type']);
100 101 102 103 104 105 106 107 108

    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public static function mainPropertyName() {
    return 'target_id';
109 110
  }

111 112 113
  /**
   * {@inheritdoc}
   */
114
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
115 116
    $target_type = $field_definition->getSetting('target_type');
    $target_type_info = \Drupal::entityManager()->getDefinition($target_type);
117 118
    $properties = static::propertyDefinitions($field_definition)['target_id'];
    if ($target_type_info->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface') && $properties->getDataType() === 'integer') {
119 120 121 122 123 124 125 126 127 128 129 130
      $columns = array(
        'target_id' => array(
          'description' => 'The ID of the target entity.',
          'type' => 'int',
          'unsigned' => TRUE,
        ),
      );
    }
    else {
      $columns = array(
        'target_id' => array(
          'description' => 'The ID of the target entity.',
131
          'type' => 'varchar_ascii',
132 133 134
          // If the target entities act as bundles for another entity type,
          // their IDs should not exceed the maximum length for bundles.
          'length' => $target_type_info->getBundleOf() ? EntityTypeInterface::BUNDLE_MAX_LENGTH : 255,
135 136 137 138 139 140 141 142 143 144 145 146 147 148
        ),
      );
    }

    $schema = array(
      'columns' => $columns,
      'indexes' => array(
        'target_id' => array('target_id'),
      ),
    );

    return $schema;
  }

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  /**
   * {@inheritdoc}
   */
  public function getConstraints() {
    $constraints = parent::getConstraints();
    list($current_handler) = explode(':', $this->getSetting('handler'), 2);
    if ($current_handler === 'default') {
      $handler_settings = $this->getSetting('handler_settings');
      if (!empty($handler_settings['target_bundles'])) {
        $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
        $constraints[] = $constraint_manager->create('ComplexData', [
          'entity' => [
            'Bundle' => [
              'bundle' => $handler_settings['target_bundles'],
            ],
          ],
        ]);
      }
    }
    return $constraints;
  }

171
  /**
172
   * {@inheritdoc}
173 174 175
   */
  public function setValue($values, $notify = TRUE) {
    if (isset($values) && !is_array($values)) {
176 177 178
      // If either a scalar or an object was passed as the value for the item,
      // assign it to the 'entity' property since that works for both cases.
      $this->set('entity', $values, $notify);
179
    }
180
    else {
181 182 183
      parent::setValue($values, FALSE);
      // Support setting the field item with only one property, but make sure
      // values stay in sync if only property is passed.
184 185
      // NULL is a valid value, so we use array_key_exists().
      if (is_array($values) && array_key_exists('target_id', $values) && !isset($values['entity'])) {
186 187
        $this->onChange('target_id', FALSE);
      }
188
      elseif (is_array($values) && !array_key_exists('target_id', $values) && isset($values['entity'])) {
189 190
        $this->onChange('entity', FALSE);
      }
191
      elseif (is_array($values) && array_key_exists('target_id', $values) && isset($values['entity'])) {
192 193 194 195 196
        // If both properties are passed, verify the passed values match. The
        // only exception we allow is when we have a new entity: in this case
        // its actual id and target_id will be different, due to the new entity
        // marker.
        $entity_id = $this->get('entity')->getTargetIdentifier();
197 198 199 200
        // If the entity has been saved and we're trying to set both the
        // target_id and the entity values with a non-null target ID, then the
        // value for target_id should match the ID of the entity value.
        if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) {
201 202 203 204 205 206
          throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
        }
      }
      // Notify the parent if necessary.
      if ($notify && $this->parent) {
        $this->parent->onChange($this->getName());
207
      }
208
    }
209

210
  }
211

212 213 214
  /**
   * {@inheritdoc}
   */
215 216
  public function getValue() {
    $values = parent::getValue();
217 218 219

    // If there is an unsaved entity, return it as part of the field item values
    // to ensure idempotency of getValue() / setValue().
220
    if ($this->hasNewEntity()) {
221 222 223 224 225
      $values['entity'] = $this->entity;
    }
    return $values;
  }

226 227 228
  /**
   * {@inheritdoc}
   */
229
  public function onChange($property_name, $notify = TRUE) {
230
    // Make sure that the target ID and the target property stay in sync.
231 232
    if ($property_name == 'entity') {
      $property = $this->get('entity');
233
      $target_id = $property->isTargetNew() ? NULL : $property->getTargetIdentifier();
234
      $this->writePropertyValue('target_id', $target_id);
235
    }
236
    elseif ($property_name == 'target_id') {
237
      $this->writePropertyValue('entity', $this->target_id);
238
    }
239
    parent::onChange($property_name, $notify);
240
  }
241

242 243 244 245 246
  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    // Avoid loading the entity by first checking the 'target_id'.
247
    if ($this->target_id !== NULL) {
248 249
      return FALSE;
    }
250
    if ($this->entity && $this->entity instanceof EntityInterface) {
251 252 253 254 255 256 257 258 259
      return FALSE;
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function preSave() {
260
    if ($this->hasNewEntity()) {
261 262 263 264 265 266
      // Save the entity if it has not already been saved by some other code.
      if ($this->entity->isNew()) {
        $this->entity->save();
      }
      // Make sure the parent knows we are updating this property so it can
      // react properly.
267 268
      $this->target_id = $this->entity->id();
    }
269 270 271
    if (!$this->isEmpty() && $this->target_id === NULL) {
      $this->target_id = $this->entity->id();
    }
272 273
  }

274 275 276 277
  /**
   * {@inheritdoc}
   */
  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
278
    $manager = \Drupal::service('plugin.manager.entity_reference_selection');
279 280 281 282 283 284 285
    if ($referenceable = $manager->getSelectionHandler($field_definition)->getReferenceableEntities()) {
      $group = array_rand($referenceable);
      $values['target_id'] = array_rand($referenceable[$group]);
      return $values;
    }
  }

286 287 288 289 290 291 292 293 294 295
  /**
   * Determines whether the item holds an unsaved entity.
   *
   * This is notably used for "autocreate" widgets, and more generally to
   * support referencing freshly created entities (they will get saved
   * automatically as the hosting entity gets saved).
   *
   * @return bool
   *   TRUE if the item holds an unsaved entity.
   */
296
  public function hasNewEntity() {
297
    return !$this->isEmpty() && $this->target_id === NULL && $this->entity->isNew();
298
  }
299

300 301 302 303 304
  /**
   * {@inheritdoc}
   */
  public static function calculateDependencies(FieldDefinitionInterface $field_definition) {
    $dependencies = [];
305
    if ($default_value = $field_definition->getDefaultValueLiteral()) {
306
      $target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
307 308 309
      foreach ($default_value as $value) {
        if (is_array($value) && isset($value['target_uuid'])) {
          $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']);
310 311 312
          // If the entity does not exist do not create the dependency.
          // @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue()
          if ($entity) {
313
            $dependencies[$target_entity_type->getConfigDependencyKey()][] = $entity->getConfigDependencyName();
314 315 316 317 318 319 320
          }
        }
      }
    }
    return $dependencies;
  }

321 322 323 324 325
  /**
   * {@inheritdoc}
   */
  public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) {
    $changed = FALSE;
326
    if ($default_value = $field_definition->getDefaultValueLiteral()) {
327
      $target_entity_type = \Drupal::entityManager()->getDefinition($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
328 329 330
      foreach ($default_value as $key => $value) {
        if (is_array($value) && isset($value['target_uuid'])) {
          $entity = \Drupal::entityManager()->loadEntityByUuid($target_entity_type->id(), $value['target_uuid']);
331 332
          // @see \Drupal\Core\Field\EntityReferenceFieldItemList::processDefaultValue()
          if ($entity && isset($dependencies[$entity->getConfigDependencyKey()][$entity->getConfigDependencyName()])) {
333
            unset($default_value[$key]);
334 335 336 337
            $changed = TRUE;
          }
        }
      }
338 339 340
      if ($changed) {
        $field_definition->setDefaultValue($default_value);
      }
341 342 343 344
    }
    return $changed;
  }

345
}