DatabaseStorageControllerNG.php 19 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Entity\DatabaseStorageControllerNG.
6 7 8 9
 */

namespace Drupal\Core\Entity;

10
use Drupal\Core\Language\Language;
11
use Drupal\field\FieldInfo;
12
use Drupal\Core\Entity\Query\QueryInterface;
13 14 15
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityStorageException;
16
use Drupal\Component\Uuid\UuidInterface;
17
use Drupal\Core\Database\Connection;
18 19 20 21 22 23

/**
 * Implements Field API specific enhancements to the DatabaseStorageController class.
 *
 * @todo: Once all entity types have been converted, merge improvements into the
 * DatabaseStorageController class.
24
 *
25
 * @see \Drupal\Core\ContentEntityBase
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
 */
class DatabaseStorageControllerNG extends DatabaseStorageController {

  /**
   * The entity class to use.
   *
   * @var string
   */
  protected $entityClass;

  /**
   * The entity bundle key.
   *
   * @var string|bool
   */
  protected $bundleKey;

43 44 45 46 47 48 49
  /**
   * The table that stores properties, if the entity has multilingual support.
   *
   * @var string
   */
  protected $dataTable;

50 51 52 53 54 55 56
  /**
   * The table that stores revision field data if the entity supports revisions.
   *
   * @var string
   */
  protected $revisionDataTable;

57 58 59
  /**
   * Overrides DatabaseStorageController::__construct().
   */
60 61
  public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service) {
    parent::__construct($entity_type,$entity_info, $database, $field_info, $uuid_service);
62 63
    $this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE;
    $this->entityClass = $this->entityInfo['class'];
64

65 66 67
    // Check if the entity type has a dedicated table for properties.
    if (!empty($this->entityInfo['data_table'])) {
      $this->dataTable = $this->entityInfo['data_table'];
68 69 70 71 72
      // Entity types having both revision and translation support should always
      // define a revision data table.
      if ($this->revisionTable && !empty($this->entityInfo['revision_data_table'])) {
        $this->revisionDataTable = $this->entityInfo['revision_data_table'];
      }
73 74
    }

75 76 77 78
    // Work-a-round to let load() get stdClass storage records without having to
    // override it. We map storage records to entities in
    // DatabaseStorageControllerNG:: mapFromStorageRecords().
    // @todo: Remove this once this is moved in the main controller.
79
    unset($this->entityInfo['class']);
80 81 82 83 84 85 86 87 88 89
  }

  /**
   * Overrides DatabaseStorageController::create().
   *
   * @param array $values
   *   An array of values to set, keyed by field name. The value has to be
   *   the plain value of an entity field, i.e. an array of field items.
   *   If no numerically indexed array is given, the value will be set for the
   *   first field item. For example, to set the first item of a 'name'
90
   *   field one can pass:
91 92 93 94 95 96 97 98
   *   @code
   *     $values = array('name' => array(0 => array('value' => 'the name')));
   *   @endcode
   *   or
   *   @code
   *     $values = array('name' => array('value' => 'the name'));
   *   @endcode
   *   If the 'name' field is a defined as 'string_item' which supports
99 100
   *   setting its value by a string, it's also possible to just pass the name
   *   string:
101 102 103 104
   *   @code
   *     $values = array('name' => 'the name');
   *   @endcode
   *
105
   * @return \Drupal\Core\Entity\EntityInterface
106 107 108
   *   A new entity object.
   */
  public function create(array $values) {
109 110 111
    $entity_class = $this->entityClass;
    $entity_class::preCreate($this, $values);

112
    // We have to determine the bundle first.
113 114 115
    $bundle = FALSE;
    if ($this->bundleKey) {
      if (!isset($values[$this->bundleKey])) {
116
        throw new EntityStorageException(format_string('Missing bundle for entity type @type', array('@type' => $this->entityType)));
117 118 119
      }
      $bundle = $values[$this->bundleKey];
    }
120
    $entity = new $this->entityClass(array(), $this->entityType, $bundle);
121

122 123 124 125 126 127 128 129
    foreach ($entity as $name => $field) {
      if (isset($values[$name])) {
        $entity->$name = $values[$name];
      }
      elseif (!array_key_exists($name, $values)) {
        $entity->get($name)->applyDefaultValue();
      }
      unset($values[$name]);
130 131
    }

132 133 134
    // Set any passed values for non-defined fields also.
    foreach ($values as $name => $value) {
      $entity->$name = $value;
135
    }
136
    $entity->postCreate($this);
137 138 139 140 141

    // Modules might need to add or change the data initially held by the new
    // entity object, for instance to fill-in default values.
    $this->invokeHook('create', $entity);

142 143 144
    return $entity;
  }

