Added Entity and Entity Reference Revisions which got dropped somewhere along the...
[yaffs-website] / web / modules / contrib / entity_reference_revisions / src / Plugin / Field / FieldType / EntityReferenceRevisionsItem.php
1 <?php
2
3 namespace Drupal\entity_reference_revisions\Plugin\Field\FieldType;
4
5 use Drupal\Component\Utility\Random;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\Entity\EntityTypeInterface;
8 use Drupal\Core\Entity\TranslatableRevisionableInterface;
9 use Drupal\Core\Entity\TypedData\EntityDataDefinition;
10 use Drupal\Core\Field\FieldDefinitionInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
13 use Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\TypedData\DataReferenceDefinition;
16 use Drupal\Core\TypedData\DataReferenceTargetDefinition;
17 use Drupal\Core\TypedData\OptionsProviderInterface;
18 use Drupal\entity_reference_revisions\EntityNeedsSaveInterface;
19
20 /**
21  * Defines the 'entity_reference_revisions' entity field type.
22  *
23  * Supported settings (below the definition's 'settings' key) are:
24  * - target_type: The entity type to reference. Required.
25  * - target_bundle: (optional): If set, restricts the entity bundles which may
26  *   may be referenced. May be set to an single bundle, or to an array of
27  *   allowed bundles.
28  *
29  * @FieldType(
30  *   id = "entity_reference_revisions",
31  *   label = @Translation("Entity reference revisions"),
32  *   description = @Translation("An entity field containing an entity reference to a specific revision."),
33  *   category = @Translation("Reference revisions"),
34  *   no_ui = FALSE,
35  *   class = "\Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem",
36  *   list_class = "\Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList",
37  *   default_formatter = "entity_reference_revisions_entity_view",
38  *   default_widget = "entity_reference_revisions_autocomplete"
39  * )
40  */
41 class EntityReferenceRevisionsItem extends EntityReferenceItem implements OptionsProviderInterface, PreconfiguredFieldUiOptionsInterface {
42
43   /**
44    * {@inheritdoc}
45    */
46   public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
47
48     $entity_types = \Drupal::entityTypeManager()->getDefinitions();
49     $options = array();
50     foreach ($entity_types as $entity_type) {
51       if ($entity_type->isRevisionable()) {
52         $options[$entity_type->id()] = $entity_type->getLabel();
53       }
54     }
55
56     $element['target_type'] = array(
57       '#type' => 'select',
58       '#title' => $this->t('Type of item to reference'),
59       '#options' => $options,
60       '#default_value' => $this->getSetting('target_type'),
61       '#required' => TRUE,
62       '#disabled' => $has_data,
63       '#size' => 1,
64     );
65
66     return $element;
67   }
68
69   /**
70    * {@inheritdoc}
71    */
72   public static function getPreconfiguredOptions() {
73     $options = array();
74
75     // Add all the commonly referenced entity types as distinct pre-configured
76     // options.
77     $entity_types = \Drupal::entityTypeManager()->getDefinitions();
78     $common_references = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
79       return $entity_type->get('common_reference_revisions_target') && $entity_type->isRevisionable();
80     });
81
82     /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
83     foreach ($common_references as $entity_type) {
84
85       $options[$entity_type->id()] = [
86         'label' => $entity_type->getLabel(),
87         'field_storage_config' => [
88           'settings' => [
89             'target_type' => $entity_type->id(),
90           ]
91         ]
92       ];
93       $default_reference_settings = $entity_type->get('default_reference_revision_settings');
94       if (is_array($default_reference_settings)) {
95         $options[$entity_type->id()] = array_merge($options[$entity_type->id()], $default_reference_settings);
96       }
97     }
98
99     return $options;
100   }
101
102   /**
103    * {@inheritdoc}
104    */
105   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
106     $settings = $field_definition->getSettings();
107     $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']);
108
109     $properties = parent::propertyDefinitions($field_definition);
110
111     if ($target_type_info->getKey('revision')) {
112       $target_revision_id_definition = DataReferenceTargetDefinition::create('integer')
113         ->setLabel(t('@label revision ID', array('@label' => $target_type_info->getLabel())))
114         ->setSetting('unsigned', TRUE);
115
116       $target_revision_id_definition->setRequired(TRUE);
117       $properties['target_revision_id'] = $target_revision_id_definition;
118     }
119
120     $properties['entity'] = DataReferenceDefinition::create('entity_revision')
121       ->setLabel($target_type_info->getLabel())
122       ->setDescription(t('The referenced entity revision'))
123       // The entity object is computed out of the entity ID.
124       ->setComputed(TRUE)
125       ->setReadOnly(FALSE)
126       ->setTargetDefinition(EntityDataDefinition::create($settings['target_type']));
127
128     return $properties;
129   }
130
131   /**
132    * {@inheritdoc}
133    */
134   public static function schema(FieldStorageDefinitionInterface $field_definition) {
135     $target_type = $field_definition->getSetting('target_type');
136     $target_type_info = \Drupal::entityTypeManager()->getDefinition($target_type);
137
138     $schema = parent::schema($field_definition);
139
140     if ($target_type_info->getKey('revision')) {
141       $schema['columns']['target_revision_id'] = array(
142         'description' => 'The revision ID of the target entity.',
143         'type' => 'int',
144         'unsigned' => TRUE,
145       );
146       $schema['indexes']['target_revision_id'] = array('target_revision_id');
147     }
148
149     return $schema;
150   }
151
152   /**
153    * {@inheritdoc}
154    */
155   public function setValue($values, $notify = TRUE) {
156     if (isset($values) && !is_array($values)) {
157       // If either a scalar or an object was passed as the value for the item,
158       // assign it to the 'entity' property since that works for both cases.
159       $this->set('entity', $values, $notify);
160     }
161     else {
162       parent::setValue($values, FALSE);
163       // Support setting the field item with only one property, but make sure
164       // values stay in sync if only property is passed.
165       // NULL is a valid value, so we use array_key_exists().
166       if (is_array($values) && array_key_exists('target_id', $values) && !isset($values['entity'])) {
167         $this->onChange('target_id', FALSE);
168       }
169       elseif (is_array($values) && array_key_exists('target_revision_id', $values) && !isset($values['entity'])) {
170         $this->onChange('target_revision_id', FALSE);
171       }
172       elseif (is_array($values) && !array_key_exists('target_id', $values) && !array_key_exists('target_revision_id', $values) && isset($values['entity'])) {
173         $this->onChange('entity', FALSE);
174       }
175       elseif (is_array($values) && array_key_exists('target_id', $values) && isset($values['entity'])) {
176         // If both properties are passed, verify the passed values match. The
177         // only exception we allow is when we have a new entity: in this case
178         // its actual id and target_id will be different, due to the new entity
179         // marker.
180         $entity_id = $this->get('entity')->getTargetIdentifier();
181         // If the entity has been saved and we're trying to set both the
182         // target_id and the entity values with a non-null target ID, then the
183         // value for target_id should match the ID of the entity value.
184         if (!$this->entity->isNew() && $values['target_id'] !== NULL && ($entity_id != $values['target_id'])) {
185           throw new \InvalidArgumentException('The target id and entity passed to the entity reference item do not match.');
186         }
187       }
188       // Notify the parent if necessary.
189       if ($notify && $this->getParent()) {
190         $this->getParent()->onChange($this->getName());
191       }
192     }
193
194   }
195
196   /**
197    * {@inheritdoc}
198    */
199   public function getValue() {
200     $values = parent::getValue();
201     if ($this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave()) {
202       $values['entity'] = $this->entity;
203     }
204     return $values;
205   }
206
207   /**
208    * {@inheritdoc}
209    */
210   public function onChange($property_name, $notify = TRUE) {
211     // Make sure that the target ID and the target property stay in sync.
212     if ($property_name == 'entity') {
213       $property = $this->get('entity');
214       $target_id = $property->isTargetNew() ? NULL : $property->getTargetIdentifier();
215       $this->writePropertyValue('target_id', $target_id);
216       $this->writePropertyValue('target_revision_id', $property->getValue()->getRevisionId());
217     }
218     elseif ($property_name == 'target_id' && $this->target_id != NULL && $this->target_revision_id) {
219       $this->writePropertyValue('entity', array(
220         'target_id' => $this->target_id,
221         'target_revision_id' => $this->target_revision_id,
222       ));
223     }
224     elseif ($property_name == 'target_revision_id' && $this->target_revision_id && $this->target_id) {
225       $this->writePropertyValue('entity', array(
226         'target_id' => $this->target_id,
227         'target_revision_id' => $this->target_revision_id,
228       ));
229     }
230     if ($notify && isset($this->parent)) {
231       $this->parent->onChange($this->name);
232     }
233   }
234
235   /**
236    * {@inheritdoc}
237    */
238   public function isEmpty() {
239     // Avoid loading the entity by first checking the 'target_id'.
240     if ($this->target_id !== NULL && $this->target_revision_id !== NULL) {
241       return FALSE;
242     }
243     if ($this->entity && $this->entity instanceof EntityInterface) {
244       return FALSE;
245     }
246     return TRUE;
247   }
248
249   /**
250    * {@inheritdoc}
251    */
252   public function preSave() {
253     $has_new = $this->hasNewEntity();
254
255     // If it is a new entity, parent will save it.
256     parent::preSave();
257
258     $is_affected = TRUE;
259     if (!$has_new) {
260       // Create a new revision if it is a composite entity in a host with a new
261       // revision.
262
263       $host = $this->getEntity();
264       $needs_save = $this->entity instanceof EntityNeedsSaveInterface && $this->entity->needsSave();
265
266       // The item is considered to be affected if the field is either
267       // untranslatable or there are translation changes. This ensures that for
268       // translatable fields, a new revision of the referenced entity is only
269       // created for the affected translations and that the revision ID does not
270       // change on the unaffected translations. In turn, the host entity is not
271       // marked as affected for these translations.
272       $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges());
273       if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
274         if ($host->isNewRevision()) {
275           $this->entity->setNewRevision();
276           $needs_save = TRUE;
277         }
278         // Additionally ensure that the default revision state is kept in sync.
279         if ($this->entity && $host->isDefaultRevision() != $this->entity->isDefaultRevision()) {
280           $this->entity->isDefaultRevision($host->isDefaultRevision());
281           $needs_save = TRUE;
282         }
283       }
284       if ($needs_save) {
285         $this->entity->save();
286       }
287     }
288     if ($this->entity && $is_affected) {
289       $this->target_revision_id = $this->entity->getRevisionId();
290     }
291   }
292
293   /**
294    * {@inheritdoc}
295    */
296   public function postSave($update) {
297     parent::postSave($update);
298
299     $needs_save = FALSE;
300     // If any of entity, parent type or parent id is missing then return.
301     if (!$this->entity || !$this->entity->getEntityType()->get('entity_revision_parent_type_field') || !$this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
302       return;
303     }
304
305     $entity = $this->entity;
306     $parent_entity = $this->getEntity();
307
308     // If the entity has a parent field name get the key.
309     if ($entity->getEntityType()->get('entity_revision_parent_field_name_field')) {
310       $parent_field_name = $entity->getEntityType()->get('entity_revision_parent_field_name_field');
311
312       // If parent field name has changed then set it.
313       if ($entity->get($parent_field_name)->value != $this->getFieldDefinition()->getName()) {
314         $entity->set($parent_field_name, $this->getFieldDefinition()->getName());
315         $needs_save = TRUE;
316       }
317     }
318
319     // Keep in sync the translation languages between the parent and the child.
320     // For non translatable fields we have to do this in ::preSave but for
321     // translatable fields we have all the information we need in ::delete.
322     if (isset($parent_entity->original) && !$this->getFieldDefinition()->isTranslatable()) {
323       $langcodes = array_keys($parent_entity->getTranslationLanguages());
324       $original_langcodes = array_keys($parent_entity->original->getTranslationLanguages());
325       if ($removed_langcodes = array_diff($original_langcodes, $langcodes)) {
326         foreach ($removed_langcodes as $removed_langcode) {
327           if ($entity->hasTranslation($removed_langcode)  && $entity->getUntranslated()->language()->getId() != $removed_langcode) {
328             $entity->removeTranslation($removed_langcode);
329           }
330         }
331         $needs_save = TRUE;
332       }
333     }
334
335     $parent_type = $entity->getEntityType()->get('entity_revision_parent_type_field');
336     $parent_id = $entity->getEntityType()->get('entity_revision_parent_id_field');
337
338     // If the parent type has changed then set it.
339     if ($entity->get($parent_type)->value != $parent_entity->getEntityTypeId()) {
340       $entity->set($parent_type, $parent_entity->getEntityTypeId());
341       $needs_save = TRUE;
342     }
343     // If the parent id has changed then set it.
344     if ($entity->get($parent_id)->value != $parent_entity->id()) {
345       $entity->set($parent_id, $parent_entity->id());
346       $needs_save = TRUE;
347     }
348
349     if ($needs_save) {
350       // Check if any of the keys has changed, save it, do not create a new
351       // revision.
352       $entity->setNewRevision(FALSE);
353       $entity->save();
354     }
355   }
356
357   /**
358    * {@inheritdoc}
359    */
360   public function deleteRevision() {
361     $child = $this->entity;
362     // Return early, and do not delete the child revision, when the child
363     // revision is either:
364     // 1: Missing.
365     // 2: A default revision.
366     if (!$child || $child->isDefaultRevision()) {
367       return;
368     }
369
370     $host = $this->getEntity();
371     $field_name = $this->getFieldDefinition()->getName() . '.target_revision_id';
372     $all_revisions = \Drupal::entityQuery($host->getEntityTypeId())
373       ->condition($field_name, $child->getRevisionId())
374       ->allRevisions()
375       ->accessCheck(FALSE)
376       ->execute();
377
378     if (count($all_revisions) > 1) {
379       // Do not delete if there is more than one usage of this revision.
380       return;
381     }
382
383     \Drupal::entityTypeManager()->getStorage($child->getEntityTypeId())->deleteRevision($child->getRevisionId());
384   }
385
386   /**
387    * {@inheritdoc}
388    */
389   public function delete() {
390     parent::delete();
391
392     if ($this->entity && $this->entity->getEntityType()->get('entity_revision_parent_type_field') && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
393       // Only delete composite entities if the host field is not translatable.
394       if (!$this->getFieldDefinition()->isTranslatable()) {
395         $this->entity->delete();
396       }
397     }
398   }
399
400   /**
401    * {@inheritdoc}
402    */
403   public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies) {
404     $changed = FALSE;
405     $entity_manager = \Drupal::entityManager();
406     $target_entity_type = $entity_manager->getDefinition($field_definition->getFieldStorageDefinition()
407       ->getSetting('target_type'));
408     $handler_settings = $field_definition->getSetting('handler_settings');
409
410     // Update the 'target_bundles' handler setting if a bundle config dependency
411     // has been removed.
412     if (!empty($handler_settings['target_bundles'])) {
413       if ($bundle_entity_type_id = $target_entity_type->getBundleEntityType()) {
414         if ($storage = $entity_manager->getStorage($bundle_entity_type_id)) {
415           foreach ($storage->loadMultiple($handler_settings['target_bundles']) as $bundle) {
416             if (isset($dependencies[$bundle->getConfigDependencyKey()][$bundle->getConfigDependencyName()])) {
417               unset($handler_settings['target_bundles'][$bundle->id()]);
418               $changed = TRUE;
419
420               // In case we deleted the only target bundle allowed by the field
421               // we can log a message because the behaviour of the field will
422               // have changed.
423               if ($handler_settings['target_bundles'] === []) {
424                 \Drupal::logger('entity_reference_revisions')
425                   ->notice('The %target_bundle bundle (entity type: %target_entity_type) was deleted. As a result, the %field_name entity reference revisions field (entity_type: %entity_type, bundle: %bundle) no longer specifies a specific target bundle. The field will now accept any bundle and may need to be adjusted.', [
426                     '%target_bundle' => $bundle->label(),
427                     '%target_entity_type' => $bundle->getEntityType()
428                       ->getBundleOf(),
429                     '%field_name' => $field_definition->getName(),
430                     '%entity_type' => $field_definition->getTargetEntityTypeId(),
431                     '%bundle' => $field_definition->getTargetBundle()
432                   ]);
433               }
434             }
435           }
436         }
437       }
438     }
439
440     if ($changed) {
441       $field_definition->setSetting('handler_settings', $handler_settings);
442     }
443
444     return $changed;
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
451     $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
452     $entity_manager = \Drupal::entityTypeManager();
453
454     // Bail if there are no referenceable entities.
455     if (!$selection_manager->getSelectionHandler($field_definition)->getReferenceableEntities()) {
456       return;
457     }
458
459     // ERR field values are never cross referenced so we need to generate new
460     // target entities. First, find the target entity type.
461     $target_type_id = $field_definition->getFieldStorageDefinition()->getSetting('target_type');
462     $target_type = $entity_manager->getDefinition($target_type_id);
463     $handler_settings = $field_definition->getSetting('handler_settings');
464
465     // Determine referenceable bundles.
466     $bundle_manager = \Drupal::service('entity_type.bundle.info');
467     if (isset($handler_settings['target_bundles']) && is_array($handler_settings['target_bundles'])) {
468       $bundles = $handler_settings['target_bundles'];
469     }
470     else {
471       $bundles = $bundle_manager->getBundleInfo($target_type_id);
472     }
473     $bundle = array_rand($bundles);
474
475     $label = NULL;
476     if ($label_key = $target_type->getKey('label')) {
477       $random = new Random();
478       // @TODO set the length somehow less arbitrary.
479       $label = $random->word(mt_rand(1, 10));
480     }
481
482     // Create entity stub.
483     $entity = $selection_manager->getSelectionHandler($field_definition)->createNewEntity($target_type_id, $bundle, $label, 0);
484
485     // Populate entity values and save.
486     $instances = $entity_manager
487       ->getStorage('field_config')
488       ->loadByProperties([
489         'entity_type' => $target_type_id,
490         'bundle' => $bundle,
491       ]);
492
493     foreach ($instances as $instance) {
494       $field_storage = $instance->getFieldStorageDefinition();
495       $max = $cardinality = $field_storage->getCardinality();
496       if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
497         // Just an arbitrary number for 'unlimited'
498         $max = rand(1, 5);
499       }
500       $field_name = $field_storage->getName();
501       $entity->{$field_name}->generateSampleItems($max);
502     }
503
504     $entity->save();
505
506     return [
507       'target_id' => $entity->id(),
508       'target_revision_id' => $entity->getRevisionId(),
509     ];
510   }
511
512 }