View source
<?php
namespace Drupal\config_split;
use Drupal\Component\FileSecurity\FileSecurity;
use Drupal\config_split\Config\ConfigPatch;
use Drupal\config_split\Config\ConfigPatchMerge;
use Drupal\config_split\Config\SplitCollectionStorage;
use Drupal\config_split\Entity\ConfigSplitEntity;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Config\MemoryStorage;
use Drupal\Core\Config\StorageCopyTrait;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\StorageTransformEvent;
use Drupal\Core\Database\Connection;
final class ConfigSplitManager {
use StorageCopyTrait;
const SPLIT_PARTIAL_PREFIX = 'config_split.patch.';
private $factory;
private $connection;
private $active;
private $sync;
private $export;
private $manager;
private $patchMerge;
public function __construct(ConfigFactoryInterface $factory, ConfigManagerInterface $manager, StorageInterface $active, StorageInterface $sync, StorageInterface $export, Connection $connection, ConfigPatchMerge $patchMerge) {
$this->factory = $factory;
$this->sync = $sync;
$this->active = $active;
$this->export = $export;
$this->connection = $connection;
$this->manager = $manager;
$this->patchMerge = $patchMerge;
}
public function getSplitConfig(string $name, StorageInterface $storage = NULL) : ?ImmutableConfig {
if (strpos($name, 'config_split.config_split.') !== 0) {
$name = 'config_split.config_split.' . $name;
}
if (!in_array($name, $this->factory
->listAll('config_split.config_split.'), TRUE)) {
return NULL;
}
return $this->factory
->get($name);
}
public function getSplitEntity(string $name) : ?ConfigSplitEntity {
$config = $this
->getSplitConfig($name);
if ($config === NULL) {
return NULL;
}
$entity = $this->manager
->loadConfigEntityByName($config
->getName());
if ($entity instanceof ConfigSplitEntity) {
return $entity;
}
throw new \RuntimeException('A split config does not load a split entity? something is very wrong.');
}
public function exportTransform(string $name, StorageTransformEvent $event) : void {
$split = $this
->getSplitConfig($name);
if ($split === NULL) {
return;
}
if (!$split
->get('status')) {
return;
}
$storage = $event
->getStorage();
$preview = $this
->getPreviewStorage($split, $storage);
if ($preview !== NULL) {
$this
->splitPreview($split, $storage, $preview);
}
}
public function importTransform(string $name, StorageTransformEvent $event) : void {
$split = $this
->getSplitConfig($name);
if ($split === NULL) {
return;
}
if (!$split
->get('status')) {
return;
}
$storage = $event
->getStorage();
$secondary = $this
->getSplitStorage($split, $storage);
if ($secondary !== NULL) {
$this
->mergeSplit($split, $storage, $secondary);
}
}
public function commitAll() : void {
$splits = $this->factory
->loadMultiple($this->factory
->listAll('config_split'));
$splits = array_filter($splits, function (ImmutableConfig $config) {
return $config
->get('status');
});
foreach ($splits as $split) {
$preview = $this
->getPreviewStorage($split);
$permanent = $this
->getSplitStorage($split);
if ($preview !== NULL && $permanent !== NULL) {
self::replaceStorageContents($preview, $permanent);
}
}
}
public function splitPreview(ImmutableConfig $config, StorageInterface $transforming, StorageInterface $splitStorage) : void {
foreach (array_merge([
StorageInterface::DEFAULT_COLLECTION,
], $splitStorage
->getAllCollectionNames()) as $collection) {
$splitStorage
->createCollection($collection)
->deleteAll();
}
$transforming = $transforming
->createCollection(StorageInterface::DEFAULT_COLLECTION);
$splitStorage = $splitStorage
->createCollection(StorageInterface::DEFAULT_COLLECTION);
$modules = array_keys($config
->get('module'));
$changes = $this->manager
->getConfigEntitiesToChangeOnDependencyRemoval('module', $modules, FALSE);
$this
->processEntitiesToChangeOnDependencyRemoval($changes, $transforming, $splitStorage);
$completelySplit = array_map(function (ConfigEntityInterface $entity) {
return $entity
->getConfigDependencyName();
}, $changes['delete']);
foreach ($modules as $module) {
$keys = $this->active
->listAll($module . '.');
$keys = array_diff($keys, $completelySplit);
foreach ($keys as $name) {
$splitStorage
->write($name, $this->active
->read($name));
$transforming
->delete($name);
$completelySplit[] = $name;
}
}
$completeSplitList = $config
->get('complete_list');
if (!empty($completeSplitList)) {
$completeList = array_filter($this->active
->listAll(), function ($name) use ($completeSplitList) {
return self::inFilterList($name, $completeSplitList);
});
$completeList = array_diff($completeList, $completelySplit);
$changes = $this->manager
->getConfigEntitiesToChangeOnDependencyRemoval('config', $completeList, FALSE);
$this
->processEntitiesToChangeOnDependencyRemoval($changes, $transforming, $splitStorage);
$processed = array_map(function (ConfigEntityInterface $entity) {
return $entity
->getConfigDependencyName();
}, $changes['delete']);
$unprocessed = array_diff($completeList, $processed);
foreach ($unprocessed as $name) {
$splitStorage
->write($name, $this->active
->read($name));
$transforming
->delete($name);
}
}
if (!empty($completelySplit) || !empty($completeSplitList)) {
foreach ($this->active
->getAllCollectionNames() as $collection) {
$storageCollection = $transforming
->createCollection($collection);
$splitCollection = $splitStorage
->createCollection($collection);
$activeCollection = $this->active
->createCollection($collection);
$removeList = array_filter($activeCollection
->listAll(), function ($name) use ($completeSplitList, $completelySplit) {
return in_array($name, $completelySplit) || self::inFilterList($name, $completeSplitList);
});
foreach ($removeList as $name) {
$splitCollection
->write($name, $activeCollection
->read($name));
$storageCollection
->delete($name);
}
}
}
$partialSplitList = $config
->get('partial_list');
if (!empty($partialSplitList)) {
foreach (array_merge([
StorageInterface::DEFAULT_COLLECTION,
], $transforming
->getAllCollectionNames()) as $collection) {
$syncCollection = $this->sync
->createCollection($collection);
$activeCollection = $this->active
->createCollection($collection);
$storageCollection = $transforming
->createCollection($collection);
$splitCollection = $splitStorage
->createCollection($collection);
$partialList = array_filter($activeCollection
->listAll(), function ($name) use ($partialSplitList, $completelySplit) {
return !in_array($name, $completelySplit) && self::inFilterList($name, $partialSplitList);
});
foreach ($partialList as $name) {
if ($syncCollection
->exists($name)) {
$sync = $syncCollection
->read($name);
$active = $activeCollection
->read($name);
if ($splitCollection
->exists(self::SPLIT_PARTIAL_PREFIX . $name)) {
$patch = ConfigPatch::fromArray($splitCollection
->read(self::SPLIT_PARTIAL_PREFIX . $name));
$sync = $this->patchMerge
->mergePatch($sync, $patch, $name);
}
$diff = $this->patchMerge
->createPatch($active, $sync);
if (!$diff
->isEmpty()) {
$splitCollection
->write(self::SPLIT_PARTIAL_PREFIX . $name, $diff
->toArray());
$storageCollection
->write($name, $sync);
}
}
else {
$splitCollection
->write($name, $activeCollection
->read($name));
$storageCollection
->delete($name);
if ($splitStorage
->exists(self::SPLIT_PARTIAL_PREFIX . $name)) {
$splitStorage
->delete(self::SPLIT_PARTIAL_PREFIX . $name);
}
}
}
}
}
$extensions = $transforming
->read('core.extension');
if ($extensions === FALSE) {
return;
}
$extensions['module'] = array_diff_key($extensions['module'], $config
->get('module') ?? []);
$extensions['theme'] = array_diff_key($extensions['theme'], $config
->get('theme') ?? []);
$transforming
->write('core.extension', $extensions);
}
public function mergeSplit(ImmutableConfig $config, StorageInterface $transforming, StorageInterface $splitStorage) : void {
$transforming = $transforming
->createCollection(StorageInterface::DEFAULT_COLLECTION);
$splitStorage = $splitStorage
->createCollection(StorageInterface::DEFAULT_COLLECTION);
foreach (array_merge([
StorageInterface::DEFAULT_COLLECTION,
], $splitStorage
->getAllCollectionNames()) as $collection) {
$split = $splitStorage
->createCollection($collection);
$storage = $transforming
->createCollection($collection);
foreach ($split
->listAll() as $name) {
$data = $split
->read($name);
if ($data !== FALSE) {
if (strpos($name, self::SPLIT_PARTIAL_PREFIX) === 0) {
$name = substr($name, strlen(self::SPLIT_PARTIAL_PREFIX));
$diff = ConfigPatch::fromArray($data);
if ($storage
->exists($name)) {
$data = $storage
->read($name);
$data = $this->patchMerge
->mergePatch($data, $diff
->invert(), $name);
$storage
->write($name, $data);
}
}
else {
$storage
->write($name, $data);
}
}
}
}
if ($config
->get('storage') === 'collection') {
$collectionStorage = new SplitCollectionStorage($transforming, $config
->get('id'));
foreach (array_merge([
StorageInterface::DEFAULT_COLLECTION,
], $collectionStorage
->getAllCollectionNames()) as $collection) {
$collectionStorage
->createCollection($collection)
->deleteAll();
}
}
$extensions = $transforming
->read('core.extension');
if ($extensions === FALSE) {
return;
}
$updated = $transforming
->read($config
->getName());
if ($updated === FALSE) {
return;
}
$extensions['module'] = array_merge($extensions['module'], $updated['module'] ?? []);
$extensions['theme'] = array_merge($extensions['theme'], $updated['theme'] ?? []);
$sorted = $extensions['module'];
uksort($sorted, function ($a, $b) use ($sorted) {
if ($sorted[$a] != $sorted[$b]) {
return $sorted[$a] > $sorted[$b] ? 1 : -1;
}
return $a > $b ? 1 : -1;
});
$extensions['module'] = $sorted;
$transforming
->write('core.extension', $extensions);
}
protected function getSplitStorage(ImmutableConfig $config, StorageInterface $transforming = NULL) : ?StorageInterface {
$storage = $config
->get('storage');
if ('collection' === $storage) {
if ($transforming instanceof StorageInterface) {
return new SplitCollectionStorage($transforming, $config
->get('id'));
}
return NULL;
}
if ('folder' === $storage) {
$directory = $config
->get('folder');
if (!is_dir($directory)) {
@mkdir($directory, 0777, TRUE);
}
if (file_exists($directory) && is_writable($directory)) {
$htaccess_path = rtrim($directory, '/\\') . '/.htaccess';
if (!file_exists($htaccess_path)) {
file_put_contents($htaccess_path, FileSecurity::htaccessLines(TRUE));
@chmod($htaccess_path, 0444);
}
}
if (file_exists($directory) || strpos($directory, 'vfs://') === 0) {
return new FileStorage($directory);
}
return NULL;
}
return new DatabaseStorage($this->connection, $this->connection
->escapeTable(strtr($config
->getName(), [
'.' => '_',
])));
}
public function getPreviewStorage(ImmutableConfig $config, StorageInterface $transforming = NULL) : ?StorageInterface {
if ('collection' === $config
->get('storage')) {
if ($transforming instanceof StorageInterface) {
return new SplitCollectionStorage($transforming, $config
->get('id'));
}
return NULL;
}
$name = substr($config
->getName(), strlen('config_split.config_split.'));
$name = 'config_split_preview_' . strtr($name, [
'.' => '_',
]);
return new DatabaseStorage($this->connection, $this->connection
->escapeTable($name));
}
public function singleExportPreview(ImmutableConfig $split) : StorageInterface {
$this->export
->listAll();
$preview = $this
->getPreviewStorage($split, $this->export);
if (!$split
->get('status') && $preview !== NULL) {
$transforming = new MemoryStorage();
self::replaceStorageContents($this->export, $transforming);
$this
->splitPreview($split, $transforming, $preview);
}
if ($preview === NULL) {
throw new \RuntimeException();
}
return $preview;
}
public function singleExportTarget(ImmutableConfig $split) : StorageInterface {
$permanent = $this
->getSplitStorage($split, $this->sync);
if ($permanent === NULL) {
throw new \RuntimeException();
}
return $permanent;
}
public function singleImport(ImmutableConfig $split, bool $activate) : StorageInterface {
$storage = $this
->getSplitStorage($split, $this->sync);
return $this
->singleImportOrActivate($split, $storage, $activate);
}
public function singleActivate(ImmutableConfig $split, bool $activate) : StorageInterface {
$storage = $this
->getSplitStorage($split, $this->active);
return $this
->singleImportOrActivate($split, $storage, $activate);
}
public function singleDeactivate(ImmutableConfig $split, bool $exportSplit = FALSE, $override = FALSE) : StorageInterface {
if (!$split
->get('status') && !$override) {
throw new \InvalidArgumentException('Split is already not active.');
}
$transformation = new MemoryStorage();
static::replaceStorageContents($this->active, $transformation);
$preview = $this
->getPreviewStorage($split, $transformation);
if ($preview === NULL) {
throw new \RuntimeException();
}
$this
->splitPreview($split, $transformation, $preview);
if ($exportSplit) {
$permanent = $this
->getSplitStorage($split, $this->sync);
if ($permanent === NULL) {
throw new \RuntimeException();
}
static::replaceStorageContents($preview, $permanent);
}
$config = $transformation
->read($split
->getName());
if ($config !== FALSE && !$override) {
$config['status'] = FALSE;
$transformation
->write($split
->getName(), $config);
}
return $transformation;
}
protected function singleImportOrActivate(ImmutableConfig $split, StorageInterface $storage, bool $activate) : StorageInterface {
$transformation = new MemoryStorage();
static::replaceStorageContents($this->active, $transformation);
$this
->mergeSplit($split, $transformation, $storage);
$config = $transformation
->read($split
->getName());
if ($activate && $config !== FALSE) {
$config['status'] = TRUE;
$transformation
->write($split
->getName(), $config);
}
return $transformation;
}
protected function processEntitiesToChangeOnDependencyRemoval(array $changes, StorageInterface $storage, StorageInterface $split) {
foreach ($changes['update'] as $entity) {
$name = $entity
->getConfigDependencyName();
$original = $this->active
->read($name);
$updated = $entity
->toArray();
$diff = $this->patchMerge
->createPatch($original, $updated);
if (!$diff
->isEmpty()) {
$split
->write(self::SPLIT_PARTIAL_PREFIX . $name, $diff
->toArray());
$data = $storage
->read($name);
$data = $this->patchMerge
->mergePatch($data, $diff);
$storage
->write($name, $data);
}
}
foreach ($changes['delete'] as $entity) {
$name = $entity
->getConfigDependencyName();
$split
->write($name, $this->active
->read($name));
$storage
->delete($name);
}
}
protected static function inFilterList($name, array $list) {
$list = array_map(function ($line) {
return str_replace('\\*', '.*', preg_quote($line, '/'));
}, $list);
foreach ($list as $line) {
if (preg_match('/^' . $line . '$/', $name)) {
return TRUE;
}
}
return FALSE;
}
}