3 namespace Drupal\migrate\Plugin\migrate\destination;
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;
20 * Provides destination class for all content entities lacking a specific class.
22 * Available configuration keys:
23 * - translations: (optional) Boolean, indicates if the entity is translatable,
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.
31 * The example below will create a 'node' entity of content type 'article'.
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.
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.
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'
47 * id: custom_article_migration
48 * label: Custom article migration
50 * plugin: embedded_data
55 * title: 'Sivun otsikko'
56 * field_example: 'Huhuu'
57 * content: '<p>Hoi maailma</p>'
65 * field_example: field_example
66 * 'body/0/value': content
68 * plugin: default_value
69 * default_value: basic_html
72 * default_bundle: article
74 * overwrite_properties:
79 * @see \Drupal\migrate\Plugin\migrate\destination\EntityRevision
81 class EntityContentBase extends Entity implements HighestIdInterface {
86 * @var \Drupal\Core\Entity\EntityManagerInterface
88 protected $entityManager;
91 * Field type plugin manager.
93 * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
95 protected $fieldTypeManager;
98 * Constructs a content entity.
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.
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;
126 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
127 $entity_type = static::getEntityTypeId($plugin_id);
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')
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);
147 throw new MigrateException('Unable to get entity');
150 $ids = $this->save($entity, $old_destination_id_values);
151 if (!empty($this->configuration['translations'])) {
152 $ids[] = $entity->language()->getId();
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.
166 * An array containing the entity ID.
168 protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
170 return [$entity->id()];
176 public function isTranslationDestination() {
177 return !empty($this->configuration['translations']);
183 public function getIds() {
184 $id_key = $this->getKey('id');
185 $ids[$id_key] = $this->getDefinitionFromEntity($id_key);
187 if ($this->isTranslationDestination()) {
188 if (!$langcode_key = $this->getKey('langcode')) {
189 throw new MigrateException('This entity type does not support translation.');
191 $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
198 * Updates an entity with the new values from row.
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.
205 * @return \Drupal\Core\Entity\EntityInterface|null
206 * An updated entity, or NULL if it's the same as the one passed in.
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;
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);
221 // We're adding a translation, so delete it on rollback.
222 $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
224 $entity = $entity->getTranslation($language);
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));
240 foreach ($row->getDestination() as $field_name => $values) {
241 $field = $entity->$field_name;
242 if ($field instanceof TypedDataInterface) {
243 $field->setValue($values);
246 foreach ($empty_destinations as $field_name) {
247 $entity->$field_name = NULL;
250 $this->setRollbackAction($row->getIdMap(), $rollback_action);
252 // We might have a different (translated) entity, so return it.
257 * Populates as much of the stub row as possible.
259 * @param \Drupal\migrate\Row $row
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());
268 $row->setDestinationProperty($bundle_key, reset($this->bundles));
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;
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);
293 $row->setDestinationProperty($field_name, $values);
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);
322 parent::rollback($destination_identifier);
327 * Gets the field definition from a specific entity base field.
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
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().
341 * @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
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];
350 'type' => $field_definition->getType(),
351 ] + $field_definition->getSettings();
357 public function getHighestId() {
358 $values = $this->storage->getQuery()
360 ->sort($this->getKey('id'), 'DESC')
363 return (int) current($values);