2d3edfc6d49a61e74a61d237b45cb3e12fd3ffb3
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Sql / SqlContentEntityStorageSchemaConverter.php
1 <?php
2
3 namespace Drupal\Core\Entity\Sql;
4
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Entity\ContentEntityTypeInterface;
7 use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
8 use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
9 use Drupal\Core\Entity\EntityStorageException;
10 use Drupal\Core\Entity\EntityTypeManagerInterface;
11 use Drupal\Core\Field\BaseFieldDefinition;
12 use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
13 use Drupal\Core\Site\Settings;
14 use Drupal\Core\StringTranslation\TranslatableMarkup;
15
16 /**
17  * Defines a schema converter for entity types with existing data.
18  *
19  * For now, this can only be used to convert an entity type from
20  * non-revisionable to revisionable, however, it should be expanded so it can
21  * also handle converting an entity type to be translatable.
22  */
23 class SqlContentEntityStorageSchemaConverter {
24
25   /**
26    * The entity type ID this schema converter is responsible for.
27    *
28    * @var string
29    */
30   protected $entityTypeId;
31
32   /**
33    * The entity type manager.
34    *
35    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
36    */
37   protected $entityTypeManager;
38
39   /**
40    * The entity definition update manager service.
41    *
42    * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
43    */
44   protected $entityDefinitionUpdateManager;
45
46   /**
47    * The last installed schema repository service.
48    *
49    * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
50    */
51   protected $lastInstalledSchemaRepository;
52
53   /**
54    * The key-value collection for tracking installed storage schema.
55    *
56    * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
57    */
58   protected $installedStorageSchema;
59
60   /**
61    * The database connection.
62    *
63    * @var \Drupal\Core\Database\Connection
64    */
65   protected $database;
66
67   /**
68    * SqlContentEntityStorageSchemaConverter constructor.
69    *
70    * @param string $entity_type_id
71    *   The ID of the entity type.
72    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
73    *   The entity type manager.
74    * @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
75    *   Entity definition update manager service.
76    * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository
77    *   Last installed schema repository service.
78    * @param \Drupal\Core\Database\Connection $database
79    *   Database connection.
80    */
81   public function __construct($entity_type_id, EntityTypeManagerInterface $entity_type_manager, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository, KeyValueStoreInterface $installed_storage_schema, Connection $database) {
82     $this->entityTypeId = $entity_type_id;
83     $this->entityTypeManager = $entity_type_manager;
84     $this->entityDefinitionUpdateManager = $entity_definition_update_manager;
85     $this->lastInstalledSchemaRepository = $last_installed_schema_repository;
86     $this->installedStorageSchema = $installed_storage_schema;
87     $this->database = $database;
88   }
89
90   /**
91    * Converts an entity type with existing data to be revisionable.
92    *
93    * This process does the following tasks:
94    *   - creates the schema from scratch with the new revisionable entity type
95    *     definition (i.e. the current definition of the entity type from code)
96    *     using temporary table names;
97    *   - loads the initial entity data by using the last installed entity and
98    *     field storage definitions;
99    *   - saves the entity data to the temporary tables;
100    *   - at the end of the process:
101    *     - deletes the original tables and replaces them with the temporary ones
102    *       that hold the new (revisionable) entity data;
103    *     - updates the installed entity schema data;
104    *     - updates the entity type definition in order to trigger the
105    *       \Drupal\Core\Entity\EntityTypeEvents::UPDATE event;
106    *     - updates the field storage definitions in order to mark the
107    *       revisionable ones as such.
108    *
109    * In case of an error during the entity save process, the temporary tables
110    * are deleted and the original entity type and field storage definitions are
111    * restored.
112    *
113    * @param array $sandbox
114    *   The sandbox array from a hook_update_N() implementation.
115    * @param string[] $fields_to_update
116    *   (optional) An array of field names that should be converted to be
117    *   revisionable. Note that the 'langcode' field, if present, is updated
118    *   automatically. Defaults to an empty array.
119    *
120    * @throws \Exception
121    *   Re-throws any exception raised during the update process.
122    */
123   public function convertToRevisionable(array &$sandbox, array $fields_to_update = []) {
124     // If 'progress' is not set, then this will be the first run of the batch.
125     if (!isset($sandbox['progress'])) {
126       // Store the original entity type and field definitions in the $sandbox
127       // array so we can use them later in the update process.
128       $this->collectOriginalDefinitions($sandbox);
129
130       // Create a temporary environment in which the new data will be stored.
131       $this->createTemporaryDefinitions($sandbox, $fields_to_update);
132
133       // Create the updated entity schema using temporary tables.
134       /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
135       $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
136       $storage->setTemporary(TRUE);
137       $storage->setEntityType($sandbox['temporary_entity_type']);
138       $storage->onEntityTypeCreate($sandbox['temporary_entity_type']);
139     }
140
141     // Copy over the existing data to the new temporary tables.
142     $this->copyData($sandbox);
143
144     // If the data copying has finished successfully, we can drop the temporary
145     // tables and call the appropriate update mechanisms.
146     if ($sandbox['#finished'] == 1) {
147       $this->entityTypeManager->useCaches(FALSE);
148       $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
149
150       // Rename the original tables so we can put them back in place in case
151       // anything goes wrong.
152       foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
153         $old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
154         $this->database->schema()->renameTable($table_name, $old_table_name);
155       }
156
157       // Put the new tables in place and update the entity type and field
158       // storage definitions.
159       try {
160         $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
161         $storage->setEntityType($actual_entity_type);
162         $storage->setTemporary(FALSE);
163         $actual_table_names = $storage->getTableMapping()->getTableNames();
164
165         $table_name_mapping = [];
166         foreach ($actual_table_names as $new_table_name) {
167           $temp_table_name = TemporaryTableMapping::getTempTableName($new_table_name);
168           $table_name_mapping[$temp_table_name] = $new_table_name;
169           $this->database->schema()->renameTable($temp_table_name, $new_table_name);
170         }
171
172         // Rename the tables in the cached entity schema data.
173         $entity_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
174         foreach ($entity_schema_data as $temp_table_name => $schema) {
175           if (isset($table_name_mapping[$temp_table_name])) {
176             $entity_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
177             unset($entity_schema_data[$temp_table_name]);
178           }
179         }
180         $this->installedStorageSchema->set($this->entityTypeId . '.entity_schema_data', $entity_schema_data);
181
182         // Rename the tables in the cached field schema data.
183         foreach ($sandbox['updated_storage_definitions'] as $storage_definition) {
184           $field_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
185           foreach ($field_schema_data as $temp_table_name => $schema) {
186             if (isset($table_name_mapping[$temp_table_name])) {
187               $field_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
188               unset($field_schema_data[$temp_table_name]);
189             }
190           }
191           $this->installedStorageSchema->set($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), $field_schema_data);
192         }
193
194         // Instruct the entity schema handler that data migration has been
195         // handled already and update the entity type.
196         $actual_entity_type->set('requires_data_migration', FALSE);
197         $this->entityDefinitionUpdateManager->updateEntityType($actual_entity_type);
198
199         // Update the field storage definitions.
200         $this->updateFieldStorageDefinitionsToRevisionable($actual_entity_type, $sandbox['original_storage_definitions'], $fields_to_update);
201       }
202       catch (\Exception $e) {
203         // Something went wrong, bring back the original tables.
204         foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
205           // We are in the 'original data recovery' phase, so we need to be sure
206           // that the initial tables can be properly restored.
207           if ($this->database->schema()->tableExists($table_name)) {
208             $this->database->schema()->dropTable($table_name);
209           }
210
211           $old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
212           $this->database->schema()->renameTable($old_table_name, $table_name);
213         }
214
215         // Re-throw the original exception.
216         throw $e;
217       }
218
219       // At this point the update process either finished successfully or any
220       // error has been handled already, so we can drop the backup entity
221       // tables.
222       foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
223         $old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
224         $this->database->schema()->dropTable($old_table_name);
225       }
226     }
227   }
228
229   /**
230    * Loads entities from the original storage and saves them to a temporary one.
231    *
232    * @param array &$sandbox
233    *   The sandbox array from a hook_update_N() implementation.
234    *
235    * @throws \Drupal\Core\Entity\EntityStorageException
236    *   Thrown in case of an error during the entity save process.
237    */
238   protected function copyData(array &$sandbox) {
239     /** @var \Drupal\Core\Entity\Sql\TemporaryTableMapping $temporary_table_mapping */
240     $temporary_table_mapping = $sandbox['temporary_table_mapping'];
241     $temporary_entity_type = $sandbox['temporary_entity_type'];
242     $original_table_mapping = $sandbox['original_table_mapping'];
243     $original_entity_type = $sandbox['original_entity_type'];
244
245     $original_base_table = $original_entity_type->getBaseTable();
246
247     $revision_id_key = $temporary_entity_type->getKey('revision');
248     $revision_default_key = $temporary_entity_type->getRevisionMetadataKey('revision_default');
249     $revision_translation_affected_key = $temporary_entity_type->getKey('revision_translation_affected');
250
251     // If 'progress' is not set, then this will be the first run of the batch.
252     if (!isset($sandbox['progress'])) {
253       $sandbox['progress'] = 0;
254       $sandbox['current_id'] = 0;
255       $sandbox['max'] = $this->database->select($original_base_table)
256         ->countQuery()
257         ->execute()
258         ->fetchField();
259     }
260
261     $id = $original_entity_type->getKey('id');
262
263     // Define the step size.
264     $step_size = Settings::get('entity_update_batch_size', 50);
265
266     // Get the next entity IDs to migrate.
267     $entity_ids = $this->database->select($original_base_table)
268       ->fields($original_base_table, [$id])
269       ->condition($id, $sandbox['current_id'], '>')
270       ->orderBy($id, 'ASC')
271       ->range(0, $step_size)
272       ->execute()
273       ->fetchAllKeyed(0, 0);
274
275     /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
276     $storage = $this->entityTypeManager->getStorage($temporary_entity_type->id());
277     $storage->setEntityType($original_entity_type);
278     $storage->setTableMapping($original_table_mapping);
279
280     $entities = $storage->loadMultiple($entity_ids);
281
282     // Now inject the temporary entity type definition and table mapping in the
283     // storage and re-save the entities.
284     $storage->setEntityType($temporary_entity_type);
285     $storage->setTableMapping($temporary_table_mapping);
286
287     foreach ($entities as $entity_id => $entity) {
288       try {
289         // Set the revision ID to be same as the entity ID.
290         $entity->set($revision_id_key, $entity_id);
291
292         // We had no revisions so far, so the existing data belongs to the
293         // default revision now.
294         $entity->set($revision_default_key, TRUE);
295
296         // Set the 'revision_translation_affected' flag to TRUE to match the
297         // previous API return value: if the field was not defined the value
298         // returned was always TRUE.
299         $entity->set($revision_translation_affected_key, TRUE);
300
301         // Treat the entity as new in order to make the storage do an INSERT
302         // rather than an UPDATE.
303         $entity->enforceIsNew(TRUE);
304
305         // Finally, save the entity in the temporary storage.
306         $storage->save($entity);
307       }
308       catch (\Exception $e) {
309         // In case of an error during the save process, we need to roll back the
310         // original entity type and field storage definitions and clean up the
311         // temporary tables.
312         $this->restoreOriginalDefinitions($sandbox);
313
314         foreach ($temporary_table_mapping->getTableNames() as $table_name) {
315           $this->database->schema()->dropTable($table_name);
316         }
317
318         // Re-throw the original exception with a helpful message.
319         throw new EntityStorageException("The entity update process failed while processing the entity {$original_entity_type->id()}:$entity_id.", $e->getCode(), $e);
320       }
321
322       $sandbox['progress']++;
323       $sandbox['current_id'] = $entity_id;
324     }
325
326     // If we're not in maintenance mode, the number of entities could change at
327     // any time so make sure that we always use the latest record count.
328     $sandbox['max'] = $this->database->select($original_base_table)
329       ->countQuery()
330       ->execute()
331       ->fetchField();
332
333     $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
334   }
335
336   /**
337    * Updates field definitions to be revisionable.
338    *
339    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
340    *   A content entity type definition.
341    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
342    *   An array of field storage definitions.
343    * @param array $fields_to_update
344    *   (optional) An array of field names for which to enable revision support.
345    *   Defaults to an empty array.
346    * @param bool $update_cached_definitions
347    *   (optional) Whether to update the cached field storage definitions in the
348    *   entity definition update manager. Defaults to TRUE.
349    *
350    * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
351    *   An array of updated field storage definitions.
352    */
353   protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = [], $update_cached_definitions = TRUE) {
354     $updated_storage_definitions = array_map(function ($storage_definition) {
355       return clone $storage_definition;
356     }, $storage_definitions);
357
358     // Update the 'langcode' field manually, as it is configured in the base
359     // content entity field definitions.
360     if ($entity_type->hasKey('langcode')) {
361       $fields_to_update = array_merge([$entity_type->getKey('langcode')], $fields_to_update);
362     }
363
364     foreach ($fields_to_update as $field_name) {
365       if (!$updated_storage_definitions[$field_name]->isRevisionable()) {
366         $updated_storage_definitions[$field_name]->setRevisionable(TRUE);
367
368         if ($update_cached_definitions) {
369           $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($updated_storage_definitions[$field_name]);
370         }
371       }
372     }
373
374     // Add the revision ID field.
375     $revision_field = BaseFieldDefinition::create('integer')
376       ->setName($entity_type->getKey('revision'))
377       ->setTargetEntityTypeId($entity_type->id())
378       ->setTargetBundle(NULL)
379       ->setLabel(new TranslatableMarkup('Revision ID'))
380       ->setReadOnly(TRUE)
381       ->setSetting('unsigned', TRUE);
382
383     if ($update_cached_definitions) {
384       $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_field);
385     }
386     $updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field;
387
388     // Add the default revision flag field.
389     $field_name = $entity_type->getRevisionMetadataKey('revision_default');
390     $storage_definition = BaseFieldDefinition::create('boolean')
391       ->setName($field_name)
392       ->setTargetEntityTypeId($entity_type->id())
393       ->setTargetBundle(NULL)
394       ->setLabel(t('Default revision'))
395       ->setDescription(t('A flag indicating whether this was a default revision when it was saved.'))
396       ->setStorageRequired(TRUE)
397       ->setTranslatable(FALSE)
398       ->setRevisionable(TRUE);
399
400     if ($update_cached_definitions) {
401       $this->entityDefinitionUpdateManager->installFieldStorageDefinition($field_name, $entity_type->id(), $entity_type->getProvider(), $storage_definition);
402     }
403     $updated_storage_definitions[$field_name] = $storage_definition;
404
405     // Add the 'revision_translation_affected' field if needed.
406     if ($entity_type->isTranslatable()) {
407       $revision_translation_affected_field = BaseFieldDefinition::create('boolean')
408         ->setName($entity_type->getKey('revision_translation_affected'))
409         ->setTargetEntityTypeId($entity_type->id())
410         ->setTargetBundle(NULL)
411         ->setLabel(new TranslatableMarkup('Revision translation affected'))
412         ->setDescription(new TranslatableMarkup('Indicates if the last edit of a translation belongs to current revision.'))
413         ->setReadOnly(TRUE)
414         ->setRevisionable(TRUE)
415         ->setTranslatable(TRUE);
416
417       if ($update_cached_definitions) {
418         $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_translation_affected_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_translation_affected_field);
419       }
420       $updated_storage_definitions[$entity_type->getKey('revision_translation_affected')] = $revision_translation_affected_field;
421     }
422
423     return $updated_storage_definitions;
424   }
425
426   /**
427    * Collects the original definitions of an entity type and its fields.
428    *
429    * @param array &$sandbox
430    *   A sandbox array from a hook_update_N() implementation.
431    */
432   protected function collectOriginalDefinitions(array &$sandbox) {
433     $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($this->entityTypeId);
434     $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
435
436     /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
437     $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
438     $storage->setEntityType($original_entity_type);
439     $original_table_mapping = $storage->getTableMapping($original_storage_definitions);
440
441     $sandbox['original_entity_type'] = $original_entity_type;
442     $sandbox['original_storage_definitions'] = $original_storage_definitions;
443     $sandbox['original_table_mapping'] = $original_table_mapping;
444
445     $sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
446     foreach ($original_storage_definitions as $storage_definition) {
447       $sandbox['original_field_schema_data'][$storage_definition->getName()] = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
448     }
449   }
450
451   /**
452    * Restores the entity type, field storage definitions and their schema data.
453    *
454    * @param array $sandbox
455    *   The sandbox array from a hook_update_N() implementation.
456    */
457   protected function restoreOriginalDefinitions(array $sandbox) {
458     $original_entity_type = $sandbox['original_entity_type'];
459     $original_storage_definitions = $sandbox['original_storage_definitions'];
460     $original_entity_schema_data = $sandbox['original_entity_schema_data'];
461     $original_field_schema_data = $sandbox['original_field_schema_data'];
462
463     $this->lastInstalledSchemaRepository->setLastInstalledDefinition($original_entity_type);
464     $this->lastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($original_entity_type->id(), $original_storage_definitions);
465
466     $this->installedStorageSchema->set($original_entity_type->id() . '.entity_schema_data', $original_entity_schema_data);
467     foreach ($original_field_schema_data as $field_name => $field_schema_data) {
468       $this->installedStorageSchema->set($original_entity_type->id() . '.field_schema_data.' . $field_name, $field_schema_data);
469     }
470   }
471
472   /**
473    * Creates temporary entity type, field storage and table mapping objects.
474    *
475    * @param array &$sandbox
476    *   A sandbox array from a hook_update_N() implementation.
477    * @param string[] $fields_to_update
478    *   (optional) An array of field names that should be converted to be
479    *   revisionable. Note that the 'langcode' field, if present, is updated
480    *   automatically. Defaults to an empty array.
481    */
482   protected function createTemporaryDefinitions(array &$sandbox, array $fields_to_update) {
483     // Make sure to get the latest entity type definition from code.
484     $this->entityTypeManager->useCaches(FALSE);
485     $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
486
487     $temporary_entity_type = clone $actual_entity_type;
488     $temporary_entity_type->set('base_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getBaseTable()));
489     $temporary_entity_type->set('revision_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionTable()));
490     if ($temporary_entity_type->isTranslatable()) {
491       $temporary_entity_type->set('data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getDataTable()));
492       $temporary_entity_type->set('revision_data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionDataTable()));
493     }
494
495     /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
496     $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
497     $storage->setTemporary(TRUE);
498     $storage->setEntityType($temporary_entity_type);
499
500     $updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE);
501     $temporary_table_mapping = $storage->getTableMapping($updated_storage_definitions);
502
503     $sandbox['temporary_entity_type'] = $temporary_entity_type;
504     $sandbox['temporary_table_mapping'] = $temporary_table_mapping;
505     $sandbox['updated_storage_definitions'] = $updated_storage_definitions;
506   }
507
508 }