3 namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Core\Entity\EntityTypeInterface;
7 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
8 use Drupal\Core\Field\FieldDefinitionInterface;
9 use Drupal\Core\Field\FieldStorageDefinitionInterface;
10 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
11 use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
12 use Drupal\Core\Form\FormStateInterface;
13 use Drupal\Core\TypedData\DataReferenceDefinition;
14 use Drupal\Core\TypedData\DataReferenceTargetDefinition;
15 use Drupal\Core\TypedData\OptionsProviderInterface;
16 use Drupal\entity_reference_revisions\EntityNeedsSaveInterface;
19 * Defines the 'entity_reference_revisions' entity field type.
21 * Supported settings (below the definition's 'settings' key) are:
22 * - target_type: The entity type to reference. Required.
23 * - target_bundle: (optional): If set, restricts the entity bundles which may
24 * may be referenced. May be set to an single bundle, or to an array of
28 * id = "entity_reference_revisions",
29 * label = @Translation("Entity reference revisions"),
30 * description = @Translation("An entity field containing an entity reference to a specific revision."),
31 * category = @Translation("Reference revisions"),
33 * class = "\Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem",
34 * list_class = "\Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList",
35 * default_formatter = "entity_reference_revisions_entity_view",
36 * default_widget = "entity_reference_revisions_autocomplete"
39 class EntityReferenceRevisionsItem extends EntityReferenceItem implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
44 public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
46 $entity_types = \Drupal::entityTypeManager()->getDefinitions();
48 foreach ($entity_types as $entity_type) {
49 if ($entity_type->isRevisionable()) {
50 $options[$entity_type->id()] = $entity_type->getLabel();
54 $element['target_type'] = array(
56 '#title' => $this->t('Type of item to reference'),
57 '#options' => $options,
58 '#default_value' => $this->getSetting('target_type'),
60 '#disabled' => $has_data,
70 public static function getPreconfiguredOptions() {
73 // Add all the commonly referenced entity types as distinct pre-configured
75 $entity_types = \Drupal::entityTypeManager()->getDefinitions();
76 $common_references = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
77 return $entity_type->get('common_reference_revisions_target') && $entity_type->isRevisionable();
80 /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
81 foreach ($common_references as $entity_type) {
83 $options[$entity_type->id()] = [
84 'label' => $entity_type->getLabel(),
85 'field_storage_config' => [
87 'target_type' => $entity_type->id(),
91 $default_reference_settings = $entity_type->get('default_reference_revision_settings');
92 if (is_array($default_reference_settings)) {
93 $options[$entity_type->id()] = array_merge($options[$entity_type->id()], $default_reference_settings);
103 public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
104 $settings = $field_definition->getSettings();
105 $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']);
107 $properties = parent::propertyDefinitions($field_definition);
109 if ($target_type_info->getKey('revision')) {
110 $target_revision_id_definition = DataReferenceTargetDefinition::create('integer')
111 ->setLabel(t('@label revision ID', array('@label' => $target_type_info->getLabel())))
112 ->setSetting('unsigned', TRUE);
114 $target_revision_id_definition->setRequired(TRUE);
115 $properties['target_revision_id'] = $target_revision_id_definition;
118 $properties['entity'] = DataReferenceDefinition::create('entity_revision')
119 ->setLabel($target_type_info->getLabel())
120 ->setDescription(t('The referenced entity revision'))
121 // The entity object is computed out of the entity ID.
124 ->setTargetDefinition(EntityDataDefinition::create($settings['target_type']));
132 public static function schema(FieldStorageDefinitionInterface $field_definition) {
133 $target_type = $field_definition->getSetting('target_type');
134 $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type);
136 $schema = parent::schema($field_definition);
138 if ($target_type_info->getKey('revision')) {
139 $schema['columns']['target_revision_id'] = array(
140 'description' => 'The revision ID of the target entity.',
144 $schema['indexes']['target_revision_id'] = array('target_revision_id');
153 public function setValue($values, $notify = TRUE) {
154 if (isset($values) && !is_array($values)) {
155 // If either a scalar or an object was passed as the value for the item,
156 // assign it to the 'entity' property since that works for both cases.
157 $this->set('entity', $values, $notify);
160 parent::setValue($values, FALSE);
161 // Support setting the field item with only one property, but make sure
162 // values stay in sync if only property is passed.
163 // NULL is a valid value, so we use array_key_exists().
164 if (is_array($values) && array_key_exists('target_id', $values) && !isset($values['entity'])) {
165 $this->onChange('target_id', FALSE);
167 elseif (is_array($values) && array_key_exists('target_revision_id', $values) && !isset($values['entity'])) {
168 $this->onChange('target_revision_id', FALSE);
170 elseif (is_array($values) && !array_key_exists('target_id', $values) && !array_key_exists('target_revision_id', $values) && isset($values['entity'])) {
171 $this->onChange('entity', FALSE);
173 elseif (is_array($values) && array_key_exists('target_id', $values) && isset($values['entity'])) {
174 // If both properties are passed, verify the passed values match. The
175 // only exception we allow is when we have a new entity: in this case
176 // its actual id and target_id will be different, due to the new entity
178 $entity_id = $this->get('entity')->getTargetIdentifier();
179 // If the entity has been saved and we're trying to set both the
180 // target_id and the entity values with a non-null target ID, then the
181 // value for target_id should match the ID of the entity value.
182 if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id !== $values['target_id'])) {
183 throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
186 // Notify the parent if necessary.
187 if ($notify && $this->getParent()) {
188 $this->getParent()->onChange($this->getName());
197 public function getValue() {
198 $values = parent::getValue();
199 if ($this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave()) {
200 $values['entity'] = $this->entity;
208 public function onChange($property_name, $notify = TRUE) {
209 // Make sure that the target ID and the target property stay in sync.
210 if ($property_name == 'entity') {
211 $property = $this->get('entity');
212 $target_id = $property->isTargetNew() ? NULL : $property->getTargetIdentifier();
213 $this->writePropertyValue('target_id', $target_id);
214 $this->writePropertyValue('target_revision_id', $property->getValue()->getRevisionId());
216 elseif ($property_name == 'target_id' && $this->target_id != NULL && $this->target_revision_id) {
217 $this->writePropertyValue('entity', array(
218 'target_id' => $this->target_id,
219 'target_revision_id' => $this->target_revision_id,
222 elseif ($property_name == 'target_revision_id' && $this->target_revision_id && $this->target_id) {
223 $this->writePropertyValue('entity', array(
224 'target_id' => $this->target_id,
225 'target_revision_id' => $this->target_revision_id,
228 if ($notify && isset($this->parent)) {
229 $this->parent->onChange($this->name);
236 public function isEmpty() {
237 // Avoid loading the entity by first checking the 'target_id'.
238 if ($this->target_id !== NULL && $this->target_revision_id !== NULL) {
241 if ($this->entity && $this->entity instanceof EntityInterface) {
250 public function preSave() {
251 $has_new = $this->hasNewEntity();
253 // If it is a new entity, parent will save it.
257 // Create a new revision if it is a composite entity in a host with a new
260 $host = $this->getEntity();
261 $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave();
262 if (!$host->isNew() && $host->isNewRevision() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
263 $this->entity->setNewRevision();
264 if ($host->isDefaultRevision()) {
265 $this->entity->isDefaultRevision(TRUE);
270 $this->entity->save();
274 $this->target_revision_id = $this->entity->getRevisionId();
281 public function postSave($update) {
282 parent::postSave($update);
285 // If any of entity, parent type or parent id is missing then return.
286 if (!$this->entity || !$this->entity->getEntityType()->get('entity_revision_parent_type_field') || !$this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
290 $entity = $this->entity;
291 $parent_entity = $this->getEntity();
293 // If the entity has a parent field name get the key.
294 if ($entity->getEntityType()->get('entity_revision_parent_field_name_field')) {
295 $parent_field_name = $entity->getEntityType()->get('entity_revision_parent_field_name_field');
297 // If parent field name has changed then set it.
298 if ($entity->get($parent_field_name)->value != $this->getFieldDefinition()->getName()) {
299 $entity->set($parent_field_name, $this->getFieldDefinition()->getName());
304 $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field');
305 $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field');
307 // If the parent type has changed then set it.
308 if ($entity->get($parent_type)->value != $parent_entity->getEntityTypeId()) {
309 $entity->set($parent_type, $parent_entity->getEntityTypeId());
312 // If the parent id has changed then set it.
313 if ($entity->get($parent_id)->value != $parent_entity->id()) {
314 $entity->set($parent_id, $parent_entity->id());
319 // Check if any of the keys has changed, save it, do not create a new
321 $entity->setNewRevision(FALSE);
329 public function deleteRevision() {
330 $child = $this->entity;
331 if ($child->isDefaultRevision()) {
332 // Do not delete if it is the default revision.
336 $host = $this->getEntity();
337 $field_name = $this->getFieldDefinition()->getName() . '.target_revision_id';
338 $all_revisions = \Drupal::entityQuery($host->getEntityTypeId())
339 ->condition($field_name, $child->getRevisionId())
343 if (count($all_revisions) > 1) {
344 // Do not delete if there is more than one usage of this revision.
348 \Drupal::entityTypeManager()->getStorage($child->getEntityTypeId())->deleteRevision($child->getRevisionId());
354 public function delete() {
357 if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
358 // Only delete composite entities if the host field is not translatable.
359 if (!$this->getFieldDefinition()->isTranslatable()) {
360 $this->entity->delete();
367 public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) {
374 public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
375 $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
376 $entity_manager = \Drupal::entityTypeManager();
378 // Bail if there are no referenceable entities.
379 if (!$selection_manager->getSelectionHandler($field_definition)->getReferenceableEntities()) {
383 // ERR field values are never cross referenced so we need to generate new
384 // target entities. First, find the target entity type.
385 $target_type_id = $field_definition->getFieldStorageDefinition()->getSetting('target_type');
386 $target_type = $entity_manager->getDefinition($target_type_id);
387 $handler_settings = $field_definition->getSetting('handler_settings');
389 // Determine referenceable bundles.
390 $bundle_manager = \Drupal::service('entity_type.bundle.info');
391 if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) {
392 $bundles = $handler_settings['target_bundles'];
395 $bundles = $bundle_manager->getBundleInfo($target_type_id);
397 $bundle = array_rand($bundles);
400 if ($label_key = $target_type->getKey('label')) {
401 $random = new Random();
402 // @TODO set the length somehow less arbitrary.
403 $label = $random->word(mt_rand(1, 10));
406 // Create entity stub.
407 $entity = $selection_manager->getSelectionHandler($field_definition)->createNewEntity($target_type_id, $bundle, $label, 0);
409 // Populate entity values and save.
410 $instances = $entity_manager
411 ->getStorage('field_config')
413 'entity_type' => $target_type_id,
417 foreach ($instances as $instance) {
418 $field_storage = $instance->getFieldStorageDefinition();
419 $max = $cardinality = $field_storage->getCardinality();
420 if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
421 // Just an arbitrary number for 'unlimited'
424 $field_name = $field_storage->getName();
425 $entity->{$field_name}->generateSampleItems($max);
431 'target_id' => $entity->id(),
432 'target_revision_id' => $entity->getRevisionId(),