145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  /**
   * Builds an entity query.
   *
   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
   *   EntityQuery instance.
   * @param array $values
   *   An associative array of properties of the entity, where the keys are the
   *   property names and the values are the values those properties must have.
   */
  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
    if ($this->dataTable) {
      // @todo We should not be using a condition to specify whether conditions
      //   apply to the default language. See http://drupal.org/node/1866330.
      // Default to the original entity language if not explicitly specified
      // otherwise.
      if (!array_key_exists('default_langcode', $values)) {
        $values['default_langcode'] = 1;
      }
      // If the 'default_langcode' flag is explicitly not set, we do not care
      // whether the queried values are in the original entity language or not.
      elseif ($values['default_langcode'] === NULL) {
        unset($values['default_langcode']);
      }
    }

    parent::buildPropertyQuery($entity_query, $values);
  }

173 174 175 176 177 178
  /**
   * Overrides DatabaseStorageController::attachLoad().
   *
   * Added mapping from storage records to entities.
   */
  protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
179
    // Map the loaded stdclass records into entity objects and according fields.
180
    $queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
181
    parent::attachLoad($queried_entities, $load_revision);
182 183 184 185 186
  }

  /**
   * Maps from storage records to entity objects.
   *
187 188 189 190 191
   * @param array $records
   *   Associative array of query results, keyed on the entity ID.
   * @param boolean $load_revision
   *   (optional) TRUE if the revision should be loaded, defaults to FALSE.
   *
192 193 194
   * @return array
   *   An array of entity objects implementing the EntityInterface.
   */
195
  protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
196
    $entities = array();
197
    foreach ($records as $id => $record) {
198
      $entities[$id] = array();
199
      foreach ($record as $name => $value) {
200 201 202
        // Skip the item delta and item value levels but let the field assign
        // the value as suiting. This avoids unnecessary array hierarchies and
        // saves memory here.
203 204 205 206 207 208 209 210
        $entities[$id][$name][Language::LANGCODE_DEFAULT] = $value;
      }
      // If we have no multilingual values we can instantiate entity objecs
      // right now, otherwise we need to collect all the field values first.
      if (!$this->dataTable) {
        $bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE;
        // Turn the record into an entity class.
        $entities[$id] = new $this->entityClass($entities[$id], $this->entityType, $bundle);
211
      }
212 213 214 215 216 217 218 219 220 221
    }
    $this->attachPropertyData($entities, $load_revision);
    return $entities;
  }

  /**
   * Attaches property data in all languages for translatable properties.
   *
   * @param array &$entities
   *   Associative array of entities, keyed on the entity ID.
222 223
   * @param int $revision_id
   *   (optional) The revision to be loaded. Defaults to FALSE.
224
   */
