Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Sql / SqlContentEntityStorage.php
1 <?php
2
3 namespace Drupal\Core\Entity\Sql;
4
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
7 use Drupal\Core\Database\Connection;
8 use Drupal\Core\Database\Database;
9 use Drupal\Core\Database\DatabaseExceptionWrapper;
10 use Drupal\Core\Database\SchemaException;
11 use Drupal\Core\Entity\ContentEntityInterface;
12 use Drupal\Core\Entity\ContentEntityStorageBase;
13 use Drupal\Core\Entity\ContentEntityTypeInterface;
14 use Drupal\Core\Entity\EntityBundleListenerInterface;
15 use Drupal\Core\Entity\EntityInterface;
16 use Drupal\Core\Entity\EntityManagerInterface;
17 use Drupal\Core\Entity\EntityStorageException;
18 use Drupal\Core\Entity\EntityTypeInterface;
19 use Drupal\Core\Entity\Query\QueryInterface;
20 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
21 use Drupal\Core\Field\FieldDefinitionInterface;
22 use Drupal\Core\Field\FieldStorageDefinitionInterface;
23 use Drupal\Core\Language\LanguageInterface;
24 use Drupal\Core\Language\LanguageManagerInterface;
25 use Symfony\Component\DependencyInjection\ContainerInterface;
26
27 /**
28  * A content entity database storage implementation.
29  *
30  * This class can be used as-is by most content entity types. Entity types
31  * requiring special handling can extend the class.
32  *
33  * The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
34  * internally in order to automatically generate the database schema based on
35  * the defined base fields. Entity types can override the schema handler to
36  * customize the generated schema; e.g., to add additional indexes.
37  *
38  * @ingroup entity_api
39  */
40 class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
41
42   /**
43    * The mapping of field columns to SQL tables.
44    *
45    * @var \Drupal\Core\Entity\Sql\TableMappingInterface
46    */
47   protected $tableMapping;
48
49   /**
50    * Name of entity's revision database table field, if it supports revisions.
51    *
52    * Has the value FALSE if this entity does not use revisions.
53    *
54    * @var string
55    */
56   protected $revisionKey = FALSE;
57
58   /**
59    * The entity langcode key.
60    *
61    * @var string|bool
62    */
63   protected $langcodeKey = FALSE;
64
65   /**
66    * The default language entity key.
67    *
68    * @var string
69    */
70   protected $defaultLangcodeKey = FALSE;
71
72   /**
73    * The base table of the entity.
74    *
75    * @var string
76    */
77   protected $baseTable;
78
79   /**
80    * The table that stores revisions, if the entity supports revisions.
81    *
82    * @var string
83    */
84   protected $revisionTable;
85
86   /**
87    * The table that stores properties, if the entity has multilingual support.
88    *
89    * @var string
90    */
91   protected $dataTable;
92
93   /**
94    * The table that stores revision field data if the entity supports revisions.
95    *
96    * @var string
97    */
98   protected $revisionDataTable;
99
100   /**
101    * Active database connection.
102    *
103    * @var \Drupal\Core\Database\Connection
104    */
105   protected $database;
106
107   /**
108    * The entity type's storage schema object.
109    *
110    * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
111    */
112   protected $storageSchema;
113
114   /**
115    * The language manager.
116    *
117    * @var \Drupal\Core\Language\LanguageManagerInterface
118    */
119   protected $languageManager;
120
121   /**
122    * Whether this storage should use the temporary table mapping.
123    *
124    * @var bool
125    */
126   protected $temporary = FALSE;
127
128   /**
129    * {@inheritdoc}
130    */
131   public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
132     return new static(
133       $entity_type,
134       $container->get('database'),
135       $container->get('entity.manager'),
136       $container->get('cache.entity'),
137       $container->get('language_manager'),
138       $container->get('entity.memory_cache')
139     );
140   }
141
142   /**
143    * Gets the base field definitions for a content entity type.
144    *
145    * @return \Drupal\Core\Field\FieldDefinitionInterface[]
146    *   The array of base field definitions for the entity type, keyed by field
147    *   name.
148    */
149   public function getFieldStorageDefinitions() {
150     return $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
151   }
152
153   /**
154    * Constructs a SqlContentEntityStorage object.
155    *
156    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
157    *   The entity type definition.
158    * @param \Drupal\Core\Database\Connection $database
159    *   The database connection to be used.
160    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
161    *   The entity manager.
162    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
163    *   The cache backend to be used.
164    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
165    *   The language manager.
166    * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
167    *   The memory cache backend to be used.
168    */
169   public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL) {
170     parent::__construct($entity_type, $entity_manager, $cache, $memory_cache);
171     $this->database = $database;
172     $this->languageManager = $language_manager;
173     $this->initTableLayout();
174   }
175
176   /**
177    * Initializes table name variables.
178    */
179   protected function initTableLayout() {
180     // Reset table field values to ensure changes in the entity type definition
181     // are correctly reflected in the table layout.
182     $this->tableMapping = NULL;
183     $this->revisionKey = NULL;
184     $this->revisionTable = NULL;
185     $this->dataTable = NULL;
186     $this->revisionDataTable = NULL;
187
188     $table_mapping = $this->getTableMapping();
189     $this->baseTable = $table_mapping->getBaseTable();
190     $revisionable = $this->entityType->isRevisionable();
191     if ($revisionable) {
192       $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
193       $this->revisionTable = $table_mapping->getRevisionTable();
194     }
195     $translatable = $this->entityType->isTranslatable();
196     if ($translatable) {
197       $this->dataTable = $table_mapping->getDataTable();
198       $this->langcodeKey = $this->entityType->getKey('langcode');
199       $this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
200     }
201     if ($revisionable && $translatable) {
202       $this->revisionDataTable = $table_mapping->getRevisionDataTable();
203     }
204   }
205
206   /**
207    * Gets the base table name.
208    *
209    * @return string
210    *   The table name.
211    */
212   public function getBaseTable() {
213     return $this->baseTable;
214   }
215
216   /**
217    * Gets the revision table name.
218    *
219    * @return string|false
220    *   The table name or FALSE if it is not available.
221    */
222   public function getRevisionTable() {
223     return $this->revisionTable;
224   }
225
226   /**
227    * Gets the data table name.
228    *
229    * @return string|false
230    *   The table name or FALSE if it is not available.
231    */
232   public function getDataTable() {
233     return $this->dataTable;
234   }
235
236   /**
237    * Gets the revision data table name.
238    *
239    * @return string|false
240    *   The table name or FALSE if it is not available.
241    */
242   public function getRevisionDataTable() {
243     return $this->revisionDataTable;
244   }
245
246   /**
247    * Gets the entity type's storage schema object.
248    *
249    * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
250    *   The schema object.
251    */
252   protected function getStorageSchema() {
253     if (!isset($this->storageSchema)) {
254       $class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
255       $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
256     }
257     return $this->storageSchema;
258   }
259
260   /**
261    * Updates the wrapped entity type definition.
262    *
263    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
264    *   The update entity type.
265    *
266    * @internal Only to be used internally by Entity API. Expected to be
267    *   removed by https://www.drupal.org/node/2274017.
268    */
269   public function setEntityType(EntityTypeInterface $entity_type) {
270     if ($this->entityType->id() == $entity_type->id()) {
271       $this->entityType = $entity_type;
272       $this->initTableLayout();
273     }
274     else {
275       throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
276     }
277   }
278
279   /**
280    * Sets the wrapped table mapping definition.
281    *
282    * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
283    *   The table mapping.
284    *
285    * @internal Only to be used internally by Entity API. Expected to be removed
286    *   by https://www.drupal.org/node/2554235.
287    */
288   public function setTableMapping(TableMappingInterface $table_mapping) {
289     $this->tableMapping = $table_mapping;
290
291     $this->baseTable = $table_mapping->getBaseTable();
292     $this->revisionTable = $table_mapping->getRevisionTable();
293     $this->dataTable = $table_mapping->getDataTable();
294     $this->revisionDataTable = $table_mapping->getRevisionDataTable();
295   }
296
297   /**
298    * Changes the temporary state of the storage.
299    *
300    * @param bool $temporary
301    *   Whether to use a temporary table mapping or not.
302    *
303    * @internal Only to be used internally by Entity API.
304    */
305   public function setTemporary($temporary) {
306     $this->temporary = $temporary;
307   }
308
309   /**
310    * {@inheritdoc}
311    */
312   public function getTableMapping(array $storage_definitions = NULL) {
313     // If a new set of field storage definitions is passed, for instance when
314     // comparing old and new storage schema, we compute the table mapping
315     // without caching.
316     if ($storage_definitions) {
317       return $this->getCustomTableMapping($this->entityType, $storage_definitions);
318     }
319
320     // If we are using our internal storage definitions, which is our main use
321     // case, we can statically cache the computed table mapping.
322     if (!isset($this->tableMapping)) {
323       $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
324
325       $this->tableMapping = $this->getCustomTableMapping($this->entityType, $storage_definitions);
326     }
327
328     return $this->tableMapping;
329   }
330
331   /**
332    * Gets a table mapping for the specified entity type and storage definitions.
333    *
334    * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
335    *   An entity type definition.
336    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
337    *   An array of field storage definitions to be used to compute the table
338    *   mapping.
339    *
340    * @return \Drupal\Core\Entity\Sql\TableMappingInterface
341    *   A table mapping object for the entity's tables.
342    *
343    * @internal
344    */
345   public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
346     $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
347     return $table_mapping_class::create($entity_type, $storage_definitions);
348   }
349
350   /**
351    * {@inheritdoc}
352    */
353   protected function doLoadMultiple(array $ids = NULL) {
354     // Attempt to load entities from the persistent cache. This will remove IDs
355     // that were loaded from $ids.
356     $entities_from_cache = $this->getFromPersistentCache($ids);
357
358     // Load any remaining entities from the database.
359     if ($entities_from_storage = $this->getFromStorage($ids)) {
360       $this->invokeStorageLoadHook($entities_from_storage);
361       $this->setPersistentCache($entities_from_storage);
362     }
363
364     return $entities_from_cache + $entities_from_storage;
365   }
366
367   /**
368    * Gets entities from the storage.
369    *
370    * @param array|null $ids
371    *   If not empty, return entities that match these IDs. Return all entities
372    *   when NULL.
373    *
374    * @return \Drupal\Core\Entity\ContentEntityInterface[]
375    *   Array of entities from the storage.
376    */
377   protected function getFromStorage(array $ids = NULL) {
378     $entities = [];
379
380     if (!empty($ids)) {
381       // Sanitize IDs. Before feeding ID array into buildQuery, check whether
382       // it is empty as this would load all entities.
383       $ids = $this->cleanIds($ids);
384     }
385
386     if ($ids === NULL || $ids) {
387       // Build and execute the query.
388       $query_result = $this->buildQuery($ids)->execute();
389       $records = $query_result->fetchAllAssoc($this->idKey);
390
391       // Map the loaded records into entity objects and according fields.
392       if ($records) {
393         $entities = $this->mapFromStorageRecords($records);
394       }
395     }
396
397     return $entities;
398   }
399
400   /**
401    * Maps from storage records to entity objects, and attaches fields.
402    *
403    * @param array $records
404    *   Associative array of query results, keyed on the entity ID or revision
405    *   ID.
406    * @param bool $load_from_revision
407    *   (optional) Flag to indicate whether revisions should be loaded or not.
408    *   Defaults to FALSE.
409    *
410    * @return array
411    *   An array of entity objects implementing the EntityInterface.
412    */
413   protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
414     if (!$records) {
415       return [];
416     }
417
418     // Get the names of the fields that are stored in the base table and, if
419     // applicable, the revision table. Other entity data will be loaded in
420     // loadFromSharedTables() and loadFromDedicatedTables().
421     $field_names = $this->tableMapping->getFieldNames($this->baseTable);
422     if ($this->revisionTable) {
423       $field_names = array_unique(array_merge($field_names, $this->tableMapping->getFieldNames($this->revisionTable)));
424     }
425
426     $values = [];
427     foreach ($records as $id => $record) {
428       $values[$id] = [];
429       // Skip the item delta and item value levels (if possible) but let the
430       // field assign the value as suiting. This avoids unnecessary array
431       // hierarchies and saves memory here.
432       foreach ($field_names as $field_name) {
433         $field_columns = $this->tableMapping->getColumnNames($field_name);
434         // Handle field types that store several properties.
435         if (count($field_columns) > 1) {
436           foreach ($field_columns as $property_name => $column_name) {
437             if (property_exists($record, $column_name)) {
438               $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $record->{$column_name};
439               unset($record->{$column_name});
440             }
441           }
442         }
443         // Handle field types that store only one property.
444         else {
445           $column_name = reset($field_columns);
446           if (property_exists($record, $column_name)) {
447             $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT] = $record->{$column_name};
448             unset($record->{$column_name});
449           }
450         }
451       }
452
453       // Handle additional record entries that are not provided by an entity
454       // field, such as 'isDefaultRevision'.
455       foreach ($record as $name => $value) {
456         $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
457       }
458     }
459
460     // Initialize translations array.
461     $translations = array_fill_keys(array_keys($values), []);
462
463     // Load values from shared and dedicated tables.
464     $this->loadFromSharedTables($values, $translations, $load_from_revision);
465     $this->loadFromDedicatedTables($values, $load_from_revision);
466
467     $entities = [];
468     foreach ($values as $id => $entity_values) {
469       $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;
470       // Turn the record into an entity class.
471       $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
472     }
473
474     return $entities;
475   }
476
477   /**
478    * Loads values for fields stored in the shared data tables.
479    *
480    * @param array &$values
481    *   Associative array of entities values, keyed on the entity ID or the
482    *   revision ID.
483    * @param array &$translations
484    *   List of translations, keyed on the entity ID.
485    * @param bool $load_from_revision
486    *   Flag to indicate whether revisions should be loaded or not.
487    */
488   protected function loadFromSharedTables(array &$values, array &$translations, $load_from_revision) {
489     $record_key = !$load_from_revision ? $this->idKey : $this->revisionKey;
490     if ($this->dataTable) {
491       // If a revision table is available, we need all the properties of the
492       // latest revision. Otherwise we fall back to the data table.
493       $table = $this->revisionDataTable ?: $this->dataTable;
494       $alias = $this->revisionDataTable ? 'revision' : 'data';
495       $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
496         ->fields($alias)
497         ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
498         ->orderBy($alias . '.' . $record_key);
499
500       $table_mapping = $this->getTableMapping();
501       if ($this->revisionDataTable) {
502         // Find revisioned fields that are not entity keys. Exclude the langcode
503         // key as the base table holds only the default language.
504         $base_fields = array_diff($table_mapping->getFieldNames($this->baseTable), [$this->langcodeKey]);
505         $revisioned_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $base_fields);
506
507         // Find fields that are not revisioned or entity keys. Data fields have
508         // the same value regardless of entity revision.
509         $data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $revisioned_fields, $base_fields);
510         // If there are no data fields then only revisioned fields are needed
511         // else both data fields and revisioned fields are needed to map the
512         // entity values.
513         $all_fields = $revisioned_fields;
514         if ($data_fields) {
515           $all_fields = array_merge($revisioned_fields, $data_fields);
516           $query->leftJoin($this->dataTable, 'data', "(revision.$this->idKey = data.$this->idKey and revision.$this->langcodeKey = data.$this->langcodeKey)");
517           $column_names = [];
518           // Some fields can have more then one columns in the data table so
519           // column names are needed.
520           foreach ($data_fields as $data_field) {
521             // \Drupal\Core\Entity\Sql\TableMappingInterface::getColumnNames()
522             // returns an array keyed by property names so remove the keys
523             // before array_merge() to avoid losing data with fields having the
524             // same columns i.e. value.
525             $column_names = array_merge($column_names, array_values($table_mapping->getColumnNames($data_field)));
526           }
527           $query->fields('data', $column_names);
528         }
529
530         // Get the revision IDs.
531         $revision_ids = [];
532         foreach ($values as $entity_values) {
533           $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
534         }
535         $query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
536       }
537       else {
538         $all_fields = $table_mapping->getFieldNames($this->dataTable);
539       }
540
541       $result = $query->execute();
542       foreach ($result as $row) {
543         $id = $row[$record_key];
544
545         // Field values in default language are stored with
546         // LanguageInterface::LANGCODE_DEFAULT as key.
547         $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
548
549         $translations[$id][$langcode] = TRUE;
550
551         foreach ($all_fields as $field_name) {
552           $columns = $table_mapping->getColumnNames($field_name);
553           // Do not key single-column fields by property name.
554           if (count($columns) == 1) {
555             $values[$id][$field_name][$langcode] = $row[reset($columns)];
556           }
557           else {
558             foreach ($columns as $property_name => $column_name) {
559               $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
560             }
561           }
562         }
563       }
564     }
565   }
566
567   /**
568    * {@inheritdoc}
569    */
570   protected function doLoadRevisionFieldItems($revision_id) {
571     @trigger_error('"\Drupal\Core\Entity\ContentEntityStorageBase::doLoadRevisionFieldItems()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. "\Drupal\Core\Entity\ContentEntityStorageBase::doLoadMultipleRevisionsFieldItems()" should be implemented instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
572
573     $revisions = $this->doLoadMultipleRevisionsFieldItems([$revision_id]);
574
575     return !empty($revisions) ? reset($revisions) : NULL;
576   }
577
578   /**
579    * {@inheritdoc}
580    */
581   protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
582     $revisions = [];
583
584     // Sanitize IDs. Before feeding ID array into buildQuery, check whether
585     // it is empty as this would load all entity revisions.
586     $revision_ids = $this->cleanIds($revision_ids, 'revision');
587
588     if (!empty($revision_ids)) {
589       // Build and execute the query.
590       $query_result = $this->buildQuery(NULL, $revision_ids)->execute();
591       $records = $query_result->fetchAllAssoc($this->revisionKey);
592
593       // Map the loaded records into entity objects and according fields.
594       if ($records) {
595         $revisions = $this->mapFromStorageRecords($records, TRUE);
596       }
597     }
598
599     return $revisions;
600   }
601
602   /**
603    * {@inheritdoc}
604    */
605   protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
606     $this->database->delete($this->revisionTable)
607       ->condition($this->revisionKey, $revision->getRevisionId())
608       ->execute();
609
610     if ($this->revisionDataTable) {
611       $this->database->delete($this->revisionDataTable)
612         ->condition($this->revisionKey, $revision->getRevisionId())
613         ->execute();
614     }
615
616     $this->deleteRevisionFromDedicatedTables($revision);
617   }
618
619   /**
620    * {@inheritdoc}
621    */
622   protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
623     if ($this->dataTable) {
624       // @todo We should not be using a condition to specify whether conditions
625       //   apply to the default language. See
626       //   https://www.drupal.org/node/1866330.
627       // Default to the original entity language if not explicitly specified
628       // otherwise.
629       if (!array_key_exists($this->defaultLangcodeKey, $values)) {
630         $values[$this->defaultLangcodeKey] = 1;
631       }
632       // If the 'default_langcode' flag is explicitly not set, we do not care
633       // whether the queried values are in the original entity language or not.
634       elseif ($values[$this->defaultLangcodeKey] === NULL) {
635         unset($values[$this->defaultLangcodeKey]);
636       }
637     }
638
639     parent::buildPropertyQuery($entity_query, $values);
640   }
641
642   /**
643    * Builds the query to load the entity.
644    *
645    * This has full revision support. For entities requiring special queries,
646    * the class can be extended, and the default query can be constructed by
647    * calling parent::buildQuery(). This is usually necessary when the object
648    * being loaded needs to be augmented with additional data from another
649    * table, such as loading node type into comments or vocabulary machine name
650    * into terms, however it can also support $conditions on different tables.
651    * See Drupal\comment\CommentStorage::buildQuery() for an example.
652    *
653    * @param array|null $ids
654    *   An array of entity IDs, or NULL to load all entities.
655    * @param array|bool $revision_ids
656    *   The IDs of the revisions to load, or FALSE if this query is asking for
657    *   the default revisions. Defaults to FALSE.
658    *
659    * @return \Drupal\Core\Database\Query\Select
660    *   A SelectQuery object for loading the entity.
661    */
662   protected function buildQuery($ids, $revision_ids = FALSE) {
663     $query = $this->database->select($this->baseTable, 'base');
664
665     $query->addTag($this->entityTypeId . '_load_multiple');
666
667     if ($revision_ids) {
668       if (!is_array($revision_ids)) {
669         @trigger_error('Passing a single revision ID to "\Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery()" is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. An array of revision IDs should be given instead. See https://www.drupal.org/node/2924915.', E_USER_DEPRECATED);
670       }
671       $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} IN (:revisionIds[])", [':revisionIds[]' => (array) $revision_ids]);
672     }
673     elseif ($this->revisionTable) {
674       $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
675     }
676
677     // Add fields from the {entity} table.
678     $table_mapping = $this->getTableMapping();
679     $entity_fields = $table_mapping->getAllColumns($this->baseTable);
680
681     if ($this->revisionTable) {
682       // Add all fields from the {entity_revision} table.
683       $entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
684       $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
685       // The ID field is provided by entity, so remove it.
686       unset($entity_revision_fields[$this->idKey]);
687
688       // Remove all fields from the base table that are also fields by the same
689       // name in the revision table.
690       $entity_field_keys = array_flip($entity_fields);
691       foreach ($entity_revision_fields as $name) {
692         if (isset($entity_field_keys[$name])) {
693           unset($entity_fields[$entity_field_keys[$name]]);
694         }
695       }
696       $query->fields('revision', $entity_revision_fields);
697
698       // Compare revision ID of the base and revision table, if equal then this
699       // is the default revision.
700       $query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
701     }
702
703     $query->fields('base', $entity_fields);
704
705     if ($ids) {
706       $query->condition("base.{$this->idKey}", $ids, 'IN');
707     }
708
709     return $query;
710   }
711
712   /**
713    * {@inheritdoc}
714    */
715   public function delete(array $entities) {
716     if (!$entities) {
717       // If no IDs or invalid IDs were passed, do nothing.
718       return;
719     }
720
721     $transaction = $this->database->startTransaction();
722     try {
723       parent::delete($entities);
724
725       // Ignore replica server temporarily.
726       db_ignore_replica();
727     }
728     catch (\Exception $e) {
729       $transaction->rollBack();
730       watchdog_exception($this->entityTypeId, $e);
731       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
732     }
733   }
734
735   /**
736    * {@inheritdoc}
737    */
738   protected function doDeleteFieldItems($entities) {
739     $ids = array_keys($entities);
740
741     $this->database->delete($this->baseTable)
742       ->condition($this->idKey, $ids, 'IN')
743       ->execute();
744
745     if ($this->revisionTable) {
746       $this->database->delete($this->revisionTable)
747         ->condition($this->idKey, $ids, 'IN')
748         ->execute();
749     }
750
751     if ($this->dataTable) {
752       $this->database->delete($this->dataTable)
753         ->condition($this->idKey, $ids, 'IN')
754         ->execute();
755     }
756
757     if ($this->revisionDataTable) {
758       $this->database->delete($this->revisionDataTable)
759         ->condition($this->idKey, $ids, 'IN')
760         ->execute();
761     }
762
763     foreach ($entities as $entity) {
764       $this->deleteFromDedicatedTables($entity);
765     }
766   }
767
768   /**
769    * {@inheritdoc}
770    */
771   public function save(EntityInterface $entity) {
772     $transaction = $this->database->startTransaction();
773     try {
774       $return = parent::save($entity);
775
776       // Ignore replica server temporarily.
777       db_ignore_replica();
778       return $return;
779     }
780     catch (\Exception $e) {
781       $transaction->rollBack();
782       watchdog_exception($this->entityTypeId, $e);
783       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
784     }
785   }
786
787   /**
788    * {@inheritdoc}
789    */
790   protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
791     $full_save = empty($names);
792     $update = !$full_save || !$entity->isNew();
793
794     if ($full_save) {
795       $shared_table_fields = TRUE;
796       $dedicated_table_fields = TRUE;
797     }
798     else {
799       $table_mapping = $this->getTableMapping();
800       $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
801       $shared_table_fields = FALSE;
802       $dedicated_table_fields = [];
803
804       // Collect the name of fields to be written in dedicated tables and check
805       // whether shared table records need to be updated.
806       foreach ($names as $name) {
807         $storage_definition = $storage_definitions[$name];
808         if ($table_mapping->allowsSharedTableStorage($storage_definition)) {
809           $shared_table_fields = TRUE;
810         }
811         elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
812           $dedicated_table_fields[] = $name;
813         }
814       }
815     }
816
817     // Update shared table records if necessary.
818     if ($shared_table_fields) {
819       $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
820       // Create the storage record to be saved.
821       if ($update) {
822         $default_revision = $entity->isDefaultRevision();
823         if ($default_revision) {
824           $this->database
825             ->update($this->baseTable)
826             ->fields((array) $record)
827             ->condition($this->idKey, $record->{$this->idKey})
828             ->execute();
829         }
830         if ($this->revisionTable) {
831           if ($full_save) {
832             $entity->{$this->revisionKey} = $this->saveRevision($entity);
833           }
834           else {
835             $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
836             $entity->preSaveRevision($this, $record);
837             $this->database
838               ->update($this->revisionTable)
839               ->fields((array) $record)
840               ->condition($this->revisionKey, $record->{$this->revisionKey})
841               ->execute();
842           }
843         }
844         if ($default_revision && $this->dataTable) {
845           $this->saveToSharedTables($entity);
846         }
847         if ($this->revisionDataTable) {
848           $new_revision = $full_save && $entity->isNewRevision();
849           $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
850         }
851       }
852       else {
853         $insert_id = $this->database
854           ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
855           ->fields((array) $record)
856           ->execute();
857         // Even if this is a new entity the ID key might have been set, in which
858         // case we should not override the provided ID. An ID key that is not set
859         // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
860         if (!isset($record->{$this->idKey})) {
861           $record->{$this->idKey} = $insert_id;
862         }
863         $entity->{$this->idKey} = (string) $record->{$this->idKey};
864         if ($this->revisionTable) {
865           $record->{$this->revisionKey} = $this->saveRevision($entity);
866         }
867         if ($this->dataTable) {
868           $this->saveToSharedTables($entity);
869         }
870         if ($this->revisionDataTable) {
871           $this->saveToSharedTables($entity, $this->revisionDataTable);
872         }
873       }
874     }
875
876     // Update dedicated table records if necessary.
877     if ($dedicated_table_fields) {
878       $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
879       $this->saveToDedicatedTables($entity, $update, $names);
880     }
881   }
882
883   /**
884    * {@inheritdoc}
885    */
886   protected function has($id, EntityInterface $entity) {
887     return !$entity->isNew();
888   }
889
890   /**
891    * Saves fields that use the shared tables.
892    *
893    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
894    *   The entity object.
895    * @param string $table_name
896    *   (optional) The table name to save to. Defaults to the data table.
897    * @param bool $new_revision
898    *   (optional) Whether we are dealing with a new revision. By default fetches
899    *   the information from the entity object.
900    */
901   protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
902     if (!isset($table_name)) {
903       $table_name = $this->dataTable;
904     }
905     if (!isset($new_revision)) {
906       $new_revision = $entity->isNewRevision();
907     }
908     $revision = $table_name != $this->dataTable;
909
910     if (!$revision || !$new_revision) {
911       $key = $revision ? $this->revisionKey : $this->idKey;
912       $value = $revision ? $entity->getRevisionId() : $entity->id();
913       // Delete and insert to handle removed values.
914       $this->database->delete($table_name)
915         ->condition($key, $value)
916         ->execute();
917     }
918
919     $query = $this->database->insert($table_name);
920
921     foreach ($entity->getTranslationLanguages() as $langcode => $language) {
922       $translation = $entity->getTranslation($langcode);
923       $record = $this->mapToDataStorageRecord($translation, $table_name);
924       $values = (array) $record;
925       $query
926         ->fields(array_keys($values))
927         ->values($values);
928     }
929
930     $query->execute();
931   }
932
933   /**
934    * Maps from an entity object to the storage record.
935    *
936    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
937    *   The entity object.
938    * @param string $table_name
939    *   (optional) The table name to map records to. Defaults to the base table.
940    *
941    * @return \stdClass
942    *   The record to store.
943    */
944   protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
945     if (!isset($table_name)) {
946       $table_name = $this->baseTable;
947     }
948
949     $record = new \stdClass();
950     $table_mapping = $this->getTableMapping();
951     foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
952
953       if (empty($this->getFieldStorageDefinitions()[$field_name])) {
954         throw new EntityStorageException("Table mapping contains invalid field $field_name.");
955       }
956       $definition = $this->getFieldStorageDefinitions()[$field_name];
957       $columns = $table_mapping->getColumnNames($field_name);
958
959       foreach ($columns as $column_name => $schema_name) {
960         // If there is no main property and only a single column, get all
961         // properties from the first field item and assume that they will be
962         // stored serialized.
963         // @todo Give field types more control over this behavior in
964         //   https://www.drupal.org/node/2232427.
965         if (!$definition->getMainPropertyName() && count($columns) == 1) {
966           $value = ($item = $entity->$field_name->first()) ? $item->getValue() : [];
967         }
968         else {
969           $value = isset($entity->$field_name->$column_name) ? $entity->$field_name->$column_name : NULL;
970         }
971         if (!empty($definition->getSchema()['columns'][$column_name]['serialize'])) {
972           $value = serialize($value);
973         }
974
975         // Do not set serial fields if we do not have a value. This supports all
976         // SQL database drivers.
977         // @see https://www.drupal.org/node/2279395
978         $value = drupal_schema_get_field_value($definition->getSchema()['columns'][$column_name], $value);
979         if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) {
980           $record->$schema_name = $value;
981         }
982       }
983     }
984
985     return $record;
986   }
987
988   /**
989    * Checks whether a field column should be treated as serial.
990    *
991    * @param $table_name
992    *   The name of the table the field column belongs to.
993    * @param $schema_name
994    *   The schema name of the field column.
995    *
996    * @return bool
997    *   TRUE if the column is serial, FALSE otherwise.
998    *
999    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
1000    * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
1001    */
1002   protected function isColumnSerial($table_name, $schema_name) {
1003     $result = FALSE;
1004
1005     switch ($table_name) {
1006       case $this->baseTable:
1007         $result = $schema_name == $this->idKey;
1008         break;
1009
1010       case $this->revisionTable:
1011         $result = $schema_name == $this->revisionKey;
1012         break;
1013     }
1014
1015     return $result;
1016   }
1017
1018   /**
1019    * Maps from an entity object to the storage record of the field data.
1020    *
1021    * @param \Drupal\Core\Entity\EntityInterface $entity
1022    *   The entity object.
1023    * @param string $table_name
1024    *   (optional) The table name to map records to. Defaults to the data table.
1025    *
1026    * @return \stdClass
1027    *   The record to store.
1028    */
1029   protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
1030     if (!isset($table_name)) {
1031       $table_name = $this->dataTable;
1032     }
1033     $record = $this->mapToStorageRecord($entity, $table_name);
1034     return $record;
1035   }
1036
1037   /**
1038    * Saves an entity revision.
1039    *
1040    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1041    *   The entity object.
1042    *
1043    * @return int
1044    *   The revision id.
1045    */
1046   protected function saveRevision(ContentEntityInterface $entity) {
1047     $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
1048
1049     $entity->preSaveRevision($this, $record);
1050
1051     if ($entity->isNewRevision()) {
1052       $insert_id = $this->database
1053         ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
1054         ->fields((array) $record)
1055         ->execute();
1056       // Even if this is a new revision, the revision ID key might have been
1057       // set in which case we should not override the provided revision ID.
1058       if (!isset($record->{$this->revisionKey})) {
1059         $record->{$this->revisionKey} = $insert_id;
1060       }
1061       if ($entity->isDefaultRevision()) {
1062         $this->database->update($this->baseTable)
1063           ->fields([$this->revisionKey => $record->{$this->revisionKey}])
1064           ->condition($this->idKey, $record->{$this->idKey})
1065           ->execute();
1066       }
1067     }
1068     else {
1069       $this->database
1070         ->update($this->revisionTable)
1071         ->fields((array) $record)
1072         ->condition($this->revisionKey, $record->{$this->revisionKey})
1073         ->execute();
1074     }
1075
1076     // Make sure to update the new revision key for the entity.
1077     $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
1078
1079     return $record->{$this->revisionKey};
1080   }
1081
1082   /**
1083    * {@inheritdoc}
1084    */
1085   protected function getQueryServiceName() {
1086     return 'entity.query.sql';
1087   }
1088
1089   /**
1090    * Loads values of fields stored in dedicated tables for a group of entities.
1091    *
1092    * @param array &$values
1093    *   An array of values keyed by entity ID.
1094    * @param bool $load_from_revision
1095    *   Flag to indicate whether revisions should be loaded or not.
1096    */
1097   protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
1098     if (empty($values)) {
1099       return;
1100     }
1101
1102     // Collect entities ids, bundles and languages.
1103     $bundles = [];
1104     $ids = [];
1105     $default_langcodes = [];
1106     foreach ($values as $key => $entity_values) {
1107       $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
1108       $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
1109       if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
1110         $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
1111       }
1112     }
1113
1114     // Collect impacted fields.
1115     $storage_definitions = [];
1116     $definitions = [];
1117     $table_mapping = $this->getTableMapping();
1118     foreach ($bundles as $bundle => $v) {
1119       $definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
1120       foreach ($definitions[$bundle] as $field_name => $field_definition) {
1121         $storage_definition = $field_definition->getFieldStorageDefinition();
1122         if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1123           $storage_definitions[$field_name] = $storage_definition;
1124         }
1125       }
1126     }
1127
1128     // Load field data.
1129     $langcodes = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
1130     foreach ($storage_definitions as $field_name => $storage_definition) {
1131       $table = !$load_from_revision ? $table_mapping->getDedicatedDataTableName($storage_definition) : $table_mapping->getDedicatedRevisionTableName($storage_definition);
1132
1133       // Ensure that only values having valid languages are retrieved. Since we
1134       // are loading values for multiple entities, we cannot limit the query to
1135       // the available translations.
1136       $results = $this->database->select($table, 't')
1137         ->fields('t')
1138         ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
1139         ->condition('deleted', 0)
1140         ->condition('langcode', $langcodes, 'IN')
1141         ->orderBy('delta')
1142         ->execute();
1143
1144       foreach ($results as $row) {
1145         $bundle = $row->bundle;
1146
1147         $value_key = !$load_from_revision ? $row->entity_id : $row->revision_id;
1148         // Field values in default language are stored with
1149         // LanguageInterface::LANGCODE_DEFAULT as key.
1150         $langcode = LanguageInterface::LANGCODE_DEFAULT;
1151         if ($this->langcodeKey && isset($default_langcodes[$value_key]) && $row->langcode != $default_langcodes[$value_key]) {
1152           $langcode = $row->langcode;
1153         }
1154
1155         if (!isset($values[$value_key][$field_name][$langcode])) {
1156           $values[$value_key][$field_name][$langcode] = [];
1157         }
1158
1159         // Ensure that records for non-translatable fields having invalid
1160         // languages are skipped.
1161         if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]->isTranslatable()) {
1162           if ($storage_definition->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$value_key][$field_name][$langcode]) < $storage_definition->getCardinality()) {
1163             $item = [];
1164             // For each column declared by the field, populate the item from the
1165             // prefixed database column.
1166             foreach ($storage_definition->getColumns() as $column => $attributes) {
1167               $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1168               // Unserialize the value if specified in the column schema.
1169               $item[$column] = (!empty($attributes['serialize'])) ? unserialize($row->$column_name) : $row->$column_name;
1170             }
1171
1172             // Add the item to the field values for the entity.
1173             $values[$value_key][$field_name][$langcode][] = $item;
1174           }
1175         }
1176       }
1177     }
1178   }
1179
1180   /**
1181    * Saves values of fields that use dedicated tables.
1182    *
1183    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1184    *   The entity.
1185    * @param bool $update
1186    *   TRUE if the entity is being updated, FALSE if it is being inserted.
1187    * @param string[] $names
1188    *   (optional) The names of the fields to be stored. Defaults to all the
1189    *   available fields.
1190    */
1191   protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = []) {
1192     $vid = $entity->getRevisionId();
1193     $id = $entity->id();
1194     $bundle = $entity->bundle();
1195     $entity_type = $entity->getEntityTypeId();
1196     $default_langcode = $entity->getUntranslated()->language()->getId();
1197     $translation_langcodes = array_keys($entity->getTranslationLanguages());
1198     $table_mapping = $this->getTableMapping();
1199
1200     if (!isset($vid)) {
1201       $vid = $id;
1202     }
1203
1204     $original = !empty($entity->original) ? $entity->original : NULL;
1205
1206     // Determine which fields should be actually stored.
1207     $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
1208     if ($names) {
1209       $definitions = array_intersect_key($definitions, array_flip($names));
1210     }
1211
1212     foreach ($definitions as $field_name => $field_definition) {
1213       $storage_definition = $field_definition->getFieldStorageDefinition();
1214       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1215         continue;
1216       }
1217
1218       // When updating an existing revision, keep the existing records if the
1219       // field values did not change.
1220       if (!$entity->isNewRevision() && $original && !$this->hasFieldValueChanged($field_definition, $entity, $original)) {
1221         continue;
1222       }
1223
1224       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1225       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1226
1227       // Delete and insert, rather than update, in case a value was added.
1228       if ($update) {
1229         // Only overwrite the field's base table if saving the default revision
1230         // of an entity.
1231         if ($entity->isDefaultRevision()) {
1232           $this->database->delete($table_name)
1233             ->condition('entity_id', $id)
1234             ->execute();
1235         }
1236         if ($this->entityType->isRevisionable()) {
1237           $this->database->delete($revision_name)
1238             ->condition('entity_id', $id)
1239             ->condition('revision_id', $vid)
1240             ->execute();
1241         }
1242       }
1243
1244       // Prepare the multi-insert query.
1245       $do_insert = FALSE;
1246       $columns = ['entity_id', 'revision_id', 'bundle', 'delta', 'langcode'];
1247       foreach ($storage_definition->getColumns() as $column => $attributes) {
1248         $columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
1249       }
1250       $query = $this->database->insert($table_name)->fields($columns);
1251       if ($this->entityType->isRevisionable()) {
1252         $revision_query = $this->database->insert($revision_name)->fields($columns);
1253       }
1254
1255       $langcodes = $field_definition->isTranslatable() ? $translation_langcodes : [$default_langcode];
1256       foreach ($langcodes as $langcode) {
1257         $delta_count = 0;
1258         $items = $entity->getTranslation($langcode)->get($field_name);
1259         $items->filterEmptyItems();
1260         foreach ($items as $delta => $item) {
1261           // We now know we have something to insert.
1262           $do_insert = TRUE;
1263           $record = [
1264             'entity_id' => $id,
1265             'revision_id' => $vid,
1266             'bundle' => $bundle,
1267             'delta' => $delta,
1268             'langcode' => $langcode,
1269           ];
1270           foreach ($storage_definition->getColumns() as $column => $attributes) {
1271             $column_name = $table_mapping->getFieldColumnName($storage_definition, $column);
1272             // Serialize the value if specified in the column schema.
1273             $value = $item->$column;
1274             if (!empty($attributes['serialize'])) {
1275               $value = serialize($value);
1276             }
1277             $record[$column_name] = drupal_schema_get_field_value($attributes, $value);
1278           }
1279           $query->values($record);
1280           if ($this->entityType->isRevisionable()) {
1281             $revision_query->values($record);
1282           }
1283
1284           if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
1285             break;
1286           }
1287         }
1288       }
1289
1290       // Execute the query if we have values to insert.
1291       if ($do_insert) {
1292         // Only overwrite the field's base table if saving the default revision
1293         // of an entity.
1294         if ($entity->isDefaultRevision()) {
1295           $query->execute();
1296         }
1297         if ($this->entityType->isRevisionable()) {
1298           $revision_query->execute();
1299         }
1300       }
1301     }
1302   }
1303
1304   /**
1305    * Deletes values of fields in dedicated tables for all revisions.
1306    *
1307    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1308    *   The entity.
1309    */
1310   protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
1311     $table_mapping = $this->getTableMapping();
1312     foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1313       $storage_definition = $field_definition->getFieldStorageDefinition();
1314       if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1315         continue;
1316       }
1317       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1318       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1319       $this->database->delete($table_name)
1320         ->condition('entity_id', $entity->id())
1321         ->execute();
1322       if ($this->entityType->isRevisionable()) {
1323         $this->database->delete($revision_name)
1324           ->condition('entity_id', $entity->id())
1325           ->execute();
1326       }
1327     }
1328   }
1329
1330   /**
1331    * Deletes values of fields in dedicated tables for all revisions.
1332    *
1333    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
1334    *   The entity. It must have a revision ID.
1335    */
1336   protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
1337     $vid = $entity->getRevisionId();
1338     if (isset($vid)) {
1339       $table_mapping = $this->getTableMapping();
1340       foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
1341         $storage_definition = $field_definition->getFieldStorageDefinition();
1342         if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1343           continue;
1344         }
1345         $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1346         $this->database->delete($revision_name)
1347           ->condition('entity_id', $entity->id())
1348           ->condition('revision_id', $vid)
1349           ->execute();
1350       }
1351     }
1352   }
1353
1354   /**
1355    * {@inheritdoc}
1356    */
1357   public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1358     return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
1359   }
1360
1361   /**
1362    * {@inheritdoc}
1363    */
1364   public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1365     return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
1366   }
1367
1368   /**
1369    * {@inheritdoc}
1370    */
1371   public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1372     return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
1373   }
1374
1375   /**
1376    * {@inheritdoc}
1377    */
1378   public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1379     return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
1380   }
1381
1382   /**
1383    * {@inheritdoc}
1384    */
1385   public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
1386     $this->wrapSchemaException(function () use ($entity_type) {
1387       $this->getStorageSchema()->onEntityTypeCreate($entity_type);
1388     });
1389   }
1390
1391   /**
1392    * {@inheritdoc}
1393    */
1394   public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
1395     // Ensure we have an updated entity type definition.
1396     $this->entityType = $entity_type;
1397     // The table layout may have changed depending on the new entity type
1398     // definition.
1399     $this->initTableLayout();
1400     // Let the schema handler adapt to possible table layout changes.
1401     $this->wrapSchemaException(function () use ($entity_type, $original) {
1402       $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
1403     });
1404   }
1405
1406   /**
1407    * {@inheritdoc}
1408    */
1409   public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
1410     $this->wrapSchemaException(function () use ($entity_type) {
1411       $this->getStorageSchema()->onEntityTypeDelete($entity_type);
1412     });
1413   }
1414
1415   /**
1416    * {@inheritdoc}
1417    */
1418   public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
1419     $this->wrapSchemaException(function () use ($storage_definition) {
1420       $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
1421     });
1422   }
1423
1424   /**
1425    * {@inheritdoc}
1426    */
1427   public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
1428     $this->wrapSchemaException(function () use ($storage_definition, $original) {
1429       $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
1430     });
1431   }
1432
1433   /**
1434    * {@inheritdoc}
1435    */
1436   public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
1437     $table_mapping = $this->getTableMapping(
1438       $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
1439     );
1440
1441     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1442       // Mark all data associated with the field for deletion.
1443       $table = $table_mapping->getDedicatedDataTableName($storage_definition);
1444       $revision_table = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1445       $this->database->update($table)
1446         ->fields(['deleted' => 1])
1447         ->execute();
1448       if ($this->entityType->isRevisionable()) {
1449         $this->database->update($revision_table)
1450           ->fields(['deleted' => 1])
1451           ->execute();
1452       }
1453     }
1454
1455     // Update the field schema.
1456     $this->wrapSchemaException(function () use ($storage_definition) {
1457       $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
1458     });
1459   }
1460
1461   /**
1462    * Wraps a database schema exception into an entity storage exception.
1463    *
1464    * @param callable $callback
1465    *   The callback to be executed.
1466    *
1467    * @throws \Drupal\Core\Entity\EntityStorageException
1468    *   When a database schema exception is thrown.
1469    */
1470   protected function wrapSchemaException(callable $callback) {
1471     $message = 'Exception thrown while performing a schema update.';
1472     try {
1473       $callback();
1474     }
1475     catch (SchemaException $e) {
1476       $message .= ' ' . $e->getMessage();
1477       throw new EntityStorageException($message, 0, $e);
1478     }
1479     catch (DatabaseExceptionWrapper $e) {
1480       $message .= ' ' . $e->getMessage();
1481       throw new EntityStorageException($message, 0, $e);
1482     }
1483   }
1484
1485   /**
1486    * {@inheritdoc}
1487    */
1488   public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
1489     $table_mapping = $this->getTableMapping();
1490     $storage_definition = $field_definition->getFieldStorageDefinition();
1491     // Mark field data as deleted.
1492     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1493       $table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
1494       $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
1495       $this->database->update($table_name)
1496         ->fields(['deleted' => 1])
1497         ->condition('bundle', $field_definition->getTargetBundle())
1498         ->execute();
1499       if ($this->entityType->isRevisionable()) {
1500         $this->database->update($revision_name)
1501           ->fields(['deleted' => 1])
1502           ->condition('bundle', $field_definition->getTargetBundle())
1503           ->execute();
1504       }
1505     }
1506   }
1507
1508   /**
1509    * {@inheritdoc}
1510    */
1511   public function onBundleCreate($bundle, $entity_type_id) {}
1512
1513   /**
1514    * {@inheritdoc}
1515    */
1516   public function onBundleDelete($bundle, $entity_type_id) {}
1517
1518   /**
1519    * {@inheritdoc}
1520    */
1521   protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
1522     // Check whether the whole field storage definition is gone, or just some
1523     // bundle fields.
1524     $storage_definition = $field_definition->getFieldStorageDefinition();
1525     $table_mapping = $this->getTableMapping();
1526     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
1527
1528     // Get the entities which we want to purge first.
1529     $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
1530     $or = $entity_query->orConditionGroup();
1531     foreach ($storage_definition->getColumns() as $column_name => $data) {
1532       $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1533     }
1534     $entity_query
1535       ->distinct(TRUE)
1536       ->fields('t', ['entity_id'])
1537       ->condition('bundle', $field_definition->getTargetBundle())
1538       ->range(0, $batch_size);
1539
1540     // Create a map of field data table column names to field column names.
1541     $column_map = [];
1542     foreach ($storage_definition->getColumns() as $column_name => $data) {
1543       $column_map[$table_mapping->getFieldColumnName($storage_definition, $column_name)] = $column_name;
1544     }
1545
1546     $entities = [];
1547     $items_by_entity = [];
1548     foreach ($entity_query->execute() as $row) {
1549       $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
1550         ->fields('t')
1551         ->condition('entity_id', $row['entity_id'])
1552         ->condition('deleted', 1)
1553         ->orderBy('delta');
1554
1555       foreach ($item_query->execute() as $item_row) {
1556         if (!isset($entities[$item_row['revision_id']])) {
1557           // Create entity with the right revision id and entity id combination.
1558           $item_row['entity_type'] = $this->entityTypeId;
1559           // @todo: Replace this by an entity object created via an entity
1560           // factory, see https://www.drupal.org/node/1867228.
1561           $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
1562         }
1563         $item = [];
1564         foreach ($column_map as $db_column => $field_column) {
1565           $item[$field_column] = $item_row[$db_column];
1566         }
1567         $items_by_entity[$item_row['revision_id']][] = $item;
1568       }
1569     }
1570
1571     // Create field item objects and return.
1572     foreach ($items_by_entity as $revision_id => $values) {
1573       $entity_adapter = $entities[$revision_id]->getTypedData();
1574       $items_by_entity[$revision_id] = \Drupal::typedDataManager()->create($field_definition, $values, $field_definition->getName(), $entity_adapter);
1575     }
1576     return $items_by_entity;
1577   }
1578
1579   /**
1580    * {@inheritdoc}
1581    */
1582   protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
1583     $storage_definition = $field_definition->getFieldStorageDefinition();
1584     $is_deleted = $storage_definition->isDeleted();
1585     $table_mapping = $this->getTableMapping();
1586     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1587     $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1588     $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id();
1589     $this->database->delete($table_name)
1590       ->condition('revision_id', $revision_id)
1591       ->condition('deleted', 1)
1592       ->execute();
1593     if ($this->entityType->isRevisionable()) {
1594       $this->database->delete($revision_name)
1595         ->condition('revision_id', $revision_id)
1596         ->condition('deleted', 1)
1597         ->execute();
1598     }
1599   }
1600
1601   /**
1602    * {@inheritdoc}
1603    */
1604   public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
1605     $this->getStorageSchema()->finalizePurge($storage_definition);
1606   }
1607
1608   /**
1609    * {@inheritdoc}
1610    */
1611   public function countFieldData($storage_definition, $as_bool = FALSE) {
1612     // The table mapping contains stale data during a request when a field
1613     // storage definition is added, so bypass the internal storage definitions
1614     // and fetch the table mapping using the passed in storage definition.
1615     // @todo Fix this in https://www.drupal.org/node/2705205.
1616     $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
1617     $storage_definitions[$storage_definition->getName()] = $storage_definition;
1618     $table_mapping = $this->getTableMapping($storage_definitions);
1619
1620     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
1621       $is_deleted = $storage_definition->isDeleted();
1622       if ($this->entityType->isRevisionable()) {
1623         $table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted);
1624       }
1625       else {
1626         $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted);
1627       }
1628       $query = $this->database->select($table_name, 't');
1629       $or = $query->orConditionGroup();
1630       foreach ($storage_definition->getColumns() as $column_name => $data) {
1631         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
1632       }
1633       $query->condition($or);
1634       if (!$as_bool) {
1635         $query
1636           ->fields('t', ['entity_id'])
1637           ->distinct(TRUE);
1638       }
1639     }
1640     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
1641       // Ascertain the table this field is mapped too.
1642       $field_name = $storage_definition->getName();
1643       $table_name = $table_mapping->getFieldTableName($field_name);
1644       $query = $this->database->select($table_name, 't');
1645       $or = $query->orConditionGroup();
1646       foreach (array_keys($storage_definition->getColumns()) as $property_name) {
1647         $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
1648       }
1649       $query->condition($or);
1650       if (!$as_bool) {
1651         $query
1652           ->fields('t', [$this->idKey])
1653           ->distinct(TRUE);
1654       }
1655     }
1656
1657     // @todo Find a way to count field data also for fields having custom
1658     //   storage. See https://www.drupal.org/node/2337753.
1659     $count = 0;
1660     if (isset($query)) {
1661       // If we are performing the query just to check if the field has data
1662       // limit the number of rows.
1663       if ($as_bool) {
1664         $query
1665           ->range(0, 1)
1666           ->addExpression('1');
1667       }
1668       else {
1669         // Otherwise count the number of rows.
1670         $query = $query->countQuery();
1671       }
1672       $count = $query->execute()->fetchField();
1673     }
1674     return $as_bool ? (bool) $count : (int) $count;
1675   }
1676
1677   /**
1678    * Determines whether the passed field has been already deleted.
1679    *
1680    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
1681    *   The field storage definition.
1682    *
1683    * @return bool
1684    *   Whether the field has been already deleted.
1685    *
1686    * @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use
1687    *   \Drupal\Core\Field\FieldStorageDefinitionInterface::isDeleted() instead.
1688    *
1689    * @see https://www.drupal.org/node/2907785
1690    */
1691   protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
1692     return $storage_definition->isDeleted();
1693   }
1694
1695 }