ConfigStorageController.php 13 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Definition of Drupal\Core\Config\Entity\ConfigStorageController.
6 7
 */

8
namespace Drupal\Core\Config\Entity;
9

10
use Drupal\Component\Utility\String;
11
use Drupal\Core\Config\ConfigFactoryInterface;
12
use Drupal\Core\Entity\EntityInterface;
13
use Drupal\Core\Entity\EntityMalformedException;
14
use Drupal\Core\Entity\EntityStorageControllerBase;
15
use Drupal\Core\Config\Config;
16
use Drupal\Core\Config\StorageInterface;
17
use Drupal\Core\Entity\EntityTypeInterface;
18
use Drupal\Core\Entity\EntityStorageException;
19
use Drupal\Core\Entity\Query\QueryFactory;
20
use Drupal\Component\Uuid\UuidInterface;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22 23

/**
24
 * Defines the storage controller class for configuration entities.
25 26 27 28 29 30 31 32 33 34 35 36
 *
 * Configuration object names of configuration entities are comprised of two
 * parts, separated by a dot:
 * - config_prefix: A string denoting the owner (module/extension) of the
 *   configuration object, followed by arbitrary other namespace identifiers
 *   that are declared by the owning extension; e.g., 'node.type'. The
 *   config_prefix does NOT contain a trailing dot. It is defined by the entity
 *   type's annotation.
 * - ID: A string denoting the entity ID within the entity type namespace; e.g.,
 *   'article'. Entity IDs may contain dots/periods. The entire remaining string
 *   after the config_prefix in a config name forms the entity ID. Additional or
 *   custom suffixes are not possible.
37
 */
38
class ConfigStorageController extends EntityStorageControllerBase implements ConfigStorageControllerInterface {
39

40 41 42 43 44 45 46
  /**
   * The UUID service.
   *
   * @var \Drupal\Component\Uuid\UuidInterface
   */
  protected $uuidService;

47 48 49 50 51 52 53
  /**
   * Name of the entity's status key or FALSE if a status is not supported.
   *
   * @var string|bool
   */
  protected $statusKey = 'status';

54
  /**
55
   * The config factory service.
56
   *
57
   * @var \Drupal\Core\Config\ConfigFactoryInterface
58
   */
59 60 61 62 63 64 65 66 67 68 69 70
  protected $configFactory;

  /**
   * The config storage service.
   *
   * @var \Drupal\Core\Config\StorageInterface
   */
  protected $configStorage;

  /**
   * Constructs a ConfigStorageController object.
   *
71 72
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
73
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
74 75 76
   *   The config factory service.
   * @param \Drupal\Core\Config\StorageInterface $config_storage
   *   The config storage service.
77 78
   * @param \Drupal\Component\Uuid\UuidInterface $uuid_service
   *   The UUID service.
79
   */
80
  public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, StorageInterface $config_storage, UuidInterface $uuid_service) {
81
    parent::__construct($entity_type);
82

83 84
    $this->idKey = $this->entityType->getKey('id');
    $this->statusKey = $this->entityType->getKey('status');
85 86 87

    $this->configFactory = $config_factory;
    $this->configStorage = $config_storage;
88
    $this->uuidService = $uuid_service;
89 90 91 92 93
  }

  /**
   * {@inheritdoc}
   */
94
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
95
    return new static(
96
      $entity_type,
97
      $container->get('config.factory'),
98
      $container->get('config.storage'),
99
      $container->get('uuid')
100
    );
101 102 103
  }

  /**
104
   * {@inheritdoc}
105
   */
106
  public function loadMultiple(array $ids = NULL) {
107 108 109 110 111 112 113 114 115 116 117 118 119
    $entities = array();

    // Create a new variable which is either a prepared version of the $ids
    // array for later comparison with the entity cache, or FALSE if no $ids
    // were passed.
    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;

    // Load any remaining entities. This is the case if $ids is set to NULL (so
    // we load all entities).
    if ($ids === NULL || $ids) {
      $queried_entities = $this->buildQuery($ids);
    }

120
    // Pass all entities loaded from the database through $this->postLoad(),
121 122 123
    // which calls the
    // entity type specific load callback, for example hook_node_type_load().
    if (!empty($queried_entities)) {
124
      $this->postLoad($queried_entities);
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
      $entities += $queried_entities;
    }

    // Ensure that the returned array is ordered the same as the original
    // $ids array if this was passed in and remove any invalid ids.
    if ($passed_ids) {
      // Remove any invalid ids from the array.
      $passed_ids = array_intersect_key($passed_ids, $entities);
      foreach ($entities as $entity) {
        $passed_ids[$entity->{$this->idKey}] = $entity;
      }
      $entities = $passed_ids;
    }

    return $entities;
  }

