PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php

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