225
  protected function attachPropertyData(array &$entities, $revision_id = FALSE) {
226
    if ($this->dataTable) {
227 228
      // If a revision table is available, we need all the properties of the
      // latest revision. Otherwise we fall back to the data table.
229
      $table = $this->revisionDataTable ?: $this->dataTable;
230
      $query = $this->database->select($table, 'data', array('fetch' => \PDO::FETCH_ASSOC))
231 232 233
        ->fields('data')
        ->condition($this->idKey, array_keys($entities))
        ->orderBy('data.' . $this->idKey);
234

235
      if ($this->revisionDataTable) {
236 237 238 239 240 241
        if ($revision_id) {
          $query->condition($this->revisionKey, $revision_id);
        }
        else {
          // Get the revision IDs.
          $revision_ids = array();
242
          foreach ($entities as $values) {
243 244 245
            $revision_ids[] = $values[$this->revisionKey];
          }
          $query->condition($this->revisionKey, $revision_ids);
246 247 248
        }
      }

249
      $data = $query->execute();
250
      $field_definition = \Drupal::entityManager()->getFieldDefinitions($this->entityType);
251
      $translations = array();
252 253
      if ($this->revisionDataTable) {
        $data_fields = array_flip(array_diff(drupal_schema_fields_sql($this->entityInfo['revision_data_table']), drupal_schema_fields_sql($this->entityInfo['base_table'])));
254 255 256 257
      }
      else {
        $data_fields = array_flip(drupal_schema_fields_sql($this->entityInfo['data_table']));
      }
258 259 260

      foreach ($data as $values) {
        $id = $values[$this->idKey];
261

262 263 264
        // Field values in default language are stored with
        // Language::LANGCODE_DEFAULT as key.
        $langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT;
265
        $translations[$id][$langcode] = TRUE;
266 267

        foreach ($field_definition as $name => $definition) {
268
          if (isset($data_fields[$name])) {
269
            $entities[$id][$name][$langcode] = $values[$name];
270 271 272
          }
        }
      }
273 274 275 276

      foreach ($entities as $id => $values) {
        $bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE;
        // Turn the record into an entity class.
277
        $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle, array_keys($translations[$id]));
278
      }
279 280 281 282 283 284 285 286 287
    }
  }

  /**
   * Overrides DatabaseStorageController::save().
   *
   * Added mapping from entities to storage records before saving.
   */
  public function save(EntityInterface $entity) {
288
    $transaction = $this->database->startTransaction();
289
    try {
290 291 292
      // Sync the changes made in the fields array to the internal values array.
      $entity->updateOriginalValues();

293 294 295 296 297
      // Load the stored entity, if any.
      if (!$entity->isNew() && !isset($entity->original)) {
        $entity->original = entity_load_unchanged($this->entityType, $entity->id());
      }

298
      $entity->preSave($this);
299
      $this->invokeFieldMethod('preSave', $entity);
300 301 302
      $this->invokeHook('presave', $entity);

      // Create the storage record to be saved.
303
      $record = $this->mapToStorageRecord($entity);
304 305

      if (!$entity->isNew()) {
306 307 308 309 310 311 312 313 314 315 316
        if ($entity->isDefaultRevision()) {
          $return = drupal_write_record($this->entityInfo['base_table'], $record, $this->idKey);
        }
        else {
          // @todo, should a different value be returned when saving an entity
          // with $isDefaultRevision = FALSE?
          $return = FALSE;
        }
        if ($this->revisionKey) {
          $record->{$this->revisionKey} = $this->saveRevision($entity);
        }
317 318 319
        if ($this->dataTable) {
          $this->savePropertyData($entity);
        }
320 321 322
        if ($this->revisionDataTable) {
          $this->savePropertyData($entity, 'revision_data_table');
        }
323
        $this->resetCache(array($entity->id()));
324
        $entity->postSave($this, TRUE);
325
        $this->invokeFieldMethod('update', $entity);
326
        $this->saveFieldItems($entity, TRUE);
327
        $this->invokeHook('update', $entity);
328 329 330
        if ($this->dataTable) {
          $this->invokeTranslationHooks($entity);
        }
331 332
      }
      else {
333 334 335
        // Ensure the entity is still seen as new after assigning it an id,
        // while storing its data.
        $entity->enforceIsNew();
336
        $return = drupal_write_record($this->entityInfo['base_table'], $record);
337
        $entity->{$this->idKey}->value = (string) $record->{$this->idKey};
338
        if ($this->revisionKey) {
339
          $entity->setNewRevision();
340 341
          $record->{$this->revisionKey} = $this->saveRevision($entity);
        }
342 343 344
        if ($this->dataTable) {
          $this->savePropertyData($entity);
        }
345 346 347
        if ($this->revisionDataTable) {
          $this->savePropertyData($entity, 'revision_data_table');
        }
348

349 350 351 352
        // Reset general caches, but keep caches specific to certain entities.
        $this->resetCache(array());

        $entity->enforceIsNew(FALSE);
353
        $entity->postSave($this, FALSE);
354
        $this->invokeFieldMethod('insert', $entity);
355
        $this->saveFieldItems($entity, FALSE);
356 357 358 359 360 361 362 363 364
        $this->invokeHook('insert', $entity);
      }

      // Ignore slave server temporarily.
      db_ignore_slave();
      unset($entity->original);

      return $return;
    }
365
    catch (\Exception $e) {
366 367 368 369 370 371
      $transaction->rollback();
      watchdog_exception($this->entityType, $e);
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

372 373 374 375 376 377 378 379 380 381
  /**
   * Saves an entity revision.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @return integer
   *   The revision id.
   */
  protected function saveRevision(EntityInterface $entity) {
382
    $record = $this->mapToStorageRecord($entity, 'revision_table');
383

384 385 386 387 388
    // When saving a new revision, set any existing revision ID to NULL so as
    // to ensure that a new revision will actually be created.
    if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) {
      $record->{$this->revisionKey} = NULL;
    }
389

390
    $entity->preSaveRevision($this, $record);
391

392 393 394 395 396 397 398
    if ($entity->isNewRevision()) {
      drupal_write_record($this->revisionTable, $record);
      if ($entity->isDefaultRevision()) {
        $this->database->update($this->entityInfo['base_table'])
          ->fields(array($this->revisionKey => $record->{$this->revisionKey}))
          ->condition($this->idKey, $record->{$this->idKey})
          ->execute();
399
      }
400 401 402 403
      $entity->setNewRevision(FALSE);
    }
    else {
      drupal_write_record($this->revisionTable, $record, array($this->revisionKey));
404
    }
405

406 407 408 409
    // Make sure to update the new revision key for the entity.
    $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};

    return $record->{$this->revisionKey};
410 411
  }

