DatabaseStorageControllerNG.php 18.2 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 12
use PDO;

13
use Drupal\Core\Entity\Query\QueryInterface;
14 15 16 17
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\DatabaseStorageController;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Component\Uuid\Uuid;
18
use Drupal\Core\Database\Connection;
19 20 21 22 23 24

/**
 * Implements Field API specific enhancements to the DatabaseStorageController class.
 *
 * @todo: Once all entity types have been converted, merge improvements into the
 * DatabaseStorageController class.
25 26 27 28
 *
 * See the EntityNG documentation for an explanation of "NG".
 *
 * @see \Drupal\Core\EntityNG
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
 */
class DatabaseStorageControllerNG extends DatabaseStorageController {

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

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

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

53 54 55
  /**
   * Overrides DatabaseStorageController::__construct().
   */
56 57
  public function __construct($entity_type, array $entity_info, Connection $database) {
    parent::__construct($entity_type,$entity_info, $database);
58 59
    $this->bundleKey = !empty($this->entityInfo['entity_keys']['bundle']) ? $this->entityInfo['entity_keys']['bundle'] : FALSE;
    $this->entityClass = $this->entityInfo['class'];
60

61 62 63 64 65
    // Check if the entity type has a dedicated table for properties.
    if (!empty($this->entityInfo['data_table'])) {
      $this->dataTable = $this->entityInfo['data_table'];
    }

66 67 68 69
    // 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.
70
    unset($this->entityInfo['class']);
71 72 73 74 75 76 77 78 79 80
  }

  /**
   * 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'
81
   *   field one can pass:
82 83 84 85 86 87 88 89
   *   @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
90 91
   *   setting its value by a string, it's also possible to just pass the name
   *   string:
92 93 94 95
   *   @code
   *     $values = array('name' => 'the name');
   *   @endcode
   *
96
   * @return \Drupal\Core\Entity\EntityInterface
97 98 99
   *   A new entity object.
   */
  public function create(array $values) {
100
    // We have to determine the bundle first.
101 102 103 104 105 106 107
    $bundle = FALSE;
    if ($this->bundleKey) {
      if (!isset($values[$this->bundleKey])) {
        throw new EntityStorageException(t('Missing bundle for entity type @type', array('@type' => $this->entityType)));
      }
      $bundle = $values[$this->bundleKey];
    }
108
    $entity = new $this->entityClass(array(), $this->entityType, $bundle);
109 110 111 112 113 114 115

    // Set all other given values.
    foreach ($values as $name => $value) {
      $entity->$name = $value;
    }

    // Assign a new UUID if there is none yet.
116
    if ($this->uuidKey && !isset($entity->{$this->uuidKey}->value)) {
117
      $uuid = new Uuid();
118
      $entity->{$this->uuidKey} = $uuid->generate();
119
    }
120 121 122 123 124

    // 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);

125 126 127
    return $entity;
  }

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
  /**
   * 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);
  }

156 157 158 159 160 161
  /**
   * Overrides DatabaseStorageController::attachLoad().
   *
   * Added mapping from storage records to entities.
   */
  protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
162
    // Map the loaded stdclass records into entity objects and according fields.
163
    $queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
164 165 166

    if ($this->entityInfo['fieldable']) {
      if ($load_revision) {
167
        field_attach_load_revision($this->entityType, $queried_entities);
168 169
      }
      else {
170
        field_attach_load($this->entityType, $queried_entities);
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
      }
    }

    // Call hook_entity_load().
    foreach (module_implements('entity_load') as $module) {
      $function = $module . '_entity_load';
      $function($queried_entities, $this->entityType);
    }
    // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
    // always the queried entities, followed by additional arguments set in
    // $this->hookLoadArguments.
    $args = array_merge(array($queried_entities), $this->hookLoadArguments);
    foreach (module_implements($this->entityType . '_load') as $module) {
      call_user_func_array($module . '_' . $this->entityType . '_load', $args);
    }
  }

  /**
   * Maps from storage records to entity objects.
   *
191 192 193 194 195
   * @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.
   *
196 197 198
   * @return array
   *   An array of entity objects implementing the EntityInterface.
   */
199
  protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
200
    $entities = array();