142 143 144 145 146 147 148 149
  /**
   * {@inheritdoc}
   */
  public function load($id) {
    $entities = $this->loadMultiple(array($id));
    return isset($entities[$id]) ? $entities[$id] : NULL;
  }

150
  /**
151
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::loadRevision().
152 153 154 155 156
   */
  public function loadRevision($revision_id) {
    return FALSE;
  }

157 158 159 160 161 162 163
  /**
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::deleteRevision().
   */
  public function deleteRevision($revision_id) {
    return NULL;
  }

164
  /**
165
   * {@inheritdoc}
166 167
   */
  public function getConfigPrefix() {
168
    return $this->entityType->getConfigPrefix() . '.';
169 170
  }

171
  /**
172
   * {@inheritdoc}
173 174 175 176 177
   */
  public static function getIDFromConfigName($config_name, $config_prefix) {
    return substr($config_name, strlen($config_prefix . '.'));
  }

178 179 180 181 182 183 184 185 186 187 188 189 190
  /**
   * Builds the query to load the entity.
   *
   * This has full revision support. For entities requiring special queries,
   * the class can be extended, and the default query can be constructed by
   * calling parent::buildQuery(). This is usually necessary when the object
   * being loaded needs to be augmented with additional data from another
   * table, such as loading node type into comments or vocabulary machine name
   * into terms, however it can also support $conditions on different tables.
   * See Drupal\comment\CommentStorageController::buildQuery() or
   * Drupal\taxonomy\TermStorageController::buildQuery() for examples.
   *
   * @param $ids
191
   *   An array of entity IDs, or NULL to load all entities.
192 193 194 195 196 197 198 199
   * @param $revision_id
   *   The ID of the revision to load, or FALSE if this query is asking for the
   *   most current revision(s).
   *
   * @return SelectQuery
   *   A SelectQuery object for loading the entity.
   */
  protected function buildQuery($ids, $revision_id = FALSE) {
200
    $config_class = $this->entityType->getClass();
201
    $prefix = $this->getConfigPrefix();
202

203
    // Get the names of the configuration entities we are going to load.
204
    if ($ids === NULL) {
205
      $names = $this->configStorage->listAll($prefix);
206 207
    }
    else {
208
      $names = array();
209
      foreach ($ids as $id) {
210
        // Add the prefix to the ID to serve as the configuration object name.
211
        $names[] = $prefix . $id;
212 213
      }
    }
214 215 216 217

    // Load all of the configuration entities.
    $result = array();
    foreach ($this->configFactory->loadMultiple($names) as $config) {
218
      $result[$config->get($this->idKey)] = new $config_class($config->get(), $this->entityTypeId);
219 220
    }
    return $result;
221 222 223
  }

  /**
224
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::create().
225
   */
226
  public function create(array $values = array()) {
227
    $class = $this->entityType->getClass();
228
    $class::preCreate($this, $values);
229

230
    // Set default language to site default if not provided.
231
    $values += array('langcode' => language_default()->id);
232

233
    $entity = new $class($values, $this->entityTypeId);
234 235 236
    // Mark this entity as new, so isNew() returns TRUE. This does not check
    // whether a configuration entity with the same ID (if any) already exists.
    $entity->enforceIsNew();
237 238

    // Assign a new UUID if there is none yet.
239 240
    if (!$entity->uuid()) {
      $entity->set('uuid', $this->uuidService->generate());
241
    }
242
    $entity->postCreate($this);
243

244 245 246 247
    // 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);

248 249 250 251 252
    // Default status to enabled.
    if (!empty($this->statusKey) && !isset($entity->{$this->statusKey})) {
      $entity->{$this->statusKey} = TRUE;
    }

253 254 255 256
    return $entity;
  }

  /**
257
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::delete().
258
   */
259
  public function delete(array $entities) {
260 261 262 263 264
    if (!$entities) {
      // If no IDs or invalid IDs were passed, do nothing.
      return;
    }

265
    $entity_class = $this->entityType->getClass();
266
    $entity_class::preDelete($this, $entities);
267
    foreach ($entities as $entity) {
268 269 270
      $this->invokeHook('predelete', $entity);
    }

271
    foreach ($entities as $entity) {
272
      $config = $this->configFactory->get($this->getConfigPrefix() . $entity->id());
273 274 275
      $config->delete();
    }

276
    $entity_class::postDelete($this, $entities);
277
    foreach ($entities as $entity) {
278 279 280 281 282
      $this->invokeHook('delete', $entity);
    }
  }

  /**
283
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::save().
284 285 286
   *
   * @throws EntityMalformedException
   *   When attempting to save a configuration entity that has no ID.
287
   */