412 413 414 415 416
  /**
   * Stores the entity property language-aware data.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
417 418 419
   * @param string $table_key
   *   (optional) The entity key identifying the target table. Defaults to
   *   'data_table'.
420
   */
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
  protected function savePropertyData(EntityInterface $entity, $table_key = NULL) {
    $revision = TRUE;
    if (!isset($table_key)) {
      $table_key = 'data_table';
      $revision = FALSE;
    }
    $table_name = $this->entityInfo[$table_key];

    if (!$revision || !$entity->isNewRevision()) {
      $key = $revision ? $this->revisionKey : $this->idKey;
      $value = $revision ? $entity->getRevisionId() : $entity->id();
      // Delete and insert to handle removed values.
      $this->database->delete($table_name)
        ->condition($key, $value)
        ->execute();
    }
437

438
    $query = $this->database->insert($table_name);
439 440

    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
441 442
      $translation = $entity->getTranslation($langcode);
      $record = $this->mapToDataStorageRecord($translation, $table_key);
443 444 445 446 447 448 449 450 451
      $values = (array) $record;
      $query
        ->fields(array_keys($values))
        ->values($values);
    }

    $query->execute();
  }

452
  /**
453
   * Maps from an entity object to the storage record.
454 455 456
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
457 458 459
   * @param string $table_key
   *   (optional) The entity key identifying the target table. Defaults to
   *   'base_table'.
460 461 462
   *
   * @return \stdClass
   *   The record to store.
463
   */
464
  protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'base_table') {
465
    $record = new \stdClass();
466
    $definitions = $entity->getPropertyDefinitions();
467 468 469 470 471 472 473 474 475 476
    $schema = drupal_get_schema($this->entityInfo[$table_key]);
    $is_new = $entity->isNew();

    foreach (drupal_schema_fields_sql($this->entityInfo[$table_key]) as $name) {
      $info = $schema['fields'][$name];
      $value = isset($definitions[$name]) && isset($entity->$name->value) ? $entity->$name->value : NULL;
      // If we are creating a new entity, we must not populate the record with
      // NULL values otherwise defaults would not be applied.
      if (isset($value) || !$is_new) {
        $record->$name = drupal_schema_get_field_value($info, $value);
477
      }
478
    }
479

480 481
    return $record;
  }
482 483

  /**
484
   * Maps from an entity object to the storage record of the field data.
485 486 487
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
488 489 490
   * @param string $table_key
   *   (optional) The entity key identifying the target table. Defaults to
   *   'data_table'.
491 492 493 494
   *
   * @return \stdClass
   *   The record to store.
   */
495 496 497 498
  protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') {
    $record = $this->mapToStorageRecord($entity, $table_key);
    $record->langcode = $entity->language()->id;
    $record->default_langcode = intval($record->langcode == $entity->getUntranslated()->language()->id);
499 500 501 502 503 504 505 506 507 508 509 510
    return $record;
  }

  /**
   * Overwrites \Drupal\Core\Entity\DatabaseStorageController::delete().
   */
  public function delete(array $entities) {
    if (!$entities) {
      // If no IDs or invalid IDs were passed, do nothing.
      return;
    }

511
    $transaction = $this->database->startTransaction();
512
    try {
513 514 515
      $entity_class = $this->entityClass;
      $entity_class::preDelete($this, $entities);

516
      foreach ($entities as $entity) {
517 518 519 520
        $this->invokeHook('predelete', $entity);
      }
      $ids = array_keys($entities);

521
      $this->database->delete($this->entityInfo['base_table'])
522 523 524 525
        ->condition($this->idKey, $ids)
        ->execute();

      if ($this->revisionKey) {
526
        $this->database->delete($this->revisionTable)
527 528 529 530 531
          ->condition($this->idKey, $ids)
          ->execute();
      }

      if ($this->dataTable) {
532
        $this->database->delete($this->dataTable)
533 534 535 536
          ->condition($this->idKey, $ids)
          ->execute();
      }

537 538 539 540 541 542
      if ($this->revisionDataTable) {
        $this->database->delete($this->revisionDataTable)
          ->condition($this->idKey, $ids)
          ->execute();
      }

543 544 545
      // Reset the cache as soon as the changes have been applied.
      $this->resetCache($ids);

546
      $entity_class::postDelete($this, $entities);
547
      foreach ($entities as $entity) {
548
        $this->invokeFieldMethod('delete', $entity);
549
        $this->deleteFieldItems($entity);
550 551 552 553 554
        $this->invokeHook('delete', $entity);
      }
      // Ignore slave server temporarily.
      db_ignore_slave();
    }
555
    catch (\Exception $e) {
556 557
      $transaction->rollback();
      watchdog_exception($this->entityType, $e);
558
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
559 560
    }
  }
561
}