201
    foreach ($records as $id => $record) {
202
      $values = array();
203
      foreach ($record as $name => $value) {
204 205 206
        // 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.
207
        $values[$name][Language::LANGCODE_DEFAULT] = $value;
208
      }
209 210
      $bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE;
      // Turn the record into an entity class.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
      $entities[$id] = new $this->entityClass($values, $this->entityType, $bundle);
    }
    $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.
   * @param boolean $load_revision
   *   (optional) TRUE if the revision should be loaded, defaults to FALSE.
   */
  protected function attachPropertyData(array &$entities, $load_revision = FALSE) {
    if ($this->dataTable) {
227
      $query = $this->database->select($this->dataTable, 'data', array('fetch' => PDO::FETCH_ASSOC))
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        ->fields('data')
        ->condition($this->idKey, array_keys($entities))
        ->orderBy('data.' . $this->idKey);
      if ($load_revision) {
        // Get revision ID's.
        $revision_ids = array();
        foreach ($entities as $id => $entity) {
          $revision_ids[] = $entity->get($this->revisionKey)->value;
        }
        $query->condition($this->revisionKey, $revision_ids);
      }
      $data = $query->execute();

      // Fetch the field definitions to check which field is translatable.
      $field_definition = $this->getFieldDefinitions(array());
243
      $data_fields = array_flip(drupal_schema_fields_sql($this->entityInfo['data_table']));
244 245 246

      foreach ($data as $values) {
        $id = $values[$this->idKey];
247 248 249
        // Field values in default language are stored with
        // Language::LANGCODE_DEFAULT as key.
        $langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT;
250 251 252 253 254
        $translation = $entities[$id]->getTranslation($langcode);

        foreach ($field_definition as $name => $definition) {
          // Set translatable properties only.
          if (isset($data_fields[$name]) && !empty($definition['translatable'])) {
255 256 257 258
            // @todo Figure out how to determine which property has to be set.
            // Currently it's guessing, and guessing is evil!
            $property_definition = $translation->{$name}->getPropertyDefinitions();
            $translation->{$name}->{key($property_definition)} = $values[$name];
259 260 261 262 263 264 265
          }
          // Avoid initializing configurable fields before loading them.
          elseif (!empty($definition['configurable'])) {
            unset($entities[$id]->fields[$name]);
          }
        }
      }
266 267 268 269 270 271 272 273 274
    }
  }

  /**
   * Overrides DatabaseStorageController::save().
   *
   * Added mapping from entities to storage records before saving.
   */
  public function save(EntityInterface $entity) {
275
    $transaction = $this->database->startTransaction();
276
    try {
277
      // Ensure we are dealing with the actual entity.
278
      $entity = $entity->getNGEntity();
279 280 281 282

      // Sync the changes made in the fields array to the internal values array.
      $entity->updateOriginalValues();

283 284 285 286 287 288 289 290 291
      // Load the stored entity, if any.
      if (!$entity->isNew() && !isset($entity->original)) {
        $entity->original = entity_load_unchanged($this->entityType, $entity->id());
      }

      $this->preSave($entity);
      $this->invokeHook('presave', $entity);

      // Create the storage record to be saved.
292
      $record = $this->mapToStorageRecord($entity);
293 294

      if (!$entity->isNew()) {
295 296 297 298 299 300 301 302 303 304 305
        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);
        }
306 307 308
        if ($this->dataTable) {
          $this->savePropertyData($entity);
        }
309 310 311 312 313
        $this->resetCache(array($entity->id()));
        $this->postSave($entity, TRUE);
        $this->invokeHook('update', $entity);
      }
      else {
314
        $return = drupal_write_record($this->entityInfo['base_table'], $record);
315
        $entity->{$this->idKey}->value = $record->{$this->idKey};
316 317 318
        if ($this->revisionKey) {
          $record->{$this->revisionKey} = $this->saveRevision($entity);
        }
319 320 321 322 323
        $entity->{$this->idKey}->value = $record->{$this->idKey};
        if ($this->dataTable) {
          $this->savePropertyData($entity);
        }

324 325 326 327 328 329 330 331 332 333 334 335 336 337
        // Reset general caches, but keep caches specific to certain entities.
        $this->resetCache(array());

        $entity->enforceIsNew(FALSE);
        $this->postSave($entity, FALSE);
        $this->invokeHook('insert', $entity);
      }

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

      return $return;
    }
338
    catch (\Exception $e) {
339 340 341 342 343 344
      $transaction->rollback();
      watchdog_exception($this->entityType, $e);
      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
    }
  }

345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
  /**
   * Saves an entity revision.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @return integer
   *   The revision id.
   */
  protected function saveRevision(EntityInterface $entity) {
    $record = $this->mapToRevisionStorageRecord($entity);

    // 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;
    }

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

    if ($entity->isNewRevision()) {
      drupal_write_record($this->revisionTable, $record);
      if ($entity->isDefaultRevision()) {
368
        $this->database->update($this->entityInfo['base_table'])
369 370 371 372 373 374 375 376 377 378 379 380 381 382
          ->fields(array($this->revisionKey => $record->{$this->revisionKey}))
          ->condition($this->idKey, $record->{$this->idKey})
          ->execute();
      }
      $entity->setNewRevision(FALSE);
    }
    else {
      drupal_write_record($this->revisionTable, $record, $this->revisionKey);
    }
    // Make sure to update the new revision key for the entity.
    $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
    return $record->{$this->revisionKey};
  }

