3e18d28b1ae8a63a3892fd8d36c2574955a117d3
[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 ($this->isTranslationDestination()) {
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     $ids = [];
185
186     $id_key = $this->getKey('id');
187     $ids[$id_key] = $this->getDefinitionFromEntity($id_key);
188
189     if ($this->isTranslationDestination()) {
190       $langcode_key = $this->getKey('langcode');
191       if (!$langcode_key) {
192         throw new MigrateException(sprintf('The "%s" entity type does not support translations.', $this->storage->getEntityTypeId()));
193       }
194       $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
195     }
196
197     return $ids;
198   }
199
200   /**
201    * Updates an entity with the new values from row.
202    *
203    * @param \Drupal\Core\Entity\EntityInterface $entity
204    *   The entity to update.
205    * @param \Drupal\migrate\Row $row
206    *   The row object to update from.
207    *
208    * @return \Drupal\Core\Entity\EntityInterface
209    *   An updated entity from row values.
210    */
211   protected function updateEntity(EntityInterface $entity, Row $row) {
212     $empty_destinations = $row->getEmptyDestinationProperties();
213     // By default, an update will be preserved.
214     $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
215
216     // Make sure we have the right translation.
217     if ($this->isTranslationDestination()) {
218       $property = $this->storage->getEntityType()->getKey('langcode');
219       if ($row->hasDestinationProperty($property)) {
220         $language = $row->getDestinationProperty($property);
221         if (!$entity->hasTranslation($language)) {
222           $entity->addTranslation($language);
223
224           // We're adding a translation, so delete it on rollback.
225           $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
226         }
227         $entity = $entity->getTranslation($language);
228       }
229     }
230
231     // If the migration has specified a list of properties to be overwritten,
232     // clone the row with an empty set of destination values, and re-add only
233     // the specified properties.
234     if (isset($this->configuration['overwrite_properties'])) {
235       $empty_destinations = array_intersect($empty_destinations, $this->configuration['overwrite_properties']);
236       $clone = $row->cloneWithoutDestination();
237       foreach ($this->configuration['overwrite_properties'] as $property) {
238         $clone->setDestinationProperty($property, $row->getDestinationProperty($property));
239       }
240       $row = $clone;
241     }
242
243     foreach ($row->getDestination() as $field_name => $values) {
244       $field = $entity->$field_name;
245       if ($field instanceof TypedDataInterface) {
246         $field->setValue($values);
247       }
248     }
249     foreach ($empty_destinations as $field_name) {
250       $entity->$field_name = NULL;
251     }
252
253     $this->setRollbackAction($row->getIdMap(), $rollback_action);
254
255     // We might have a different (translated) entity, so return it.
256     return $entity;
257   }
258
259   /**
260    * Populates as much of the stub row as possible.
261    *
262    * @param \Drupal\migrate\Row $row
263    *   The row of data.
264    */
265   protected function processStubRow(Row $row) {
266     $bundle_key = $this->getKey('bundle');
267     if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
268       if (empty($this->bundles)) {
269         throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId());
270       }
271       $row->setDestinationProperty($bundle_key, reset($this->bundles));
272     }
273
274     // Populate any required fields not already populated.
275     $fields = $this->entityManager
276       ->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle_key);
277     foreach ($fields as $field_name => $field_definition) {
278       if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) {
279         // Use the configured default value for this specific field, if any.
280         if ($default_value = $field_definition->getDefaultValueLiteral()) {
281           $values = $default_value;
282         }
283         else {
284           // Otherwise, ask the field type to generate a sample value.
285           $field_type = $field_definition->getType();
286           /** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */
287           $field_type_class = $this->fieldTypeManager
288             ->getPluginClass($field_definition->getType());
289           $values = $field_type_class::generateSampleValue($field_definition);
290           if (is_null($values)) {
291             // Handle failure to generate a sample value.
292             throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name);
293           }
294         }
295
296         $row->setDestinationProperty($field_name, $values);
297       }
298     }
299   }
300
301   /**
302    * {@inheritdoc}
303    */
304   public function rollback(array $destination_identifier) {
305     if ($this->isTranslationDestination()) {
306       // Attempt to remove the translation.
307       $entity = $this->storage->load(reset($destination_identifier));
308       if ($entity && $entity instanceof TranslatableInterface) {
309         if ($key = $this->getKey('langcode')) {
310           if (isset($destination_identifier[$key])) {
311             $langcode = $destination_identifier[$key];
312             if ($entity->hasTranslation($langcode)) {
313               // Make sure we don't remove the default translation.
314               $translation = $entity->getTranslation($langcode);
315               if (!$translation->isDefaultTranslation()) {
316                 $entity->removeTranslation($langcode);
317                 $entity->save();
318               }
319             }
320           }
321         }
322       }
323     }
324     else {
325       parent::rollback($destination_identifier);
326     }
327   }
328
329   /**
330    * Gets the field definition from a specific entity base field.
331    *
332    * The method takes the field ID as an argument and returns the field storage
333    * definition to be used in getIds() by querying the destination entity base
334    * field definition.
335    *
336    * @param string $key
337    *   The field ID key.
338    *
339    * @return array
340    *   An associative array with a structure that contains the field type, keyed
341    *   as 'type', together with field storage settings as they are returned by
342    *   FieldStorageDefinitionInterface::getSettings().
343    *
344    * @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
345    */
346   protected function getDefinitionFromEntity($key) {
347     $entity_type_id = static::getEntityTypeId($this->getPluginId());
348     /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $definitions */
349     $definitions = $this->entityManager->getBaseFieldDefinitions($entity_type_id);
350     $field_definition = $definitions[$key];
351
352     return [
353       'type' => $field_definition->getType(),
354     ] + $field_definition->getSettings();
355   }
356
357   /**
358    * {@inheritdoc}
359    */
360   public function getHighestId() {
361     $values = $this->storage->getQuery()
362       ->accessCheck(FALSE)
363       ->sort($this->getKey('id'), 'DESC')
364       ->range(0, 1)
365       ->execute();
366     return (int) current($values);
367   }
368
369 }