Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / core / modules / migrate / src / Plugin / migrate / destination / EntityContentBase.php
1 <?php
2
3 namespace Drupal\migrate\Plugin\migrate\destination;
4
5 use Drupal\Core\Entity\ContentEntityInterface;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\Entity\EntityManagerInterface;
8 use Drupal\Core\Entity\EntityStorageInterface;
9 use Drupal\Core\Field\FieldTypePluginManagerInterface;
10 use Drupal\Core\TypedData\TranslatableInterface;
11 use Drupal\Core\TypedData\TypedDataInterface;
12 use Drupal\migrate\Audit\HighestIdInterface;
13 use Drupal\migrate\Plugin\MigrationInterface;
14 use Drupal\migrate\MigrateException;
15 use Drupal\migrate\Plugin\MigrateIdMapInterface;
16 use Drupal\migrate\Row;
17 use Symfony\Component\DependencyInjection\ContainerInterface;
18
19 /**
20  * Provides destination class for all content entities lacking a specific class.
21  *
22  * Available configuration keys:
23  * - translations: (optional) Boolean, indicates if the entity is translatable,
24  *   defaults to FALSE.
25  * - overwrite_properties: (optional) A list of properties that will be
26  *   overwritten if an entity with the same ID already exists. Any properties
27  *   that are not listed will not be overwritten.
28  *
29  * Example:
30  *
31  * The example below will create a 'node' entity of content type 'article'.
32  *
33  * The language of the source will be used because the configuration
34  * 'translations: true' was set. Without this configuration option the site's
35  * default language would be used.
36  *
37  * The example content type has fields 'title', 'body' and 'field_example'.
38  * The text format of the body field is defaulted to 'basic_html'. The example
39  * uses the EmbeddedDataSource source plugin for the sake of simplicity.
40  *
41  * If the migration is executed again in an update mode, any updates done in the
42  * destination Drupal site to the 'title' and 'body' fields would be overwritten
43  * with the original source values. Updates done to 'field_example' would be
44  * preserved because 'field_example' is not included in 'overwrite_properties'
45  * configuration.
46  * @code
47  * id: custom_article_migration
48  * label: Custom article migration
49  * source:
50  *   plugin: embedded_data
51  *   data_rows:
52  *     -
53  *       id: 1
54  *       langcode: 'fi'
55  *       title: 'Sivun otsikko'
56  *       field_example: 'Huhuu'
57  *       content: '<p>Hoi maailma</p>'
58  *   ids:
59  *     id:
60  *       type: integer
61  * process:
62  *   nid: id
63  *   langcode: langcode
64  *   title: title
65  *   field_example: field_example
66  *   'body/0/value': content
67  *   'body/0/format':
68  *     plugin: default_value
69  *     default_value: basic_html
70  * destination:
71  *   plugin: entity:node
72  *   default_bundle: article
73  *   translations: true
74  *   overwrite_properties:
75  *     - title
76  *     - body
77  * @endcode
78  *
79  * @see \Drupal\migrate\Plugin\migrate\destination\EntityRevision
80  */
81 class EntityContentBase extends Entity implements HighestIdInterface {
82
83   /**
84    * Entity manager.
85    *
86    * @var \Drupal\Core\Entity\EntityManagerInterface
87    */
88   protected $entityManager;
89
90   /**
91    * Field type plugin manager.
92    *
93    * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
94    */
95   protected $fieldTypeManager;
96
97   /**
98    * Constructs a content entity.
99    *
100    * @param array $configuration
101    *   A configuration array containing information about the plugin instance.
102    * @param string $plugin_id
103    *   The plugin ID for the plugin instance.
104    * @param mixed $plugin_definition
105    *   The plugin implementation definition.
106    * @param \Drupal\migrate\Plugin\MigrationInterface $migration
107    *   The migration entity.
108    * @param \Drupal\Core\Entity\EntityStorageInterface $storage
109    *   The storage for this entity type.
110    * @param array $bundles
111    *   The list of bundles this entity type has.
112    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
113    *   The entity manager service.
114    * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
115    *   The field type plugin manager service.
116    */
117   public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) {
118     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
119     $this->entityManager = $entity_manager;
120     $this->fieldTypeManager = $field_type_manager;
121   }
122
123   /**
124    * {@inheritdoc}
125    */
126   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
127     $entity_type = static::getEntityTypeId($plugin_id);
128     return new static(
129       $configuration,
130       $plugin_id,
131       $plugin_definition,
132       $migration,
133       $container->get('entity.manager')->getStorage($entity_type),
134       array_keys($container->get('entity.manager')->getBundleInfo($entity_type)),
135       $container->get('entity.manager'),
136       $container->get('plugin.manager.field.field_type')
137     );
138   }
139
140   /**
141    * {@inheritdoc}
142    */
143   public function import(Row $row, array $old_destination_id_values = []) {
144     $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
145     $entity = $this->getEntity($row, $old_destination_id_values);
146     if (!$entity) {
147       throw new MigrateException('Unable to get entity');
148     }
149
150     $ids = $this->save($entity, $old_destination_id_values);
151     if (!empty($this->configuration['translations'])) {
152       $ids[] = $entity->language()->getId();
153     }
154     return $ids;
155   }
156
157   /**
158    * Saves the entity.
159    *
160    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
161    *   The content entity.
162    * @param array $old_destination_id_values
163    *   (optional) An array of destination ID values. Defaults to an empty array.
164    *
165    * @return array
166    *   An array containing the entity ID.
167    */
168   protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
169     $entity->save();
170     return [$entity->id()];
171   }
172
173   /**
174    * {@inheritdoc}
175    */
176   public function isTranslationDestination() {
177     return !empty($this->configuration['translations']);
178   }
179
180   /**
181    * {@inheritdoc}
182    */
183   public function getIds() {
184     $id_key = $this->getKey('id');
185     $ids[$id_key] = $this->getDefinitionFromEntity($id_key);
186
187     if ($this->isTranslationDestination()) {
188       if (!$langcode_key = $this->getKey('langcode')) {
189         throw new MigrateException('This entity type does not support translation.');
190       }
191       $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
192     }
193
194     return $ids;
195   }
196
197   /**
198    * Updates an entity with the new values from row.
199    *
200    * @param \Drupal\Core\Entity\EntityInterface $entity
201    *   The entity to update.
202    * @param \Drupal\migrate\Row $row
203    *   The row object to update from.
204    *
205    * @return \Drupal\Core\Entity\EntityInterface|null
206    *   An updated entity, or NULL if it's the same as the one passed in.
207    */
208   protected function updateEntity(EntityInterface $entity, Row $row) {
209     $empty_destinations = $row->getEmptyDestinationProperties();
210     // By default, an update will be preserved.
211     $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
212
213     // Make sure we have the right translation.
214     if ($this->isTranslationDestination()) {
215       $property = $this->storage->getEntityType()->getKey('langcode');
216       if ($row->hasDestinationProperty($property)) {
217         $language = $row->getDestinationProperty($property);
218         if (!$entity->hasTranslation($language)) {
219           $entity->addTranslation($language);
220
221           // We're adding a translation, so delete it on rollback.
222           $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
223         }
224         $entity = $entity->getTranslation($language);
225       }
226     }
227
228     // If the migration has specified a list of properties to be overwritten,
229     // clone the row with an empty set of destination values, and re-add only
230     // the specified properties.
231     if (isset($this->configuration['overwrite_properties'])) {
232       $empty_destinations = array_intersect($empty_destinations, $this->configuration['overwrite_properties']);
233       $clone = $row->cloneWithoutDestination();
234       foreach ($this->configuration['overwrite_properties'] as $property) {
235         $clone->setDestinationProperty($property, $row->getDestinationProperty($property));
236       }
237       $row = $clone;
238     }
239
240     foreach ($row->getDestination() as $field_name => $values) {
241       $field = $entity->$field_name;
242       if ($field instanceof TypedDataInterface) {
243         $field->setValue($values);
244       }
245     }
246     foreach ($empty_destinations as $field_name) {
247       $entity->$field_name = NULL;
248     }
249
250     $this->setRollbackAction($row->getIdMap(), $rollback_action);
251
252     // We might have a different (translated) entity, so return it.
253     return $entity;
254   }
255
256   /**
257    * Populates as much of the stub row as possible.
258    *
259    * @param \Drupal\migrate\Row $row
260    *   The row of data.
261    */
262   protected function processStubRow(Row $row) {
263     $bundle_key = $this->getKey('bundle');
264     if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
265       if (empty($this->bundles)) {
266         throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId());
267       }
268       $row->setDestinationProperty($bundle_key, reset($this->bundles));
269     }
270
271     // Populate any required fields not already populated.
272     $fields = $this->entityManager
273       ->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle_key);
274     foreach ($fields as $field_name => $field_definition) {
275       if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) {
276         // Use the configured default value for this specific field, if any.
277         if ($default_value = $field_definition->getDefaultValueLiteral()) {
278           $values = $default_value;
279         }
280         else {
281           // Otherwise, ask the field type to generate a sample value.
282           $field_type = $field_definition->getType();
283           /** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */
284           $field_type_class = $this->fieldTypeManager
285             ->getPluginClass($field_definition->getType());
286           $values = $field_type_class::generateSampleValue($field_definition);
287           if (is_null($values)) {
288             // Handle failure to generate a sample value.
289             throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name);
290           }
291         }
292
293         $row->setDestinationProperty($field_name, $values);
294       }
295     }
296   }
297
298   /**
299    * {@inheritdoc}
300    */
301   public function rollback(array $destination_identifier) {
302     if ($this->isTranslationDestination()) {
303       // Attempt to remove the translation.
304       $entity = $this->storage->load(reset($destination_identifier));
305       if ($entity && $entity instanceof TranslatableInterface) {
306         if ($key = $this->getKey('langcode')) {
307           if (isset($destination_identifier[$key])) {
308             $langcode = $destination_identifier[$key];
309             if ($entity->hasTranslation($langcode)) {
310               // Make sure we don't remove the default translation.
311               $translation = $entity->getTranslation($langcode);
312               if (!$translation->isDefaultTranslation()) {
313                 $entity->removeTranslation($langcode);
314                 $entity->save();
315               }
316             }
317           }
318         }
319       }
320     }
321     else {
322       parent::rollback($destination_identifier);
323     }
324   }
325
326   /**
327    * Gets the field definition from a specific entity base field.
328    *
329    * The method takes the field ID as an argument and returns the field storage
330    * definition to be used in getIds() by querying the destination entity base
331    * field definition.
332    *
333    * @param string $key
334    *   The field ID key.
335    *
336    * @return array
337    *   An associative array with a structure that contains the field type, keyed
338    *   as 'type', together with field storage settings as they are returned by
339    *   FieldStorageDefinitionInterface::getSettings().
340    *
341    * @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
342    */
343   protected function getDefinitionFromEntity($key) {
344     $entity_type_id = static::getEntityTypeId($this->getPluginId());
345     /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $definitions */
346     $definitions = $this->entityManager->getBaseFieldDefinitions($entity_type_id);
347     $field_definition = $definitions[$key];
348
349     return [
350       'type' => $field_definition->getType(),
351     ] + $field_definition->getSettings();
352   }
353
354   /**
355    * {@inheritdoc}
356    */
357   public function getHighestId() {
358     $values = $this->storage->getQuery()
359       ->accessCheck(FALSE)
360       ->sort($this->getKey('id'), 'DESC')
361       ->range(0, 1)
362       ->execute();
363     return (int) current($values);
364   }
365
366 }