383 384 385 386 387 388 389 390
  /**
   * Stores the entity property language-aware data.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   */
  protected function savePropertyData(EntityInterface $entity) {
    // Delete and insert to handle removed values.
391
    $this->database->delete($this->dataTable)
392 393 394
      ->condition($this->idKey, $entity->id())
      ->execute();

395
    $query = $this->database->insert($this->dataTable);
396 397 398 399 400 401 402 403 404 405 406 407

    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
      $record = $this->mapToDataStorageRecord($entity, $langcode);
      $values = (array) $record;
      $query
        ->fields(array_keys($values))
        ->values($values);
    }

    $query->execute();
  }

408 409 410
  /**
   * Overrides DatabaseStorageController::invokeHook().
   *
411
   * Invokes field API attachers with a BC entity.
412 413
   */
  protected function invokeHook($hook, EntityInterface $entity) {
414 415 416 417 418 419 420
    $function = 'field_attach_' . $hook;
    // @todo: field_attach_delete_revision() is named the wrong way round,
    // consider renaming it.
    if ($function == 'field_attach_revision_delete') {
      $function = 'field_attach_delete_revision';
    }
    if (!empty($this->entityInfo['fieldable']) && function_exists($function)) {
421
      $function($entity);
422 423 424 425 426 427 428 429 430 431
    }

    // Invoke the hook.
    module_invoke_all($this->entityType . '_' . $hook, $entity);
    // Invoke the respective entity-level hook.
    module_invoke_all('entity_' . $hook, $entity, $this->entityType);
  }

  /**
   * Maps from an entity object to the storage record of the base table.
432 433 434 435 436 437
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @return \stdClass
   *   The record to store.
438 439 440
   */
  protected function mapToStorageRecord(EntityInterface $entity) {
    $record = new \stdClass();
441
    foreach (drupal_schema_fields_sql($this->entityInfo['base_table']) as $name) {
442 443 444 445
      $record->$name = $entity->$name->value;
    }
    return $record;
  }
446 447 448

  /**
   * Maps from an entity object to the storage record of the revision table.
449 450 451 452 453 454
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @return \stdClass
   *   The record to store.
455 456 457
   */
  protected function mapToRevisionStorageRecord(EntityInterface $entity) {
    $record = new \stdClass();
458
    foreach (drupal_schema_fields_sql($this->entityInfo['revision_table']) as $name) {
459 460 461
      if (isset($entity->$name->value)) {
        $record->$name = $entity->$name->value;
      }
462 463 464
    }
    return $record;
  }
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

  /**
   * Maps from an entity object to the storage record of the data table.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   * @param $langcode
   *   The language code of the translation to get.
   *
   * @return \stdClass
   *   The record to store.
   */
  protected function mapToDataStorageRecord(EntityInterface $entity, $langcode) {
    $default_langcode = $entity->language()->langcode;
    // Don't use strict mode, this way there's no need to do checks here, as
    // non-translatable properties are replicated for each language.
    $translation = $entity->getTranslation($langcode, FALSE);

    $record = new \stdClass();
484
    foreach (drupal_schema_fields_sql($this->entityInfo['data_table']) as $name) {
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
      $record->$name = $translation->$name->value;
    }
    $record->langcode = $langcode;
    $record->default_langcode = intval($default_langcode == $langcode);

    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;
    }

502
    $transaction = $this->database->startTransaction();
503
    try {
504 505
      // Ensure we are dealing with the actual entities.
      foreach ($entities as $id => $entity) {
506
        $entities[$id] = $entity->getNGEntity();
507 508
      }

509 510 511 512 513 514
      $this->preDelete($entities);
      foreach ($entities as $id => $entity) {
        $this->invokeHook('predelete', $entity);
      }
      $ids = array_keys($entities);

515
      $this->database->delete($this->entityInfo['base_table'])
516 517 518 519
        ->condition($this->idKey, $ids)
        ->execute();

      if ($this->revisionKey) {
520
        $this->database->delete($this->revisionTable)
521 522 523 524 525
          ->condition($this->idKey, $ids)
          ->execute();
      }

      if ($this->dataTable) {
526
        $this->database->delete($this->dataTable)
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
          ->condition($this->idKey, $ids)
          ->execute();
      }

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

      $this->postDelete($entities);
      foreach ($entities as $id => $entity) {
        $this->invokeHook('delete', $entity);
      }
      // Ignore slave server temporarily.
      db_ignore_slave();
    }
    catch (Exception $e) {
      $transaction->rollback();
      watchdog_exception($this->entityType, $e);
      throw new EntityStorageException($e->getMessage, $e->getCode, $e);
    }
  }
547
}