288
  public function save(EntityInterface $entity) {
289
    $prefix = $this->getConfigPrefix();
290

291 292 293 294 295 296
    // Configuration entity IDs are strings, and '0' is a valid ID.
    $id = $entity->id();
    if ($id === NULL || $id === '') {
      throw new EntityMalformedException('The entity does not have an ID.');
    }

297
    // Load the stored entity, if any.
298
    // At this point, the original ID can only be NULL or a valid ID.
299 300
    if ($entity->getOriginalId() !== NULL) {
      $id = $entity->getOriginalId();
301
    }
302
    $config = $this->configFactory->get($prefix . $id);
303

304 305
    // Prevent overwriting an existing configuration file if the entity is new.
    if ($entity->isNew() && !$config->isNew()) {
306
      throw new EntityStorageException(String::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
307 308 309
    }

    if (!$config->isNew() && !isset($entity->original)) {
310
      $this->resetCache(array($id));
311
      $entity->original = $this->load($id);
312 313
    }

314 315 316 317 318
    if ($id !== $entity->id()) {
      // Renaming a config object needs to cater for:
      // - Storage controller needs to access the original object.
      // - The object needs to be renamed/copied in ConfigFactory and reloaded.
      // - All instances of the object need to be renamed.
319
      $config = $this->configFactory->rename($prefix . $id, $prefix . $entity->id());
320
    }
321

322 323 324 325 326
    // Build an ID if none is set.
    if (!isset($entity->{$this->idKey})) {
      $entity->{$this->idKey} = $entity->id();
    }

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

330
    // Retrieve the desired properties and set them in config.
331
    foreach ($entity->getExportProperties() as $key => $value) {
332
      $config->set($key, $value);
333 334
    }

335
    if (!$config->isNew()) {
336
      $return = SAVED_UPDATED;
337
      $config->save();
338
      $entity->postSave($this, TRUE);
339
      $this->invokeHook('update', $entity);
340 341

      // Immediately update the original ID.
342
      $entity->setOriginalId($entity->id());
343 344
    }
    else {
345
      $return = SAVED_NEW;
346 347
      $config->save();
      $entity->enforceIsNew(FALSE);
348
      $entity->postSave($this, FALSE);
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
      $this->invokeHook('insert', $entity);
    }

    unset($entity->original);

    return $return;
  }

  /**
   * Invokes a hook on behalf of the entity.
   *
   * @param $hook
   *   One of 'presave', 'insert', 'update', 'predelete', or 'delete'.
   * @param $entity
   *   The entity object.
   */
365
  protected function invokeHook($hook, EntityInterface $entity) {
366
    // Invoke the hook.
367
    $this->moduleHandler->invokeAll($this->entityTypeId . '_' . $hook, array($entity));
368
    // Invoke the respective entity-level hook.
369
    $this->moduleHandler->invokeAll('entity_' . $hook, array($entity, $this->entityTypeId));
370
  }
371 372 373 374 375

  /**
   * Implements Drupal\Core\Entity\EntityStorageControllerInterface::getQueryServicename().
   */
  public function getQueryServicename() {
376
    return 'entity.query.config';
377
  }
378 379

  /**
380
   * {@inheritdoc}
381 382 383
   */
  public function importCreate($name, Config $new_config, Config $old_config) {
    $entity = $this->create($new_config->get());
384
    $entity->setSyncing(TRUE);
385 386 387 388 389
    $entity->save();
    return TRUE;
  }

  /**
390
   * {@inheritdoc}
391
   */
392
  public function importUpdate($name, Config $new_config, Config $old_config) {
393
    $id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
394
    $entity = $this->load($id);
395
    $entity->setSyncing(TRUE);
396 397 398
    $entity->original = clone $entity;

    foreach ($old_config->get() as $property => $value) {
399
      $entity->original->set($property, $value);
400 401 402
    }

    foreach ($new_config->get() as $property => $value) {
403
      $entity->set($property, $value);
404 405 406 407 408 409 410
    }

    $entity->save();
    return TRUE;
  }

  /**
411
   * {@inheritdoc}
412 413
   */
  public function importDelete($name, Config $new_config, Config $old_config) {
414
    $id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix());
415
    $entity = $this->load($id);
416
    $entity->setSyncing(TRUE);
417 418 419 420
    $entity->delete();
    return TRUE;
  }

421
}