class CheckpointStorage
Same name in other branches
- 10 core/lib/Drupal/Core/Config/Checkpoint/CheckpointStorage.php \Drupal\Core\Config\Checkpoint\CheckpointStorage
Provides a config storage that can make checkpoints.
This storage wraps the active storage, and provides the ability to take checkpoints. Once a checkpoint has been created all configuration operations made after the checkpoint will be recorded, so it is possible to revert to original state when the checkpoint was taken.
This class cannot be used to checkpoint another storage since it relies on events triggered by the configuration system in order to work. It is the responsibility of the caller to construct this class with the active storage.
@internal This API is experimental.
Hierarchy
- class \Drupal\Core\Config\Checkpoint\CheckpointStorage implements \Drupal\Core\Config\Checkpoint\CheckpointStorageInterface, \Symfony\Component\EventDispatcher\EventSubscriberInterface, \Psr\Log\LoggerAwareInterface uses \Psr\Log\LoggerAwareTrait
Expanded class hierarchy of CheckpointStorage
1 file declares its use of CheckpointStorage
- CheckpointStorageTest.php in core/
tests/ Drupal/ Tests/ Core/ Config/ Checkpoint/ CheckpointStorageTest.php
File
-
core/
lib/ Drupal/ Core/ Config/ Checkpoint/ CheckpointStorage.php, line 35
Namespace
Drupal\Core\Config\CheckpointView source
final class CheckpointStorage implements CheckpointStorageInterface, EventSubscriberInterface, LoggerAwareInterface {
use LoggerAwareTrait;
/**
* Used as prefix to a config checkpoint collection.
*
* If this code is copied in order to checkpoint a different storage then
* this value must be changed.
*/
private const KEY_VALUE_COLLECTION_PREFIX = 'config.checkpoint.';
/**
* Used to store the list of collections in each checkpoint.
*
* Note this cannot be a valid configuration name.
*
* @see \Drupal\Core\Config\ConfigBase::validateName()
*/
private const CONFIG_COLLECTION_KEY = 'collections';
/**
* The key value stores that store configuration changed for each checkpoint.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface[]
*/
private array $keyValueStores;
/**
* The checkpoint to read from.
*
* @var \Drupal\Core\Config\Checkpoint\Checkpoint|null
*/
private ?Checkpoint $readFromCheckpoint = NULL;
/**
* Constructs a CheckpointStorage object.
*
* @param \Drupal\Core\Config\StorageInterface $activeStorage
* The active configuration storage.
* @param \Drupal\Core\Config\Checkpoint\CheckpointListInterface $checkpoints
* The list of checkpoints.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $keyValueFactory
* The key value factory.
* @param string $collection
* (optional) The configuration collection.
*/
public function __construct(StorageInterface $activeStorage, CheckpointListInterface $checkpoints, KeyValueFactoryInterface $keyValueFactory, string $collection = StorageInterface::DEFAULT_COLLECTION) {
}
/**
* {@inheritdoc}
*/
public function exists($name) {
if (count($this->checkpoints) === 0) {
throw new NoCheckpointsException();
}
foreach ($this->getCheckpointsToReadFrom() as $checkpoint) {
$in_checkpoint = $this->getKeyValue($checkpoint->id, $this->collection)
->get($name);
if ($in_checkpoint !== NULL) {
// If $in_checkpoint is FALSE then the configuration has been deleted.
return $in_checkpoint !== FALSE;
}
}
return $this->activeStorage
->exists($name);
}
/**
* {@inheritdoc}
*/
public function read($name) {
$return = $this->readMultiple([
$name,
]);
return $return[$name] ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
if (count($this->checkpoints) === 0) {
throw new NoCheckpointsException();
}
$return = [];
foreach ($this->getCheckpointsToReadFrom() as $checkpoint) {
$return = array_merge($return, $this->getKeyValue($checkpoint->id, $this->collection)
->getMultiple($names));
// Remove the read names from the list to fetch.
$names = array_diff($names, array_keys($return));
if (empty($names)) {
// All the configuration has been read. Nothing more to do.
break;
}
}
// Names not found in the checkpoints have not been modified: read from
// active storage.
if (!empty($names)) {
$return = array_merge($return, $this->activeStorage
->readMultiple($names));
}
// Remove any renamed or new configuration (FALSE has been recorded for
// these operations in the checkpoint).
// @see ::onConfigRename()
// @see ::onConfigSaveAndDelete()
return array_filter($return);
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->activeStorage
->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->activeStorage
->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
if (count($this->checkpoints) === 0) {
throw new NoCheckpointsException();
}
$names = $new_configuration = [];
foreach ($this->getCheckpointsToReadFrom() as $checkpoint) {
$checkpoint_names = array_keys(array_filter($this->getKeyValue($checkpoint->id, $this->collection)
->getAll(), function (mixed $value, string $name) use (&$new_configuration, $prefix) {
if ($name === static::CONFIG_COLLECTION_KEY) {
return FALSE;
}
// Remove any that don't start with the prefix.
if ($prefix !== '' && !str_starts_with($name, $prefix)) {
return FALSE;
}
// We've determined in a previous checkpoint that the configuration did
// not exist.
if (in_array($name, $new_configuration, TRUE)) {
return FALSE;
}
// If the value is FALSE then the configuration was created after the
// checkpoint.
if ($value === FALSE) {
$new_configuration[] = $name;
return FALSE;
}
return TRUE;
}, ARRAY_FILTER_USE_BOTH));
$names = array_merge($names, $checkpoint_names);
}
// Remove any names that did not exist prior to the checkpoint.
$active_names = array_diff($this->activeStorage
->listAll($prefix), $new_configuration);
$names = array_unique(array_merge($names, $active_names));
sort($names);
return $names;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
$collection = new self($this->activeStorage
->createCollection($collection), $this->checkpoints, $this->keyValueFactory, $collection);
// \Drupal\Core\Config\Checkpoint\CheckpointStorage::$readFromCheckpoint is
// assigned by reference so that it is consistent across all collection
// objects created from the same initial object.
$collection->readFromCheckpoint =& $this->readFromCheckpoint;
return $collection;
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
$names = [];
foreach ($this->getCheckpointsToReadFrom() as $checkpoint) {
$names = array_merge($names, $this->getKeyValue($checkpoint->id, StorageInterface::DEFAULT_COLLECTION)
->get(static::CONFIG_COLLECTION_KEY, []));
}
return array_unique(array_merge($this->activeStorage
->getAllCollectionNames(), $names));
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->collection;
}
/**
* {@inheritdoc}
*/
public function checkpoint(string|\Stringable $label) : Checkpoint {
// Generate a new ID based on the state of the current active checkpoint.
$active_checkpoint = $this->checkpoints
->getActiveCheckpoint();
if (!$active_checkpoint instanceof Checkpoint) {
// @todo https://www.drupal.org/i/3408525 Consider options for generating
// a real fingerprint.
$id = hash('sha1', random_bytes(32));
return $this->checkpoints
->add($id, $label);
}
// Determine if we need to create a new checkpoint by checking if
// configuration has changed since the last checkpoint.
$collections = $this->getAllCollectionNames();
$collections[] = StorageInterface::DEFAULT_COLLECTION;
foreach ($collections as $collection) {
$current_checkpoint_data[$collection] = $this->getKeyValue($active_checkpoint->id, $collection)
->getAll();
// Remove the collections key because it is irrelevant.
unset($current_checkpoint_data[$collection][static::CONFIG_COLLECTION_KEY]);
// If there is no data in the collection then there is no need to hash
// the empty array.
if (empty($current_checkpoint_data[$collection])) {
unset($current_checkpoint_data[$collection]);
}
}
if (!empty($current_checkpoint_data)) {
// Use json_encode() here because it is both quicker and results in
// smaller output than serialize().
$id = hash('sha1', ($active_checkpoint->parent ?? '') . json_encode($current_checkpoint_data));
return $this->checkpoints
->add($id, $label);
}
$this->logger?->notice('A backup checkpoint was not created because nothing has changed since the "{active}" checkpoint was created.', [
'active' => $active_checkpoint->label,
]);
return $active_checkpoint;
}
/**
* {@inheritdoc}
*/
public function setCheckpointToReadFrom(string|Checkpoint $checkpoint_id) : static {
if ($checkpoint_id instanceof Checkpoint) {
$checkpoint_id = $checkpoint_id->id;
}
$this->readFromCheckpoint = $this->checkpoints
->get($checkpoint_id);
return $this;
}
/**
* Gets the key value storage for the provided checkpoint.
*
* @param string $checkpoint
* The checkpoint to get the key value storage for.
* @param string $collection
* The config collection to get the key value storage for.
*
* @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
* The key value storage for the provided checkpoint.
*/
private function getKeyValue(string $checkpoint, string $collection) : KeyValueStoreInterface {
$checkpoint_key = $checkpoint;
if ($collection !== StorageInterface::DEFAULT_COLLECTION) {
$checkpoint_key = $collection . '.' . $checkpoint_key;
}
return $this->keyValueStores[$checkpoint_key] ??= $this->keyValueFactory
->get(self::KEY_VALUE_COLLECTION_PREFIX . $checkpoint_key);
}
/**
* Gets the checkpoints to read from.
*
* @return \Traversable<string, \Drupal\Core\Config\Checkpoint\Checkpoint>
* The checkpoints, keyed by ID.
*/
private function getCheckpointsToReadFrom() : \Traversable {
$checkpoint = $this->checkpoints
->getActiveCheckpoint();
/** @var \Drupal\Core\Config\Checkpoint\Checkpoint[] $checkpoints_to_read_from */
$checkpoints_to_read_from = [
$checkpoint,
];
if ($checkpoint->id !== $this->readFromCheckpoint?->id) {
// Follow ancestors to find the checkpoint to start reading from.
foreach ($this->checkpoints
->getParents($checkpoint->id) as $checkpoint) {
array_unshift($checkpoints_to_read_from, $checkpoint);
if ($checkpoint->id === $this->readFromCheckpoint?->id) {
break;
}
}
}
// Replay in parent to child order.
foreach ($checkpoints_to_read_from as $checkpoint) {
(yield $checkpoint->id => $checkpoint);
}
}
/**
* Updates checkpoint when configuration is saved.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSaveAndDelete(ConfigCrudEvent $event) : void {
$active_checkpoint = $this->checkpoints
->getActiveCheckpoint();
if ($active_checkpoint === NULL) {
return;
}
$saved_config = $event->getConfig();
$collection = $saved_config->getStorage()
->getCollectionName();
$this->storeCollectionName($collection);
$key_value = $this->getKeyValue($active_checkpoint->id, $collection);
// If we have not yet stored a checkpoint for this configuration we should.
if ($key_value->get($saved_config->getName()) === NULL) {
$original_data = $this->getOriginalConfig($saved_config);
// An empty array indicates that the config has to be new as a sequence
// cannot be the root of a config object. We need to make this assumption
// because $saved_config->isNew() will always return FALSE here.
if (empty($original_data)) {
$original_data = FALSE;
}
// Only save change to state if there is a change, even if it's just keys
// being re-ordered.
if ($original_data !== $saved_config->getRawData()) {
$key_value->set($saved_config->getName(), $original_data);
}
}
}
/**
* Updates checkpoint when configuration is saved.
*
* @param \Drupal\Core\Config\ConfigRenameEvent $event
* The configuration event.
*/
public function onConfigRename(ConfigRenameEvent $event) : void {
$active_checkpoint = $this->checkpoints
->getActiveCheckpoint();
if ($active_checkpoint === NULL) {
return;
}
$collection = $event->getConfig()
->getStorage()
->getCollectionName();
$this->storeCollectionName($collection);
$key_value = $this->getKeyValue($active_checkpoint->id, $collection);
$old_name = $event->getOldName();
// If we have not yet stored a checkpoint for this configuration, store a
// complete copy of the original configuration. Note that renames do not
// change data but storing the complete data allows
// \Drupal\Core\Config\ConfigImporter to track renames using UUIDs.
if ($key_value->get($old_name) === NULL) {
$key_value->set($old_name, $this->getOriginalConfig($event->getConfig()));
}
// Record that the new name did not exist prior to the checkpoint.
$new_name = $event->getConfig()
->getName();
if ($key_value->get($new_name) === NULL) {
$key_value->set($new_name, FALSE);
}
}
/**
* Gets the original data from the configuration.
*
* @param \Drupal\Core\Config\StorableConfigBase $config
* The config to get the original data from.
*
* @return mixed
* The original data.
*/
private function getOriginalConfig(StorableConfigBase $config) : mixed {
if ($config instanceof Config) {
return $config->getOriginal(apply_overrides: FALSE);
}
return $config->getOriginal();
}
/**
* Stores the collection name so the storage knows its own collections.
*
* @param string $collection
* The name of the collection.
*/
private function storeCollectionName(string $collection) : void {
// We do not need to store the default collection.
if ($collection === StorageInterface::DEFAULT_COLLECTION) {
return;
}
$key_value = $this->getKeyValue($this->checkpoints
->getActiveCheckpoint()->id, StorageInterface::DEFAULT_COLLECTION);
$collections = $key_value->get(static::CONFIG_COLLECTION_KEY, []);
assert(is_array($collections));
if (in_array($collection, $collections, TRUE)) {
return;
}
$collections[] = $collection;
$key_value->set(static::CONFIG_COLLECTION_KEY, $collections);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() : array {
$events[ConfigEvents::SAVE][] = 'onConfigSaveAndDelete';
$events[ConfigEvents::DELETE][] = 'onConfigSaveAndDelete';
$events[ConfigEvents::RENAME][] = 'onConfigRename';
$events[ConfigCollectionEvents::SAVE_IN_COLLECTION][] = 'onConfigSaveAndDelete';
$events[ConfigCollectionEvents::DELETE_IN_COLLECTION][] = 'onConfigSaveAndDelete';
$events[ConfigCollectionEvents::RENAME_IN_COLLECTION][] = 'onConfigRename';
return $events;
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) : never {
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a CheckpointStorage');
}
/**
* {@inheritdoc}
*/
public function delete($name) : never {
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a CheckpointStorage');
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) : never {
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a CheckpointStorage');
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') : never {
throw new \BadMethodCallException(__METHOD__ . ' is not allowed on a CheckpointStorage');
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overriden Title |
---|---|---|---|---|
CheckpointStorage::$keyValueStores | private | property | The key value stores that store configuration changed for each checkpoint. | |
CheckpointStorage::$readFromCheckpoint | private | property | The checkpoint to read from. | |
CheckpointStorage::checkpoint | public | function | Creates a checkpoint, if required, and returns the active checkpoint. | Overrides CheckpointStorageInterface::checkpoint |
CheckpointStorage::CONFIG_COLLECTION_KEY | private | constant | Used to store the list of collections in each checkpoint. | |
CheckpointStorage::createCollection | public | function | Creates a collection on the storage. | Overrides StorageInterface::createCollection |
CheckpointStorage::decode | public | function | Decodes configuration data from the storage-specific format. | Overrides StorageInterface::decode |
CheckpointStorage::delete | public | function | Deletes a configuration object from the storage. | Overrides StorageInterface::delete |
CheckpointStorage::deleteAll | public | function | Deletes configuration objects whose names start with a given prefix. | Overrides StorageInterface::deleteAll |
CheckpointStorage::encode | public | function | Encodes configuration data into the storage-specific format. | Overrides StorageInterface::encode |
CheckpointStorage::exists | public | function | Returns whether a configuration object exists. | Overrides StorageInterface::exists |
CheckpointStorage::getAllCollectionNames | public | function | Gets the existing collections. | Overrides StorageInterface::getAllCollectionNames |
CheckpointStorage::getCheckpointsToReadFrom | private | function | Gets the checkpoints to read from. | |
CheckpointStorage::getCollectionName | public | function | Gets the name of the current collection the storage is using. | Overrides StorageInterface::getCollectionName |
CheckpointStorage::getKeyValue | private | function | Gets the key value storage for the provided checkpoint. | |
CheckpointStorage::getOriginalConfig | private | function | Gets the original data from the configuration. | |
CheckpointStorage::getSubscribedEvents | public static | function | ||
CheckpointStorage::KEY_VALUE_COLLECTION_PREFIX | private | constant | Used as prefix to a config checkpoint collection. | |
CheckpointStorage::listAll | public | function | Gets configuration object names starting with a given prefix. | Overrides StorageInterface::listAll |
CheckpointStorage::onConfigRename | public | function | Updates checkpoint when configuration is saved. | |
CheckpointStorage::onConfigSaveAndDelete | public | function | Updates checkpoint when configuration is saved. | |
CheckpointStorage::read | public | function | Reads configuration data from the storage. | Overrides StorageInterface::read |
CheckpointStorage::readMultiple | public | function | Reads configuration data from the storage. | Overrides StorageInterface::readMultiple |
CheckpointStorage::rename | public | function | Renames a configuration object in the storage. | Overrides StorageInterface::rename |
CheckpointStorage::setCheckpointToReadFrom | public | function | Sets the checkpoint to read from. | Overrides CheckpointStorageInterface::setCheckpointToReadFrom |
CheckpointStorage::storeCollectionName | private | function | Stores the collection name so the storage knows its own collections. | |
CheckpointStorage::write | public | function | Writes configuration data to the storage. | Overrides StorageInterface::write |
CheckpointStorage::__construct | public | function | Constructs a CheckpointStorage object. | |
StorageInterface::DEFAULT_COLLECTION | constant | The default collection name. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.